go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config/validation/validation.go (about) 1 // Copyright 2017 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package validation provides a helper for performing config validations. 16 package validation 17 18 import ( 19 "context" 20 "fmt" 21 "strings" 22 23 "go.chromium.org/luci/common/errors" 24 "go.chromium.org/luci/common/logging" 25 26 configpb "go.chromium.org/luci/common/proto/config" 27 ) 28 29 // Error is an error with details of validation issues. 30 // 31 // Returned by Context.Finalize(). 32 type Error struct { 33 // Errors is a list of individual validation errors. 34 // 35 // Each one is annotated with "file" string, logical path pointing to 36 // the element that contains the error, and its severity. It is provided as a 37 // slice of strings in "element" annotation. 38 Errors errors.MultiError 39 } 40 41 // Error makes *Error implement 'error' interface. 42 func (e *Error) Error() string { 43 return e.Errors.Error() 44 } 45 46 // WithSeverity returns a multi-error with errors of a given severity only. 47 func (e *Error) WithSeverity(s Severity) error { 48 var filtered errors.MultiError 49 for _, valErr := range e.Errors { 50 if severity, ok := SeverityTag.In(valErr); ok && severity == s { 51 filtered = append(filtered, valErr) 52 } 53 } 54 if len(filtered) != 0 { 55 return filtered 56 } 57 return nil 58 } 59 60 // ToValidationResultMsgs converts `Error` to a slice of 61 // `configpb.ValidationResult.Message`s. 62 func (e *Error) ToValidationResultMsgs(ctx context.Context) []*configpb.ValidationResult_Message { 63 if e == nil || len(e.Errors) == 0 { 64 return nil 65 } 66 ret := make([]*configpb.ValidationResult_Message, len(e.Errors)) 67 for i, err := range e.Errors { 68 // validation.Context supports just 2 severities now, 69 // but defensively default to ERROR level in unexpected cases. 70 msgSeverity := configpb.ValidationResult_ERROR 71 switch severity, ok := SeverityTag.In(err); { 72 case !ok: 73 logging.Errorf(ctx, "unset validation.Severity in %s", err) 74 case severity == Warning: 75 msgSeverity = configpb.ValidationResult_WARNING 76 case severity != Blocking: 77 logging.Errorf(ctx, "unrecognized validation.Severity %d in %s", severity, err) 78 } 79 file, ok := fileTag.In(err) 80 if !ok || file == "" { 81 file = "unspecified file" 82 } 83 ret[i] = &configpb.ValidationResult_Message{ 84 Path: file, 85 Severity: msgSeverity, 86 Text: err.Error(), 87 } 88 } 89 return ret 90 } 91 92 // Context is an accumulator for validation errors. 93 // 94 // It is passed to a function that does config validation. Such function may 95 // validate a bunch of files (using SetFile to indicate which one is processed 96 // now). Each file may have some internal nested structure. The logical path 97 // inside this structure is captured through Enter and Exit calls. 98 type Context struct { 99 Context context.Context 100 101 errors errors.MultiError // all accumulated errors, including those with Warning severity. 102 file string // the currently validated file 103 element []string // logical path of a sub-element we validate, see Enter 104 } 105 106 type fileTagType struct{ Key errors.TagKey } 107 108 func (f fileTagType) With(name string) errors.TagValue { 109 return errors.TagValue{Key: f.Key, Value: name} 110 } 111 func (f fileTagType) In(err error) (v string, ok bool) { 112 d, ok := errors.TagValueIn(f.Key, err) 113 if ok { 114 v = d.(string) 115 } 116 return 117 } 118 119 type elementTagType struct{ Key errors.TagKey } 120 121 func (e elementTagType) With(elements []string) errors.TagValue { 122 return errors.TagValue{Key: e.Key, Value: append([]string(nil), elements...)} 123 } 124 func (e elementTagType) In(err error) (v []string, ok bool) { 125 d, ok := errors.TagValueIn(e.Key, err) 126 if ok { 127 v = d.([]string) 128 } 129 return 130 } 131 132 // Severity of the validation message. 133 // 134 // Only Blocking and Warning severities are supported. 135 type Severity int 136 137 const ( 138 // Blocking severity blocks config from being accepted. 139 // 140 // Corresponds to ValidationResponseMessage_Severity:ERROR. 141 Blocking Severity = 0 142 // Warning severity doesn't block config from being accepted. 143 // 144 // Corresponds to ValidationResponseMessage_Severity:WARNING. 145 Warning Severity = 1 146 ) 147 148 type severityTagType struct{ Key errors.TagKey } 149 150 func (s severityTagType) With(severity Severity) errors.TagValue { 151 return errors.TagValue{Key: s.Key, Value: severity} 152 } 153 func (s severityTagType) In(err error) (v Severity, ok bool) { 154 d, ok := errors.TagValueIn(s.Key, err) 155 if ok { 156 v = d.(Severity) 157 } 158 return 159 } 160 161 var fileTag = fileTagType{errors.NewTagKey("holds the file name for tests")} 162 var elementTag = elementTagType{errors.NewTagKey("holds the elements for tests")} 163 164 // SeverityTag holds the severity of the given validation error. 165 var SeverityTag = severityTagType{errors.NewTagKey("holds the severity")} 166 167 // Errorf records the given format string and args as a blocking validation error. 168 func (v *Context) Errorf(format string, args ...any) { 169 v.record(Blocking, errors.Reason(format, args...).Err()) 170 } 171 172 // Error records the given error as a blocking validation error. 173 func (v *Context) Error(err error) { 174 v.record(Blocking, err) 175 } 176 177 // Warningf records the given format string and args as a validation warning. 178 func (v *Context) Warningf(format string, args ...any) { 179 v.record(Warning, errors.Reason(format, args...).Err()) 180 } 181 182 // Warning records the given error as a validation warning. 183 func (v *Context) Warning(err error) { 184 v.record(Warning, err) 185 } 186 187 func (v *Context) record(severity Severity, err error) { 188 ctx := "" 189 if v.file != "" { 190 ctx = fmt.Sprintf("in %q", v.file) 191 } else { 192 ctx = "in <unspecified file>" 193 } 194 if len(v.element) != 0 { 195 ctx += " (" + strings.Join(v.element, " / ") + ")" 196 } 197 // Make the file and the logical path also usable through error inspection. 198 v.errors = append(v.errors, errors.Annotate(err, "%s", ctx).Tag( 199 fileTag.With(v.file), elementTag.With(v.element), SeverityTag.With(severity)).Err()) 200 } 201 202 // SetFile records that what follows is errors for this particular file. 203 // 204 // Changing the file resets the current element (see Enter/Exit). 205 func (v *Context) SetFile(path string) { 206 if v.file != path { 207 v.file = path 208 v.element = nil 209 } 210 } 211 212 // Enter descends into a sub-element when validating a nested structure. 213 // 214 // Useful for defining context. A current path of elements shows up in 215 // validation messages. 216 // 217 // The reverse is Exit. 218 func (v *Context) Enter(title string, args ...any) { 219 e := fmt.Sprintf(title, args...) 220 v.element = append(v.element, e) 221 } 222 223 // Exit pops the current element we are visiting from the stack. 224 // 225 // This is the reverse of Enter. Each Enter must have corresponding Exit. Use 226 // functions and defers to ensure this, if it's otherwise hard to track. 227 func (v *Context) Exit() { 228 if len(v.element) != 0 { 229 v.element = v.element[:len(v.element)-1] 230 } 231 } 232 233 // Finalize returns *Error if some validation errors were recorded. 234 // 235 // Returns nil otherwise. 236 func (v *Context) Finalize() error { 237 if len(v.errors) == 0 { 238 return nil 239 } 240 return &Error{ 241 Errors: append(errors.MultiError{}, v.errors...), 242 } 243 }