11package v1
22
33import (
4+ "errors"
45 "fmt"
56 "os"
67 "path/filepath"
@@ -13,30 +14,30 @@ import (
1314
1415var (
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