diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 7dc7009..9ce426e 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -37,7 +37,7 @@ updates: patterns: - "github.com/stretchr/testify" - golang.org-dependencies: + golang-org-dependencies: patterns: - "golang.org/*" diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 9420058..1f18207 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -35,7 +35,7 @@ jobs: GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Auto-merge dependabot PRs for golang.org updates - if: contains(steps.metadata.outputs.dependency-group, 'golang.org-dependencies') + if: contains(steps.metadata.outputs.dependency-group, 'golang-org-dependencies') run: gh pr merge --auto --rebase "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} diff --git a/.golangci.yml b/.golangci.yml index 5006306..66eac0d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,15 +14,19 @@ linters: - gocognit - godot - godox + - gomoddirectives - gosmopolitan - inamedparam + - intrange # disabled while < go1.22 - ireturn - lll - musttag - nestif - nlreturn - nonamedreturns + - noinlineerr - paralleltest + - recvcheck - testpackage - thelper - tparallel @@ -31,6 +35,7 @@ linters: - whitespace - wrapcheck - wsl + - wsl_v5 settings: dupl: threshold: 200 @@ -60,3 +65,12 @@ formatters: - third_party$ - builtin$ - examples$ +issues: + # Maximum issues count per one linter. + # Set to 0 to disable. + # Default: 50 + max-issues-per-linter: 0 + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 0 diff --git a/default_validator.go b/default_validator.go index 62f97ed..e82bb30 100644 --- a/default_validator.go +++ b/default_validator.go @@ -29,6 +29,18 @@ type defaultValidator struct { schemaOptions *SchemaValidatorOptions } +// Validate validates the default values declared in the swagger spec +func (d *defaultValidator) Validate() *Result { + errs := pools.poolOfResults.BorrowResult() // will redeem when merged + + if d == nil || d.SpecValidator == nil { + return errs + } + d.resetVisited() + errs.Merge(d.validateDefaultValueValidAgainstSchema()) // error - + return errs +} + // resetVisited resets the internal state of visited schemas func (d *defaultValidator) resetVisited() { if d.visitedSchemas == nil { @@ -82,18 +94,6 @@ func (d *defaultValidator) isVisited(path string) bool { return isVisited(path, d.visitedSchemas) } -// Validate validates the default values declared in the swagger spec -func (d *defaultValidator) Validate() *Result { - errs := pools.poolOfResults.BorrowResult() // will redeem when merged - - if d == nil || d.SpecValidator == nil { - return errs - } - d.resetVisited() - errs.Merge(d.validateDefaultValueValidAgainstSchema()) // error - - return errs -} - func (d *defaultValidator) validateDefaultValueValidAgainstSchema() *Result { // every default value that is specified must validate against the schema for that property // headers, items, parameters, schema @@ -285,7 +285,7 @@ func (d *defaultValidator) validateDefaultValueSchemaAgainstSchema(path, in stri // TODO: Temporary duplicated code. Need to refactor with examples -func (d *defaultValidator) validateDefaultValueItemsAgainstSchema(path, in string, root interface{}, items *spec.Items) *Result { +func (d *defaultValidator) validateDefaultValueItemsAgainstSchema(path, in string, root any, items *spec.Items) *Result { res := pools.poolOfResults.BorrowResult() s := d.SpecValidator if items != nil { diff --git a/example_validator.go b/example_validator.go index d089569..0663b21 100644 --- a/example_validator.go +++ b/example_validator.go @@ -27,6 +27,25 @@ type exampleValidator struct { schemaOptions *SchemaValidatorOptions } +// Validate validates the example values declared in the swagger spec +// Example values MUST conform to their schema. +// +// With Swagger 2.0, examples are supported in: +// - schemas +// - individual property +// - responses +func (ex *exampleValidator) Validate() *Result { + errs := pools.poolOfResults.BorrowResult() + + if ex == nil || ex.SpecValidator == nil { + return errs + } + ex.resetVisited() + errs.Merge(ex.validateExampleValueValidAgainstSchema()) // error - + + return errs +} + // resetVisited resets the internal state of visited schemas func (ex *exampleValidator) resetVisited() { if ex.visitedSchemas == nil { @@ -51,25 +70,6 @@ func (ex *exampleValidator) isVisited(path string) bool { return isVisited(path, ex.visitedSchemas) } -// Validate validates the example values declared in the swagger spec -// Example values MUST conform to their schema. -// -// With Swagger 2.0, examples are supported in: -// - schemas -// - individual property -// - responses -func (ex *exampleValidator) Validate() *Result { - errs := pools.poolOfResults.BorrowResult() - - if ex == nil || ex.SpecValidator == nil { - return errs - } - ex.resetVisited() - errs.Merge(ex.validateExampleValueValidAgainstSchema()) // error - - - return errs -} - func (ex *exampleValidator) validateExampleValueValidAgainstSchema() *Result { // every example value that is specified must validate against the schema for that property // in: schemas, properties, object, items @@ -278,7 +278,7 @@ func (ex *exampleValidator) validateExampleValueSchemaAgainstSchema(path, in str // TODO: Temporary duplicated code. Need to refactor with examples // -func (ex *exampleValidator) validateExampleValueItemsAgainstSchema(path, in string, root interface{}, items *spec.Items) *Result { +func (ex *exampleValidator) validateExampleValueItemsAgainstSchema(path, in string, root any, items *spec.Items) *Result { res := pools.poolOfResults.BorrowResult() s := ex.SpecValidator if items != nil { diff --git a/helpers.go b/helpers.go index 2b23056..16840da 100644 --- a/helpers.go +++ b/helpers.go @@ -273,7 +273,7 @@ func (h *paramHelper) checkExpandedParam(pr *spec.Parameter, path, in, operation simpleZero := spec.SimpleSchema{} // Try to explain why... best guess switch { - case pr.In == swaggerBody && (pr.SimpleSchema != simpleZero && pr.SimpleSchema.Type != objectType): + case pr.In == swaggerBody && (pr.SimpleSchema != simpleZero && pr.Type != objectType): if isRef { // Most likely, a $ref with a sibling is an unwanted situation: in itself this is a warning... // but we detect it because of the following error: diff --git a/object_validator.go b/object_validator.go index dff73fa..76301d0 100644 --- a/object_validator.go +++ b/object_validator.go @@ -33,7 +33,7 @@ type objectValidator struct { Properties map[string]spec.Schema AdditionalProperties *spec.SchemaOrBool PatternProperties map[string]spec.Schema - Root interface{} + Root any KnownFormats strfmt.Registry Options *SchemaValidatorOptions splitPath []string @@ -42,7 +42,7 @@ type objectValidator struct { func newObjectValidator(path, in string, maxProperties, minProperties *int64, required []string, properties spec.SchemaProperties, additionalProperties *spec.SchemaOrBool, patternProperties spec.SchemaProperties, - root interface{}, formats strfmt.Registry, opts *SchemaValidatorOptions) *objectValidator { + root any, formats strfmt.Registry, opts *SchemaValidatorOptions) *objectValidator { if opts == nil { opts = new(SchemaValidatorOptions) } @@ -70,12 +70,76 @@ func newObjectValidator(path, in string, return v } +func (o *objectValidator) Validate(data any) *Result { + if o.Options.recycleValidators { + defer func() { + o.redeem() + }() + } + + var val map[string]any + if data != nil { + var ok bool + val, ok = data.(map[string]any) + if !ok { + return errorHelp.sErr(invalidObjectMsg(o.Path, o.In), o.Options.recycleResult) + } + } + numKeys := int64(len(val)) + + if o.MinProperties != nil && numKeys < *o.MinProperties { + return errorHelp.sErr(errors.TooFewProperties(o.Path, o.In, *o.MinProperties), o.Options.recycleResult) + } + if o.MaxProperties != nil && numKeys > *o.MaxProperties { + return errorHelp.sErr(errors.TooManyProperties(o.Path, o.In, *o.MaxProperties), o.Options.recycleResult) + } + + var res *Result + if o.Options.recycleResult { + res = pools.poolOfResults.BorrowResult() + } else { + res = new(Result) + } + + o.precheck(res, val) + + // check validity of field names + if o.AdditionalProperties != nil && !o.AdditionalProperties.Allows { + // Case: additionalProperties: false + o.validateNoAdditionalProperties(val, res) + } else { + // Cases: empty additionalProperties (implying: true), or additionalProperties: true, or additionalProperties: { <> } + o.validateAdditionalProperties(val, res) + } + + o.validatePropertiesSchema(val, res) + + // Check patternProperties + // TODO: it looks like we have done that twice in many cases + for key, value := range val { + _, regularProperty := o.Properties[key] + matched, _, patterns := o.validatePatternProperty(key, value, res) // applies to regular properties as well + if regularProperty || !matched { + continue + } + + for _, pName := range patterns { + if v, ok := o.PatternProperties[pName]; ok { + r := newSchemaValidator(&v, o.Root, o.Path+"."+key, o.KnownFormats, o.Options).Validate(value) + res.mergeForField(data.(map[string]any), key, r) + } + } + } + + return res +} + func (o *objectValidator) SetPath(path string) { o.Path = path o.splitPath = strings.Split(path, ".") } -func (o *objectValidator) Applies(source interface{}, kind reflect.Kind) bool { +func (o *objectValidator) Applies(source any, kind reflect.Kind) bool { // TODO: this should also work for structs // there is a problem in the type validator where it will be unhappy about null values // so that requires more testing @@ -98,7 +162,7 @@ func (o *objectValidator) isExample() bool { return len(p) > 1 && (p[len(p)-1] == swaggerExample || p[len(p)-1] == swaggerExamples) && p[len(p)-2] != swaggerExample } -func (o *objectValidator) checkArrayMustHaveItems(res *Result, val map[string]interface{}) { +func (o *objectValidator) checkArrayMustHaveItems(res *Result, val map[string]any) { // for swagger 2.0 schemas, there is an additional constraint to have array items defined explicitly. // with pure jsonschema draft 4, one may have arrays with undefined items (i.e. any type). if val == nil { @@ -123,7 +187,7 @@ func (o *objectValidator) checkArrayMustHaveItems(res *Result, val map[string]in res.AddErrors(errors.Required(jsonItems, o.Path, item)) } -func (o *objectValidator) checkItemsMustBeTypeArray(res *Result, val map[string]interface{}) { +func (o *objectValidator) checkItemsMustBeTypeArray(res *Result, val map[string]any) { if val == nil { return } @@ -148,7 +212,7 @@ func (o *objectValidator) checkItemsMustBeTypeArray(res *Result, val map[string] } } -func (o *objectValidator) precheck(res *Result, val map[string]interface{}) { +func (o *objectValidator) precheck(res *Result, val map[string]any) { if o.Options.EnableArrayMustHaveItemsCheck { o.checkArrayMustHaveItems(res, val) } @@ -157,71 +221,7 @@ func (o *objectValidator) precheck(res *Result, val map[string]interface{}) { } } -func (o *objectValidator) Validate(data interface{}) *Result { - if o.Options.recycleValidators { - defer func() { - o.redeem() - }() - } - - var val map[string]interface{} - if data != nil { - var ok bool - val, ok = data.(map[string]interface{}) - if !ok { - return errorHelp.sErr(invalidObjectMsg(o.Path, o.In), o.Options.recycleResult) - } - } - numKeys := int64(len(val)) - - if o.MinProperties != nil && numKeys < *o.MinProperties { - return errorHelp.sErr(errors.TooFewProperties(o.Path, o.In, *o.MinProperties), o.Options.recycleResult) - } - if o.MaxProperties != nil && numKeys > *o.MaxProperties { - return errorHelp.sErr(errors.TooManyProperties(o.Path, o.In, *o.MaxProperties), o.Options.recycleResult) - } - - var res *Result - if o.Options.recycleResult { - res = pools.poolOfResults.BorrowResult() - } else { - res = new(Result) - } - - o.precheck(res, val) - - // check validity of field names - if o.AdditionalProperties != nil && !o.AdditionalProperties.Allows { - // Case: additionalProperties: false - o.validateNoAdditionalProperties(val, res) - } else { - // Cases: empty additionalProperties (implying: true), or additionalProperties: true, or additionalProperties: { <> } - o.validateAdditionalProperties(val, res) - } - - o.validatePropertiesSchema(val, res) - - // Check patternProperties - // TODO: it looks like we have done that twice in many cases - for key, value := range val { - _, regularProperty := o.Properties[key] - matched, _, patterns := o.validatePatternProperty(key, value, res) // applies to regular properties as well - if regularProperty || !matched { - continue - } - - for _, pName := range patterns { - if v, ok := o.PatternProperties[pName]; ok { - r := newSchemaValidator(&v, o.Root, o.Path+"."+key, o.KnownFormats, o.Options).Validate(value) - res.mergeForField(data.(map[string]interface{}), key, r) - } - } - } - - return res -} - -func (o *objectValidator) validateNoAdditionalProperties(val map[string]interface{}, res *Result) { +func (o *objectValidator) validateNoAdditionalProperties(val map[string]any, res *Result) { for k := range val { if k == "$schema" || k == "id" { // special properties "$schema" and "id" are ignored @@ -266,7 +266,7 @@ func (o *objectValidator) validateNoAdditionalProperties(val map[string]interfac } // $ref is forbidden in header - headers, mapOk := val[k].(map[string]interface{}) + headers, mapOk := val[k].(map[string]any) if !mapOk { continue } @@ -276,7 +276,7 @@ func (o *objectValidator) validateNoAdditionalProperties(val map[string]interfac continue } - headerSchema, mapOfMapOk := headerBody.(map[string]interface{}) + headerSchema, mapOfMapOk := headerBody.(map[string]any) if !mapOfMapOk { continue } @@ -303,7 +303,7 @@ func (o *objectValidator) validateNoAdditionalProperties(val map[string]interfac } } -func (o *objectValidator) validateAdditionalProperties(val map[string]interface{}, res *Result) { +func (o *objectValidator) validateAdditionalProperties(val map[string]any, res *Result) { for key, value := range val { _, regularProperty := o.Properties[key] if regularProperty { @@ -331,7 +331,7 @@ func (o *objectValidator) validateAdditionalProperties(val map[string]interface{ // Valid cases: additionalProperties: true or undefined } -func (o *objectValidator) validatePropertiesSchema(val map[string]interface{}, res *Result) { +func (o *objectValidator) validatePropertiesSchema(val map[string]any, res *Result) { createdFromDefaults := map[string]struct{}{} // Property types: @@ -389,7 +389,7 @@ func (o *objectValidator) validatePropertiesSchema(val map[string]interface{}, r } // TODO: succeededOnce is not used anywhere -func (o *objectValidator) validatePatternProperty(key string, value interface{}, result *Result) (bool, bool, []string) { +func (o *objectValidator) validatePatternProperty(key string, value any, result *Result) (bool, bool, []string) { if len(o.PatternProperties) == 0 { return false, false, nil } diff --git a/result.go b/result.go index ab6d6cd..cf7c421 100644 --- a/result.go +++ b/result.go @@ -43,7 +43,7 @@ type Result struct { MatchCount int // the object data - data interface{} + data any // Schemata for the root object rootObjectSchemata schemata @@ -60,24 +60,24 @@ type Result struct { // 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 + object reflect.Value // actually a map[string]any, 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 + slice reflect.Value // actually a []any, 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 { +func NewFieldKey(obj map[string]any, 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{}) +func (fk *FieldKey) Object() map[string]any { + return fk.object.Interface().(map[string]any) } // Field returns the underlying field of this key. @@ -86,13 +86,13 @@ func (fk *FieldKey) Field() string { } // NewItemKey returns a pair of a slice and index usable as a key of a map. -func NewItemKey(slice interface{}, i int) ItemKey { +func NewItemKey(slice any, 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{}) +func (ik *ItemKey) Slice() []any { + return ik.slice.Interface().([]any) } // Index returns the underlying index of this key. @@ -101,7 +101,7 @@ func (ik *ItemKey) Index() int { } type fieldSchemata struct { - obj map[string]interface{} + obj map[string]any field string schemata schemata } @@ -129,7 +129,7 @@ func (r *Result) Merge(others ...*Result) *Result { // Data returns the original data object used for validation. Mutating this renders // the result invalid. -func (r *Result) Data() interface{} { +func (r *Result) Data() any { return r.data } @@ -177,6 +177,138 @@ func (r *Result) ItemSchemata() map[ItemKey][]*spec.Schema { return ret } +// 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 + if other.wantsRedeemOnMerge { + pools.poolOfResults.RedeemResult(other) + } + } + } + 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 + if other.wantsRedeemOnMerge { + pools.poolOfResults.RedeemResult(other) + } + } + } + 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) + } + } + } +} + +// 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...) +} + func (r *Result) resetCaches() { r.cachedFieldSchemata = nil r.cachedItemSchemata = nil @@ -185,7 +317,7 @@ func (r *Result) resetCaches() { // mergeForField merges other into r, assigning other's root schemata to the given Object and field name. // //nolint:unparam -func (r *Result) mergeForField(obj map[string]interface{}, field string, other *Result) *Result { +func (r *Result) mergeForField(obj map[string]any, field string, other *Result) *Result { if other == nil { return r } @@ -248,7 +380,7 @@ func (r *Result) addRootObjectSchemata(s *spec.Schema) { // addPropertySchemata adds the given schemata for the object and field. // // Since the slice schemata might be reused, it is shallow-cloned before saving it into the result. -func (r *Result) addPropertySchemata(obj map[string]interface{}, fld string, schema *spec.Schema) { +func (r *Result) addPropertySchemata(obj map[string]any, fld string, schema *spec.Schema) { if r.fieldSchemata == nil { r.fieldSchemata = make([]fieldSchemata, 0, len(obj)) } @@ -295,82 +427,6 @@ func (r *Result) mergeWithoutRootSchemata(other *Result) { } } -// 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 - if other.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(other) - } - } - } - 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 - if other.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(other) - } - } - } - 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 isImportant(err error) bool { return strings.HasPrefix(err.Error(), "IMPORTANT!") } @@ -415,62 +471,6 @@ func (r *Result) keepRelevantErrors() *Result { 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...) -} - func (r *Result) cleared() *Result { // clear the Result to be reusable. Keep allocated capacity. r.Errors = r.Errors[:0] diff --git a/spec.go b/spec.go index 4bf8bf0..9b57b4d 100644 --- a/spec.go +++ b/spec.go @@ -85,7 +85,7 @@ func NewSpecValidator(schema *spec.Schema, formats strfmt.Registry) *SpecValidat } // Validate validates the swagger spec -func (s *SpecValidator) Validate(data interface{}) (*Result, *Result) { +func (s *SpecValidator) Validate(data any) (*Result, *Result) { s.schemaOptions.skipSchemataResult = s.Options.SkipSchemataResult var sd *loads.Document errs, warnings := new(Result), new(Result) @@ -101,7 +101,7 @@ func (s *SpecValidator) Validate(data interface{}) (*Result, *Result) { s.analyzer = analysis.New(sd.Spec()) // Raw spec unmarshalling errors - var obj interface{} + var obj any if err := json.Unmarshal(sd.Raw(), &obj); err != nil { // NOTE: under normal conditions, the *load.Document has been already unmarshalled // So this one is just a paranoid check on the behavior of the spec package @@ -161,6 +161,11 @@ func (s *SpecValidator) Validate(data interface{}) (*Result, *Result) { return errs, warnings } +// SetContinueOnErrors sets the ContinueOnErrors option for this validator. +func (s *SpecValidator) SetContinueOnErrors(c bool) { + s.Options.ContinueOnErrors = c +} + func (s *SpecValidator) validateNonEmptyPathParamNames() *Result { res := pools.poolOfResults.BorrowResult() if s.spec.Spec().Paths == nil { @@ -598,7 +603,7 @@ func (s *SpecValidator) validateRequiredProperties(path, in string, v *spec.Sche } } - if !(propertyMatch || patternMatch) { + if !propertyMatch && !patternMatch { if v.AdditionalProperties != nil { if v.AdditionalProperties.Allows && v.AdditionalProperties.Schema == nil { additionalPropertiesMatch = true @@ -618,7 +623,7 @@ func (s *SpecValidator) validateRequiredProperties(path, in string, v *spec.Sche } } - if !(propertyMatch || patternMatch || additionalPropertiesMatch) { + if !propertyMatch && !patternMatch && !additionalPropertiesMatch { res.AddErrors(requiredButNotDefinedMsg(path, in)) } @@ -717,19 +722,19 @@ func (s *SpecValidator) validateParameters() *Result { hasForm = true } - if !(pr.Type == numberType || pr.Type == integerType) && + if pr.Type != numberType && pr.Type != integerType && (pr.Maximum != nil || pr.Minimum != nil || pr.MultipleOf != nil) { // A non-numeric parameter has validation keywords for numeric instances (number and integer) res.AddWarnings(parameterValidationTypeMismatchMsg(pr.Name, path, pr.Type)) } - if !(pr.Type == stringType) && + if pr.Type != stringType && // A non-string parameter has validation keywords for strings (pr.MaxLength != nil || pr.MinLength != nil || pr.Pattern != "") { res.AddWarnings(parameterValidationTypeMismatchMsg(pr.Name, path, pr.Type)) } - if !(pr.Type == arrayType) && + if pr.Type != arrayType && // A non-array parameter has validation keywords for arrays (pr.MaxItems != nil || pr.MinItems != nil || pr.UniqueItems) { res.AddWarnings(parameterValidationTypeMismatchMsg(pr.Name, path, pr.Type)) @@ -823,11 +828,6 @@ func (s *SpecValidator) checkUniqueParams(path, method string, op *spec.Operatio return res } -// SetContinueOnErrors sets the ContinueOnErrors option for this validator. -func (s *SpecValidator) SetContinueOnErrors(c bool) { - s.Options.ContinueOnErrors = c -} - // expandedAnalyzer returns expanded.Analyzer when it is available. // otherwise just analyzer. func (s *SpecValidator) expandedAnalyzer() *analysis.Spec { diff --git a/type.go b/type.go index f87abb3..d64ece8 100644 --- a/type.go +++ b/type.go @@ -55,6 +55,70 @@ func newTypeValidator(path, in string, typ spec.StringOrArray, nullable bool, fo return t } +func (t *typeValidator) SetPath(path string) { + t.Path = path +} + +func (t *typeValidator) Applies(source interface{}, _ reflect.Kind) bool { + // typeValidator applies to Schema, Parameter and Header objects + switch source.(type) { + case *spec.Schema: + case *spec.Parameter: + case *spec.Header: + default: + return false + } + + return (len(t.Type) > 0 || t.Format != "") +} + +func (t *typeValidator) Validate(data interface{}) *Result { + if t.Options.recycleValidators { + defer func() { + t.redeem() + }() + } + + if data == nil { + // nil or zero value for the passed structure require Type: null + if len(t.Type) > 0 && !t.Type.Contains(nullType) && !t.Nullable { // TODO: if a property is not required it also passes this + return errorHelp.sErr(errors.InvalidType(t.Path, t.In, strings.Join(t.Type, ","), nullType), t.Options.recycleResult) + } + + return emptyResult + } + + // check if the type matches, should be used in every validator chain as first item + val := reflect.Indirect(reflect.ValueOf(data)) + kind := val.Kind() + + // infer schema type (JSON) and format from passed data type + schType, format := t.schemaInfoForType(data) + + // check numerical types + // TODO: check unsigned ints + // TODO: check json.Number (see schema.go) + isLowerInt := t.Format == integerFormatInt64 && format == integerFormatInt32 + isLowerFloat := t.Format == numberFormatFloat64 && format == numberFormatFloat32 + isFloatInt := schType == numberType && swag.IsFloat64AJSONInteger(val.Float()) && t.Type.Contains(integerType) + isIntFloat := schType == integerType && t.Type.Contains(numberType) + + if kind != reflect.String && kind != reflect.Slice && t.Format != "" && !t.Type.Contains(schType) && format != t.Format && !isFloatInt && !isIntFloat && !isLowerInt && !isLowerFloat { + // TODO: test case + return errorHelp.sErr(errors.InvalidType(t.Path, t.In, t.Format, format), t.Options.recycleResult) + } + + if !t.Type.Contains(numberType) && !t.Type.Contains(integerType) && t.Format != "" && (kind == reflect.String || kind == reflect.Slice) { + return emptyResult + } + + if !t.Type.Contains(schType) && !isFloatInt && !isIntFloat { + return errorHelp.sErr(errors.InvalidType(t.Path, t.In, strings.Join(t.Type, ","), schType), t.Options.recycleResult) + } + + return emptyResult +} + func (t *typeValidator) schemaInfoForType(data interface{}) (string, string) { // internal type to JSON type with swagger 2.0 format (with go-openapi/strfmt extensions), // see https://github.com/go-openapi/strfmt/blob/master/README.md @@ -144,70 +208,6 @@ func (t *typeValidator) schemaInfoForType(data interface{}) (string, string) { return "", "" } -func (t *typeValidator) SetPath(path string) { - t.Path = path -} - -func (t *typeValidator) Applies(source interface{}, _ reflect.Kind) bool { - // typeValidator applies to Schema, Parameter and Header objects - switch source.(type) { - case *spec.Schema: - case *spec.Parameter: - case *spec.Header: - default: - return false - } - - return (len(t.Type) > 0 || t.Format != "") -} - -func (t *typeValidator) Validate(data interface{}) *Result { - if t.Options.recycleValidators { - defer func() { - t.redeem() - }() - } - - if data == nil { - // nil or zero value for the passed structure require Type: null - if len(t.Type) > 0 && !t.Type.Contains(nullType) && !t.Nullable { // TODO: if a property is not required it also passes this - return errorHelp.sErr(errors.InvalidType(t.Path, t.In, strings.Join(t.Type, ","), nullType), t.Options.recycleResult) - } - - return emptyResult - } - - // check if the type matches, should be used in every validator chain as first item - val := reflect.Indirect(reflect.ValueOf(data)) - kind := val.Kind() - - // infer schema type (JSON) and format from passed data type - schType, format := t.schemaInfoForType(data) - - // check numerical types - // TODO: check unsigned ints - // TODO: check json.Number (see schema.go) - isLowerInt := t.Format == integerFormatInt64 && format == integerFormatInt32 - isLowerFloat := t.Format == numberFormatFloat64 && format == numberFormatFloat32 - isFloatInt := schType == numberType && swag.IsFloat64AJSONInteger(val.Float()) && t.Type.Contains(integerType) - isIntFloat := schType == integerType && t.Type.Contains(numberType) - - if kind != reflect.String && kind != reflect.Slice && t.Format != "" && !(t.Type.Contains(schType) || format == t.Format || isFloatInt || isIntFloat || isLowerInt || isLowerFloat) { - // TODO: test case - return errorHelp.sErr(errors.InvalidType(t.Path, t.In, t.Format, format), t.Options.recycleResult) - } - - if !(t.Type.Contains(numberType) || t.Type.Contains(integerType)) && t.Format != "" && (kind == reflect.String || kind == reflect.Slice) { - return emptyResult - } - - if !(t.Type.Contains(schType) || isFloatInt || isIntFloat) { - return errorHelp.sErr(errors.InvalidType(t.Path, t.In, strings.Join(t.Type, ","), schType), t.Options.recycleResult) - } - - return emptyResult -} - func (t *typeValidator) redeem() { pools.poolOfTypeValidators.RedeemValidator(t) }