| // Copyright 2015 go-swagger maintainers |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package validate |
| |
| import ( |
| "fmt" |
| "reflect" |
| "strings" |
| |
| "github.com/go-openapi/errors" |
| "github.com/go-openapi/spec" |
| ) |
| |
| // Result represents a validation result set, composed of |
| // errors and warnings. |
| // |
| // It is used to keep track of all detected errors and warnings during |
| // the validation of a specification. |
| // |
| // Matchcount is used to determine |
| // which errors are relevant in the case of AnyOf, OneOf |
| // schema validation. Results from the validation branch |
| // with most matches get eventually selected. |
| // |
| // TODO: keep path of key originating the error |
| type Result struct { |
| Errors []error |
| Warnings []error |
| MatchCount int |
| |
| // the object data |
| data interface{} |
| |
| // Schemata for the root object |
| rootObjectSchemata schemata |
| // Schemata for object fields |
| fieldSchemata []fieldSchemata |
| // Schemata for slice items |
| itemSchemata []itemSchemata |
| |
| cachedFieldSchemta map[FieldKey][]*spec.Schema |
| cachedItemSchemata map[ItemKey][]*spec.Schema |
| } |
| |
| // FieldKey is a pair of an object and a field, usable as a key for a map. |
| type FieldKey struct { |
| object reflect.Value // actually a map[string]interface{}, but the latter cannot be a key |
| field string |
| } |
| |
| // ItemKey is a pair of a slice and an index, usable as a key for a map. |
| type ItemKey struct { |
| slice reflect.Value // actually a []interface{}, but the latter cannot be a key |
| index int |
| } |
| |
| // NewFieldKey returns a pair of an object and field usable as a key of a map. |
| func NewFieldKey(obj map[string]interface{}, field string) FieldKey { |
| return FieldKey{object: reflect.ValueOf(obj), field: field} |
| } |
| |
| // Object returns the underlying object of this key. |
| func (fk *FieldKey) Object() map[string]interface{} { |
| return fk.object.Interface().(map[string]interface{}) |
| } |
| |
| // Field returns the underlying field of this key. |
| func (fk *FieldKey) Field() string { |
| return fk.field |
| } |
| |
| // NewItemKey returns a pair of a slice and index usable as a key of a map. |
| func NewItemKey(slice interface{}, i int) ItemKey { |
| return ItemKey{slice: reflect.ValueOf(slice), index: i} |
| } |
| |
| // Slice returns the underlying slice of this key. |
| func (ik *ItemKey) Slice() []interface{} { |
| return ik.slice.Interface().([]interface{}) |
| } |
| |
| // Index returns the underlying index of this key. |
| func (ik *ItemKey) Index() int { |
| return ik.index |
| } |
| |
| type fieldSchemata struct { |
| obj map[string]interface{} |
| field string |
| schemata schemata |
| } |
| |
| type itemSchemata struct { |
| slice reflect.Value |
| index int |
| schemata schemata |
| } |
| |
| // Merge merges this result with the other one(s), preserving match counts etc. |
| func (r *Result) Merge(others ...*Result) *Result { |
| for _, other := range others { |
| if other == nil { |
| continue |
| } |
| r.mergeWithoutRootSchemata(other) |
| r.rootObjectSchemata.Append(other.rootObjectSchemata) |
| } |
| return r |
| } |
| |
| // Data returns the original data object used for validation. Mutating this renders |
| // the result invalid. |
| func (r *Result) Data() interface{} { |
| return r.data |
| } |
| |
| // RootObjectSchemata returns the schemata which apply to the root object. |
| func (r *Result) RootObjectSchemata() []*spec.Schema { |
| return r.rootObjectSchemata.Slice() |
| } |
| |
| // FieldSchemata returns the schemata which apply to fields in objects. |
| func (r *Result) FieldSchemata() map[FieldKey][]*spec.Schema { |
| if r.cachedFieldSchemta != nil { |
| return r.cachedFieldSchemta |
| } |
| |
| ret := make(map[FieldKey][]*spec.Schema, len(r.fieldSchemata)) |
| for _, fs := range r.fieldSchemata { |
| key := NewFieldKey(fs.obj, fs.field) |
| if fs.schemata.one != nil { |
| ret[key] = append(ret[key], fs.schemata.one) |
| } else if len(fs.schemata.multiple) > 0 { |
| ret[key] = append(ret[key], fs.schemata.multiple...) |
| } |
| } |
| r.cachedFieldSchemta = ret |
| return ret |
| } |
| |
| // ItemSchemata returns the schemata which apply to items in slices. |
| func (r *Result) ItemSchemata() map[ItemKey][]*spec.Schema { |
| if r.cachedItemSchemata != nil { |
| return r.cachedItemSchemata |
| } |
| |
| ret := make(map[ItemKey][]*spec.Schema, len(r.itemSchemata)) |
| for _, ss := range r.itemSchemata { |
| key := NewItemKey(ss.slice, ss.index) |
| if ss.schemata.one != nil { |
| ret[key] = append(ret[key], ss.schemata.one) |
| } else if len(ss.schemata.multiple) > 0 { |
| ret[key] = append(ret[key], ss.schemata.multiple...) |
| } |
| } |
| r.cachedItemSchemata = ret |
| return ret |
| } |
| |
| func (r *Result) resetCaches() { |
| r.cachedFieldSchemta = nil |
| r.cachedItemSchemata = nil |
| } |
| |
| // mergeForField merges other into r, assigning other's root schemata to the given Object and field name. |
| func (r *Result) mergeForField(obj map[string]interface{}, field string, other *Result) *Result { |
| if other == nil { |
| return r |
| } |
| r.mergeWithoutRootSchemata(other) |
| |
| if other.rootObjectSchemata.Len() > 0 { |
| if r.fieldSchemata == nil { |
| r.fieldSchemata = make([]fieldSchemata, len(obj)) |
| } |
| r.fieldSchemata = append(r.fieldSchemata, fieldSchemata{ |
| obj: obj, |
| field: field, |
| schemata: other.rootObjectSchemata, |
| }) |
| } |
| |
| return r |
| } |
| |
| // mergeForSlice merges other into r, assigning other's root schemata to the given slice and index. |
| func (r *Result) mergeForSlice(slice reflect.Value, i int, other *Result) *Result { |
| if other == nil { |
| return r |
| } |
| r.mergeWithoutRootSchemata(other) |
| |
| if other.rootObjectSchemata.Len() > 0 { |
| if r.itemSchemata == nil { |
| r.itemSchemata = make([]itemSchemata, slice.Len()) |
| } |
| r.itemSchemata = append(r.itemSchemata, itemSchemata{ |
| slice: slice, |
| index: i, |
| schemata: other.rootObjectSchemata, |
| }) |
| } |
| |
| return r |
| } |
| |
| // addRootObjectSchemata adds the given schemata for the root object of the result. |
| // The slice schemata might be reused. I.e. do not modify it after being added to a result. |
| func (r *Result) addRootObjectSchemata(s *spec.Schema) { |
| r.rootObjectSchemata.Append(schemata{one: s}) |
| } |
| |
| // addPropertySchemata adds the given schemata for the object and field. |
| // The slice schemata might be reused. I.e. do not modify it after being added to a result. |
| func (r *Result) addPropertySchemata(obj map[string]interface{}, fld string, schema *spec.Schema) { |
| if r.fieldSchemata == nil { |
| r.fieldSchemata = make([]fieldSchemata, 0, len(obj)) |
| } |
| r.fieldSchemata = append(r.fieldSchemata, fieldSchemata{obj: obj, field: fld, schemata: schemata{one: schema}}) |
| } |
| |
| // addSliceSchemata adds the given schemata for the slice and index. |
| // The slice schemata might be reused. I.e. do not modify it after being added to a result. |
| func (r *Result) addSliceSchemata(slice reflect.Value, i int, schema *spec.Schema) { |
| if r.itemSchemata == nil { |
| r.itemSchemata = make([]itemSchemata, 0, slice.Len()) |
| } |
| r.itemSchemata = append(r.itemSchemata, itemSchemata{slice: slice, index: i, schemata: schemata{one: schema}}) |
| } |
| |
| // mergeWithoutRootSchemata merges other into r, ignoring the rootObject schemata. |
| func (r *Result) mergeWithoutRootSchemata(other *Result) { |
| r.resetCaches() |
| r.AddErrors(other.Errors...) |
| r.AddWarnings(other.Warnings...) |
| r.MatchCount += other.MatchCount |
| |
| if other.fieldSchemata != nil { |
| if r.fieldSchemata == nil { |
| r.fieldSchemata = other.fieldSchemata |
| } else { |
| for _, x := range other.fieldSchemata { |
| r.fieldSchemata = append(r.fieldSchemata, x) |
| } |
| } |
| } |
| |
| if other.itemSchemata != nil { |
| if r.itemSchemata == nil { |
| r.itemSchemata = other.itemSchemata |
| } else { |
| for _, x := range other.itemSchemata { |
| r.itemSchemata = append(r.itemSchemata, x) |
| } |
| } |
| } |
| } |
| |
| // MergeAsErrors merges this result with the other one(s), preserving match counts etc. |
| // |
| // Warnings from input are merged as Errors in the returned merged Result. |
| func (r *Result) MergeAsErrors(others ...*Result) *Result { |
| for _, other := range others { |
| if other != nil { |
| r.resetCaches() |
| r.AddErrors(other.Errors...) |
| r.AddErrors(other.Warnings...) |
| r.MatchCount += other.MatchCount |
| } |
| } |
| return r |
| } |
| |
| // MergeAsWarnings merges this result with the other one(s), preserving match counts etc. |
| // |
| // Errors from input are merged as Warnings in the returned merged Result. |
| func (r *Result) MergeAsWarnings(others ...*Result) *Result { |
| for _, other := range others { |
| if other != nil { |
| r.resetCaches() |
| r.AddWarnings(other.Errors...) |
| r.AddWarnings(other.Warnings...) |
| r.MatchCount += other.MatchCount |
| } |
| } |
| return r |
| } |
| |
| // AddErrors adds errors to this validation result (if not already reported). |
| // |
| // Since the same check may be passed several times while exploring the |
| // spec structure (via $ref, ...) reported messages are kept |
| // unique. |
| func (r *Result) AddErrors(errors ...error) { |
| for _, e := range errors { |
| found := false |
| if e != nil { |
| for _, isReported := range r.Errors { |
| if e.Error() == isReported.Error() { |
| found = true |
| break |
| } |
| } |
| if !found { |
| r.Errors = append(r.Errors, e) |
| } |
| } |
| } |
| } |
| |
| // AddWarnings adds warnings to this validation result (if not already reported). |
| func (r *Result) AddWarnings(warnings ...error) { |
| for _, e := range warnings { |
| found := false |
| if e != nil { |
| for _, isReported := range r.Warnings { |
| if e.Error() == isReported.Error() { |
| found = true |
| break |
| } |
| } |
| if !found { |
| r.Warnings = append(r.Warnings, e) |
| } |
| } |
| } |
| } |
| |
| func (r *Result) keepRelevantErrors() *Result { |
| // TODO: this one is going to disapear... |
| // keepRelevantErrors strips a result from standard errors and keeps |
| // the ones which are supposedly more accurate. |
| // |
| // The original result remains unaffected (creates a new instance of Result). |
| // This method is used to work around the "matchCount" filter which would otherwise |
| // strip our result from some accurate error reporting from lower level validators. |
| // |
| // NOTE: this implementation with a placeholder (IMPORTANT!) is neither clean nor |
| // very efficient. On the other hand, relying on go-openapi/errors to manipulate |
| // codes would require to change a lot here. So, for the moment, let's go with |
| // placeholders. |
| strippedErrors := []error{} |
| for _, e := range r.Errors { |
| if strings.HasPrefix(e.Error(), "IMPORTANT!") { |
| strippedErrors = append(strippedErrors, fmt.Errorf(strings.TrimPrefix(e.Error(), "IMPORTANT!"))) |
| } |
| } |
| strippedWarnings := []error{} |
| for _, e := range r.Warnings { |
| if strings.HasPrefix(e.Error(), "IMPORTANT!") { |
| strippedWarnings = append(strippedWarnings, fmt.Errorf(strings.TrimPrefix(e.Error(), "IMPORTANT!"))) |
| } |
| } |
| strippedResult := new(Result) |
| strippedResult.Errors = strippedErrors |
| strippedResult.Warnings = strippedWarnings |
| return strippedResult |
| } |
| |
| // IsValid returns true when this result is valid. |
| // |
| // Returns true on a nil *Result. |
| func (r *Result) IsValid() bool { |
| if r == nil { |
| return true |
| } |
| return len(r.Errors) == 0 |
| } |
| |
| // HasErrors returns true when this result is invalid. |
| // |
| // Returns false on a nil *Result. |
| func (r *Result) HasErrors() bool { |
| if r == nil { |
| return false |
| } |
| return !r.IsValid() |
| } |
| |
| // HasWarnings returns true when this result contains warnings. |
| // |
| // Returns false on a nil *Result. |
| func (r *Result) HasWarnings() bool { |
| if r == nil { |
| return false |
| } |
| return len(r.Warnings) > 0 |
| } |
| |
| // HasErrorsOrWarnings returns true when this result contains |
| // either errors or warnings. |
| // |
| // Returns false on a nil *Result. |
| func (r *Result) HasErrorsOrWarnings() bool { |
| if r == nil { |
| return false |
| } |
| return len(r.Errors) > 0 || len(r.Warnings) > 0 |
| } |
| |
| // Inc increments the match count |
| func (r *Result) Inc() { |
| r.MatchCount++ |
| } |
| |
| // AsError renders this result as an error interface |
| // |
| // TODO: reporting / pretty print with path ordered and indented |
| func (r *Result) AsError() error { |
| if r.IsValid() { |
| return nil |
| } |
| return errors.CompositeValidationError(r.Errors...) |
| } |
| |
| // schemata is an arbitrary number of schemata. It does a distinction between zero, |
| // one and many schemata to avoid slice allocations. |
| type schemata struct { |
| // one is set if there is exactly one schema. In that case multiple must be nil. |
| one *spec.Schema |
| // multiple is an arbitrary number of schemas. If it is set, one must be nil. |
| multiple []*spec.Schema |
| } |
| |
| func (s *schemata) Len() int { |
| if s.one != nil { |
| return 1 |
| } |
| return len(s.multiple) |
| } |
| |
| func (s *schemata) Slice() []*spec.Schema { |
| if s == nil { |
| return nil |
| } |
| if s.one != nil { |
| return []*spec.Schema{s.one} |
| } |
| return s.multiple |
| } |
| |
| // appendSchemata appends the schemata in other to s. It mutated s in-place. |
| func (s *schemata) Append(other schemata) { |
| if other.one == nil && len(other.multiple) == 0 { |
| return |
| } |
| if s.one == nil && len(s.multiple) == 0 { |
| *s = other |
| return |
| } |
| |
| if s.one != nil { |
| if other.one != nil { |
| s.multiple = []*spec.Schema{s.one, other.one} |
| } else { |
| t := make([]*spec.Schema, 0, 1+len(other.multiple)) |
| s.multiple = append(append(t, s.one), other.multiple...) |
| } |
| s.one = nil |
| } else { |
| if other.one != nil { |
| s.multiple = append(s.multiple, other.one) |
| } else { |
| if cap(s.multiple) >= len(s.multiple)+len(other.multiple) { |
| s.multiple = append(s.multiple, other.multiple...) |
| } else { |
| t := make([]*spec.Schema, 0, len(s.multiple)+len(other.multiple)) |
| s.multiple = append(append(t, s.multiple...), other.multiple...) |
| } |
| } |
| } |
| } |