Skip to content

Commit 405d143

Browse files
committed
blueprint_rules: simplify error handling
Also simplifies code in general.
1 parent 43ab4ca commit 405d143

File tree

4 files changed

+128
-181
lines changed

4 files changed

+128
-181
lines changed

internal/v1/blueprint_rules.go

Lines changed: 77 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package v1
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
"path/filepath"
@@ -13,30 +14,30 @@ import (
1314

1415
var (
1516
blueprintNameRegex = regexp.MustCompile(`\S+`)
16-
blueprintInvalidNameDetail = "The blueprint name must contain at least two characters."
17+
blueprintInvalidNameDetail = "the blueprint name must contain at least two characters"
18+
ErrBlueprintValidation = errors.New("blueprint validation failed")
1719
)
1820

19-
// RuleViolationError represents a rule violation that should be returned as HTTP 422
20-
type RuleViolationError struct {
21-
HTTPErrorList HTTPErrorList
21+
// validationError wraps an error with HTTP response details
22+
type validationError struct {
23+
title string
24+
detail string
25+
err error
2226
}
2327

24-
func (e RuleViolationError) Error() string {
25-
if len(e.HTTPErrorList.Errors) > 0 {
26-
return e.HTTPErrorList.Errors[0].Detail
27-
}
28-
return "rule violation"
28+
func (ve validationError) Error() string {
29+
return ve.detail
30+
}
31+
32+
func (ve validationError) Unwrap() error {
33+
return ve.err
2934
}
3035

31-
// newRuleViolation creates a RuleViolationError with the given title and detail
32-
func newRuleViolation(title, detail string) RuleViolationError {
33-
return RuleViolationError{
34-
HTTPErrorList: HTTPErrorList{
35-
Errors: []HTTPError{{
36-
Title: title,
37-
Detail: detail,
38-
}},
39-
},
36+
func newValidationError(title, detail string) error {
37+
return validationError{
38+
title: title,
39+
detail: detail,
40+
err: ErrBlueprintValidation,
4041
}
4142
}
4243

@@ -54,115 +55,73 @@ func parseOctalMode(modeStr *string) *os.FileMode {
5455
}
5556

5657
// parseFileUser extracts user from File union type
57-
func parseFileUser(fileUser *File_User) interface{} {
58+
func parseFileUser(fileUser *File_User) any {
5859
if fileUser == nil {
5960
return nil
6061
}
6162

6263
if userStr, err := fileUser.AsFileUser0(); err == nil {
6364
return userStr
64-
} else if userInt, err := fileUser.AsFileUser1(); err == nil {
65+
}
66+
if userInt, err := fileUser.AsFileUser1(); err == nil {
6567
return userInt
6668
}
6769
return nil
6870
}
6971

7072
// parseFileGroup extracts group from File union type
71-
func parseFileGroup(fileGroup *File_Group) interface{} {
73+
func parseFileGroup(fileGroup *File_Group) any {
7274
if fileGroup == nil {
7375
return nil
7476
}
7577

7678
if groupStr, err := fileGroup.AsFileGroup0(); err == nil {
7779
return groupStr
78-
} else if groupInt, err := fileGroup.AsFileGroup1(); err == nil {
80+
}
81+
if groupInt, err := fileGroup.AsFileGroup1(); err == nil {
7982
return groupInt
8083
}
8184
return nil
8285
}
8386

8487
// parseDirectoryUser extracts user from Directory union type
85-
func parseDirectoryUser(dirUser *Directory_User) interface{} {
88+
func parseDirectoryUser(dirUser *Directory_User) any {
8689
if dirUser == nil {
8790
return nil
8891
}
8992

9093
if userStr, err := dirUser.AsDirectoryUser0(); err == nil {
9194
return userStr
92-
} else if userInt, err := dirUser.AsDirectoryUser1(); err == nil {
95+
}
96+
if userInt, err := dirUser.AsDirectoryUser1(); err == nil {
9397
return userInt
9498
}
9599
return nil
96100
}
97101

98102
// parseDirectoryGroup extracts group from Directory union type
99-
func parseDirectoryGroup(dirGroup *Directory_Group) interface{} {
103+
func parseDirectoryGroup(dirGroup *Directory_Group) any {
100104
if dirGroup == nil {
101105
return nil
102106
}
103107

104108
if groupStr, err := dirGroup.AsDirectoryGroup0(); err == nil {
105109
return groupStr
106-
} else if groupInt, err := dirGroup.AsDirectoryGroup1(); err == nil {
110+
}
111+
if groupInt, err := dirGroup.AsDirectoryGroup1(); err == nil {
107112
return groupInt
108113
}
109114
return nil
110115
}
111116

112-
// BlueprintRuleChecker interface for the Chain of Responsibility pattern
113-
type BlueprintRuleChecker interface {
114-
CheckRules(ctx echo.Context, request *CreateBlueprintRequest, existingUsers []User) error
115-
}
116-
117-
// RuleCheckingChain manages a chain of blueprint rule checkers
118-
type RuleCheckingChain struct {
119-
checkers []BlueprintRuleChecker
120-
}
121-
122-
// CheckRules executes all rule checkers in the chain and collects all violations
123-
func (rc *RuleCheckingChain) CheckRules(ctx echo.Context, request *CreateBlueprintRequest, existingUsers []User) error {
124-
var allViolations []HTTPError
125-
126-
for _, checker := range rc.checkers {
127-
if err := checker.CheckRules(ctx, request, existingUsers); err != nil {
128-
if ruleViolationErr, ok := err.(RuleViolationError); ok {
129-
// Collect all violations from this checker
130-
allViolations = append(allViolations, ruleViolationErr.HTTPErrorList.Errors...)
131-
} else {
132-
// Handle unexpected error types
133-
allViolations = append(allViolations, HTTPError{
134-
Title: "Rule Violation",
135-
Detail: err.Error(),
136-
})
137-
}
138-
}
139-
}
140-
141-
if len(allViolations) > 0 {
142-
return RuleViolationError{
143-
HTTPErrorList: HTTPErrorList{
144-
Errors: allViolations,
145-
},
146-
}
147-
}
148-
149-
return nil
150-
}
151-
152-
// NameRuleChecker checks blueprint name rules
153-
type NameRuleChecker struct{}
154-
155-
func (nrc *NameRuleChecker) CheckRules(ctx echo.Context, request *CreateBlueprintRequest, existingUsers []User) error {
117+
func checkNameRule(request *CreateBlueprintRequest) error {
156118
if !blueprintNameRegex.MatchString(request.Name) {
157-
return newRuleViolation("Blueprint name rule violation", blueprintInvalidNameDetail)
119+
return newValidationError("blueprint name rule violation", blueprintInvalidNameDetail)
158120
}
159121
return nil
160122
}
161123

162-
// UserRuleChecker checks blueprint user rules
163-
type UserRuleChecker struct{}
164-
165-
func (urc *UserRuleChecker) CheckRules(ctx echo.Context, request *CreateBlueprintRequest, existingUsers []User) error {
124+
func checkUserRule(request *CreateBlueprintRequest, existingUsers []User) error {
166125
users := request.Customizations.Users
167126
if users == nil {
168127
return nil
@@ -177,24 +136,20 @@ func (urc *UserRuleChecker) CheckRules(ctx echo.Context, request *CreateBlueprin
177136
}
178137

179138
if err != nil {
180-
return newRuleViolation("User rule violation", err.Error())
139+
return newValidationError("user rule violation", err.Error())
181140
}
182141
}
183142
return nil
184143
}
185144

186-
// FileRuleChecker checks blueprint file customization rules
187-
type FileRuleChecker struct{}
188-
189-
func (frc *FileRuleChecker) CheckRules(ctx echo.Context, request *CreateBlueprintRequest, existingUsers []User) error {
145+
func checkFileRule(request *CreateBlueprintRequest) error {
190146
files := request.Customizations.Files
191147
if files == nil {
192148
return nil
193149
}
194150

195-
var violations []HTTPError
151+
var errs []error
196152
for _, file := range *files {
197-
// Convert API types to fsnode types for rule checking
198153
mode := parseOctalMode(file.Mode)
199154
user := parseFileUser(file.User)
200155
group := parseFileGroup(file.Group)
@@ -204,38 +159,26 @@ func (frc *FileRuleChecker) CheckRules(ctx echo.Context, request *CreateBlueprin
204159
data = []byte(*file.Data)
205160
}
206161

207-
// Use fsnode.NewFile for rule checking - this handles all path, mode, user, group rules
208162
_, err := fsnode.NewFile(file.Path, mode, user, group, data)
209163
if err != nil {
210-
violations = append(violations, HTTPError{
211-
Title: "File rule violation",
212-
Detail: fmt.Sprintf("file %q: %s", file.Path, err.Error()),
213-
})
164+
errs = append(errs, newValidationError(
165+
"file rule violation",
166+
fmt.Sprintf("file %q: %s", file.Path, err.Error()),
167+
))
214168
}
215169
}
216170

217-
if len(violations) > 0 {
218-
return RuleViolationError{
219-
HTTPErrorList: HTTPErrorList{
220-
Errors: violations,
221-
},
222-
}
223-
}
224-
return nil
171+
return errors.Join(errs...)
225172
}
226173

227-
// DirectoryRuleChecker checks blueprint directory customization rules
228-
type DirectoryRuleChecker struct{}
229-
230-
func (drc *DirectoryRuleChecker) CheckRules(ctx echo.Context, request *CreateBlueprintRequest, existingUsers []User) error {
174+
func checkDirectoryRule(request *CreateBlueprintRequest) error {
231175
directories := request.Customizations.Directories
232176
if directories == nil {
233177
return nil
234178
}
235179

236-
var violations []HTTPError
180+
var errs []error
237181
for _, dir := range *directories {
238-
// Convert API types to fsnode types for rule checking
239182
mode := parseOctalMode(dir.Mode)
240183
user := parseDirectoryUser(dir.User)
241184
group := parseDirectoryGroup(dir.Group)
@@ -245,90 +188,61 @@ func (drc *DirectoryRuleChecker) CheckRules(ctx echo.Context, request *CreateBlu
245188
ensureParents = *dir.EnsureParents
246189
}
247190

248-
// Use fsnode.NewDirectory for rule checking - this handles all path, mode, user, group rules
249191
_, err := fsnode.NewDirectory(dir.Path, mode, user, group, ensureParents)
250192
if err != nil {
251-
violations = append(violations, HTTPError{
252-
Title: "Directory rule violation",
253-
Detail: fmt.Sprintf("directory %q: %s", dir.Path, err.Error()),
254-
})
193+
errs = append(errs, newValidationError(
194+
"directory rule violation",
195+
fmt.Sprintf("directory %q: %s", dir.Path, err.Error()),
196+
))
255197
}
256198
}
257199

258-
if len(violations) > 0 {
259-
return RuleViolationError{
260-
HTTPErrorList: HTTPErrorList{
261-
Errors: violations,
262-
},
263-
}
264-
}
265-
return nil
200+
return errors.Join(errs...)
266201
}
267202

268-
// FilesystemRuleChecker checks blueprint filesystem customization rules
269-
type FilesystemRuleChecker struct{}
270-
271-
func (fsrc *FilesystemRuleChecker) CheckRules(ctx echo.Context, request *CreateBlueprintRequest, existingUsers []User) error {
203+
func checkFilesystemRule(request *CreateBlueprintRequest) error {
272204
filesystem := request.Customizations.Filesystem
273205
if filesystem == nil {
274206
return nil
275207
}
276208

277-
var violations []HTTPError
209+
var errs []error
278210
for _, fs := range *filesystem {
279-
// Use the same path rule checking logic as fsnode (following library patterns)
280211
if fs.Mountpoint == "" {
281-
violations = append(violations, HTTPError{
282-
Title: "Filesystem rule violation",
283-
Detail: "mountpoint must not be empty",
284-
})
212+
errs = append(errs, newValidationError(
213+
"filesystem rule violation",
214+
"mountpoint must not be empty",
215+
))
285216
} else if fs.Mountpoint[0] != '/' {
286-
violations = append(violations, HTTPError{
287-
Title: "Filesystem rule violation",
288-
Detail: fmt.Sprintf("mountpoint %q must be absolute", fs.Mountpoint),
289-
})
217+
errs = append(errs, newValidationError(
218+
"filesystem rule violation",
219+
fmt.Sprintf("mountpoint %q must be absolute", fs.Mountpoint),
220+
))
290221
} else if fs.Mountpoint != filepath.Clean(fs.Mountpoint) {
291-
violations = append(violations, HTTPError{
292-
Title: "Filesystem rule violation",
293-
Detail: fmt.Sprintf("mountpoint %q must be canonical", fs.Mountpoint),
294-
})
222+
errs = append(errs, newValidationError(
223+
"filesystem rule violation",
224+
fmt.Sprintf("mountpoint %q must be canonical", fs.Mountpoint),
225+
))
295226
}
296227

297-
// Check minimum size is reasonable
298-
if fs.MinSize > 0 && fs.MinSize < 1024*1024 { // 1MB minimum
299-
violations = append(violations, HTTPError{
300-
Title: "Filesystem rule violation",
301-
Detail: fmt.Sprintf("mountpoint %q minimum size must be at least 1MB", fs.Mountpoint),
302-
})
228+
if fs.MinSize > 0 && fs.MinSize < 1024*1024 {
229+
errs = append(errs, newValidationError(
230+
"filesystem rule violation",
231+
fmt.Sprintf("mountpoint %q minimum size must be at least 1MB", fs.Mountpoint),
232+
))
303233
}
304234
}
305235

306-
if len(violations) > 0 {
307-
return RuleViolationError{
308-
HTTPErrorList: HTTPErrorList{
309-
Errors: violations,
310-
},
311-
}
312-
}
313-
return nil
314-
}
315-
316-
// NewBlueprintRuleChecker creates a new rule checking chain with all checkers
317-
func NewBlueprintRuleChecker() *RuleCheckingChain {
318-
return &RuleCheckingChain{
319-
checkers: []BlueprintRuleChecker{
320-
&NameRuleChecker{},
321-
&UserRuleChecker{},
322-
&FileRuleChecker{},
323-
&DirectoryRuleChecker{},
324-
&FilesystemRuleChecker{},
325-
},
326-
}
236+
return errors.Join(errs...)
327237
}
328238

329239
// CheckBlueprintRules performs common rule checking for blueprint requests
330-
// using the Chain of Responsibility pattern
331-
func CheckBlueprintRules(ctx echo.Context, blueprintRequest *CreateBlueprintRequest, existingUsers []User) error {
332-
chain := NewBlueprintRuleChecker()
333-
return chain.CheckRules(ctx, blueprintRequest, existingUsers)
240+
func CheckBlueprintRules(ctx echo.Context, request *CreateBlueprintRequest, existingUsers []User) error {
241+
return errors.Join(
242+
checkNameRule(request),
243+
checkUserRule(request, existingUsers),
244+
checkFileRule(request),
245+
checkDirectoryRule(request),
246+
checkFilesystemRule(request),
247+
)
334248
}

0 commit comments

Comments
 (0)