From 6d81fa8ff028065dffc6fa002dfaf87e313d88ce Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 31 May 2021 09:03:52 +0200 Subject: [PATCH 1/3] wip Signed-off-by: Pierre Fenoll --- README.md | 45 +- go.mod | 1 + go.sum | 6 + openapi3/errors.go | 38 + openapi3/openapi3.go | 41 + openapi3/schema.go | 963 ++++------------------- openapi3/schema_formats.go | 103 +-- openapi3/schema_issue289_test.go | 8 +- openapi3/schema_test.go | 18 +- openapi3/schema_validation_settings.go | 6 - openapi3/unique_items_checker_test.go | 36 - openapi3filter/validate_readonly_test.go | 3 + openapi3filter/validate_request.go | 16 +- openapi3filter/validate_response.go | 8 +- openapi3filter/validation_handler.go | 3 + openapi3filter/validation_test.go | 18 +- 16 files changed, 324 insertions(+), 989 deletions(-) delete mode 100644 openapi3/unique_items_checker_test.go diff --git a/README.md b/README.md index f147fe1be..c86110bfd 100644 --- a/README.md +++ b/README.md @@ -155,44 +155,19 @@ func xmlBodyDecoder(body []byte) (interface{}, error) { } ``` -## Custom function to check uniqueness of array items - -By defaut, the library check unique items by below predefined function - -```go -func isSliceOfUniqueItems(xs []interface{}) bool { - s := len(xs) - m := make(map[string]struct{}, s) - for _, x := range xs { - key, _ := json.Marshal(&x) - m[string(key)] = struct{}{} - } - return s == len(m) -} -``` - -In the predefined function using `json.Marshal` to generate a string can -be used as a map key which is to support check the uniqueness of an array -when the array items are objects or arrays. You can register -you own function according to your input data to get better performance: - -```go -func main() { - // ... - - // Register a customized function used to check uniqueness of array. - openapi3.RegisterArrayUniqueItemsChecker(arrayUniqueItemsChecker) - - // ... other validate codes -} - -func arrayUniqueItemsChecker(items []interface{}) bool { - // Check the uniqueness of the input slice -} -``` ## Sub-v0 breaking API changes +### v0.??? +OpenAPIv3 "in-house" schema validation was replaced with a correct JSON Schema implementation and conversion from OpenAPIv3 Schema to JSON Schema. +* Dropped `openapi3.ErrOneOfConflict`: now when a value matches more than one `oneOf` schemas the error string contains `Must validate one and only one schema (oneOf)` +* Dropped `openapi3.SchemaFormatValidationDisabled`: any `openapi3.Schema.Format` value is valid. +* Dropped `openapi3.FailFast() openapi3.SchemaValidationOption` and `openapi3.SchemaErrorDetailsDisabled`: validating values against schemas is offloaded to a third-party library that does not provide such a mechanism. +* Dropped `openapi3.RegisterArrayUniqueItemsChecker(openapi3.SliceUniqueItemsChecker)`: validating values against schemas is offloaded to a third-party library that does not provide such a mechanism. +* Dropped `openapi3.SchemaStringFormats`, `openapi3.FormatCallback`, `openapi3.Format`, `openapi3.FormatOfStringForUUIDOfRFC4122`, `openapi3.DefineStringFormat(...)` and `openapi3.DefineStringFormatCallback(...)`. If your special format is not already under [`gojsonschema.FormatCheckers`](https://pkg.go.dev/github.com/xeipuuv/gojsonschema#pkg-variables), first define a [`gojsonschema.FormatChecker`](https://pkg.go.dev/github.com/xeipuuv/gojsonschema#FormatChecker) and register it with [`gojsonschema.FormatCheckers.Add("my-format", myImpl{})`](https://pkg.go.dev/github.com/xeipuuv/gojsonschema#FormatCheckerChain.Add) *before compiling your schemas*. +* Dropped `openapi3.ErrSchemaInputNaN` and `openapi3.ErrSchemaInputInf`: OpenAPIv3 does not explicitly mention the related values. +* Replaced `openapi3.SchemaError` with `openapi3.SchemaValidationError` which wraps `[]gojsonschema.ResultError` and thus provides similar functionality and more. + ### v0.61.0 * Renamed `openapi2.Swagger` to `openapi2.T`. * Renamed `openapi2conv.FromV3Swagger` to `openapi2conv.FromV3`. diff --git a/go.mod b/go.mod index f84f470c1..ce446d226 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,6 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 github.com/gorilla/mux v1.8.0 github.com/stretchr/testify v1.5.1 + github.com/xeipuuv/gojsonschema v1.2.0 gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index 2b289d716..f73a627bd 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,12 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/openapi3/errors.go b/openapi3/errors.go index ce52cd483..99880414c 100644 --- a/openapi3/errors.go +++ b/openapi3/errors.go @@ -3,8 +3,46 @@ package openapi3 import ( "bytes" "errors" + "strings" + + "github.com/xeipuuv/gojsonschema" ) +// SchemaValidationError is a collection of errors +type SchemaValidationError []gojsonschema.ResultError + +var _ error = (*SchemaValidationError)(nil) + +func (e SchemaValidationError) Error() string { + var buff strings.Builder + for i, re := range []gojsonschema.ResultError(e) { + if i != 0 { + buff.WriteString("\n") + } + buff.WriteString(re.String()) + } + return buff.String() +} + +// Errors unwraps into much detailed errors. +// See https://pkg.go.dev/github.com/xeipuuv/gojsonschema#ResultError +func (e SchemaValidationError) Errors() []gojsonschema.ResultError { + return e +} + +// JSONPointer returns a dot (.) delimited "JSON path" to the context of the first error. +func (e SchemaValidationError) JSONPointer() string { + return []gojsonschema.ResultError(e)[0].Field() +} + +func (e SchemaValidationError) asMultiError() MultiError { + errs := make([]error, 0, len(e)) + for _, re := range e { + errs = append(errs, errors.New(re.String())) + } + return errs +} + // MultiError is a collection of errors, intended for when // multiple issues need to be reported upstream type MultiError []error diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index ee6887727..a124fac1c 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/xeipuuv/gojsonschema" ) // T is the root of an OpenAPI v3 document @@ -19,6 +20,46 @@ type T struct { Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"` Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + + refd, refdAsReq, refdAsRep *gojsonschema.SchemaLoader +} + +// CompileSchemas needs to be called before any use of VisitJSON*() +func (doc *T) CompileSchemas() error { + if err := doc.compileSchemas(newSchemaValidationSettings(VisitAsRequest())); err != nil { + return err + } + if err := doc.compileSchemas(newSchemaValidationSettings(VisitAsResponse())); err != nil { + return err + } + return doc.compileSchemas(newSchemaValidationSettings()) +} + +func (doc *T) compileSchemas(settings *schemaValidationSettings) (err error) { + docSchemas := doc.Components.Schemas + schemas := make(schemasJSON, len(docSchemas)) + for name, docSchema := range docSchemas { + schemas[name] = docSchema.Value.fromOpenAPISchema(settings) + } + //FIXME merge loops + refd := gojsonschema.NewSchemaLoader() + for name, schema := range schemas { + absRef := "#/components/schemas/" + name + sl := gojsonschema.NewGoLoader(schema) + if err = refd.AddSchema(absRef, sl); err != nil { + return + } + } + + switch { + case settings.asreq: + doc.refdAsReq = refd + case settings.asrep: + doc.refdAsRep = refd + default: + doc.refd = refd + } + return } func (doc *T) MarshalJSON() ([]byte, error) { diff --git a/openapi3/schema.go b/openapi3/schema.go index 8c59f3c04..86798bdac 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -6,33 +6,15 @@ import ( "encoding/json" "errors" "fmt" - "math" - "math/big" "regexp" "strconv" - "unicode/utf16" "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" + "github.com/xeipuuv/gojsonschema" ) -var ( - // SchemaErrorDetailsDisabled disables printing of details about schema errors. - SchemaErrorDetailsDisabled = false - - //SchemaFormatValidationDisabled disables validation of schema type formats. - SchemaFormatValidationDisabled = false - - errSchema = errors.New("input does not match the schema") - - // ErrOneOfConflict is the SchemaError Origin when data matches more than one oneOf schema - ErrOneOfConflict = errors.New("input matches more than one oneOf schemas") - - // ErrSchemaInputNaN may be returned when validating a number - ErrSchemaInputNaN = errors.New("floating point NaN is not allowed") - // ErrSchemaInputInf may be returned when validating a number - ErrSchemaInputInf = errors.New("floating point Inf is not allowed") -) +var errSchema = errors.New("input does not match the schema") // Float64Ptr is a helper for defining OpenAPI schemas. func Float64Ptr(value float64) *float64 { @@ -636,47 +618,14 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } schemaType := schema.Type + // NOTE: any format is valid, as per: + // > However, to support documentation needs, the format property is an open string-valued property, and can have any value. switch schemaType { case "": case "boolean": case "number": - if format := schema.Format; len(format) > 0 { - switch format { - case "float", "double": - default: - if !SchemaFormatValidationDisabled { - return unsupportedFormat(format) - } - } - } case "integer": - if format := schema.Format; len(format) > 0 { - switch format { - case "int32", "int64": - default: - if !SchemaFormatValidationDisabled { - return unsupportedFormat(format) - } - } - } case "string": - if format := schema.Format; len(format) > 0 { - switch format { - // Supported by OpenAPIv3.0.1: - case "byte", "binary", "date", "date-time", "password": - // In JSON Draft-07 (not validated yet though): - case "regex": - case "time", "email", "idn-email": - case "hostname", "idn-hostname", "ipv4", "ipv6": - case "uri", "uri-reference", "iri", "iri-reference", "uri-template": - case "json-pointer", "relative-json-pointer": - default: - // Try to check for custom defined formats - if _, ok := SchemaStringFormats[format]; !ok && !SchemaFormatValidationDisabled { - return unsupportedFormat(format) - } - } - } case "array": if schema.Items == nil { return errors.New("when schema type is 'array', schema 'items' must be non-null") @@ -719,809 +668,222 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) return } -func (schema *Schema) IsMatching(value interface{}) bool { - settings := newSchemaValidationSettings(FailFast()) - return schema.visitJSON(settings, value) == nil -} - -func (schema *Schema) IsMatchingJSONBoolean(value bool) bool { - settings := newSchemaValidationSettings(FailFast()) - return schema.visitJSON(settings, value) == nil -} - -func (schema *Schema) IsMatchingJSONNumber(value float64) bool { - settings := newSchemaValidationSettings(FailFast()) - return schema.visitJSON(settings, value) == nil -} - -func (schema *Schema) IsMatchingJSONString(value string) bool { - settings := newSchemaValidationSettings(FailFast()) - return schema.visitJSON(settings, value) == nil -} - -func (schema *Schema) IsMatchingJSONArray(value []interface{}) bool { - settings := newSchemaValidationSettings(FailFast()) - return schema.visitJSON(settings, value) == nil -} - -func (schema *Schema) IsMatchingJSONObject(value map[string]interface{}) bool { - settings := newSchemaValidationSettings(FailFast()) - return schema.visitJSON(settings, value) == nil -} - -func (schema *Schema) VisitJSON(value interface{}, opts ...SchemaValidationOption) error { - settings := newSchemaValidationSettings(opts...) - return schema.visitJSON(settings, value) -} +type schemaJSON = map[string]interface{} +type schemasJSON = map[string]schemaJSON -func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interface{}) (err error) { - switch value := value.(type) { - case nil: - return schema.visitJSONNull(settings) - case float64: - if math.IsNaN(value) { - return ErrSchemaInputNaN - } - if math.IsInf(value, 0) { - return ErrSchemaInputInf - } - } - - if schema.IsEmpty() { - return - } - if err = schema.visitSetOperations(settings, value); err != nil { - return - } - - switch value := value.(type) { - case nil: - return schema.visitJSONNull(settings) - case bool: - return schema.visitJSONBoolean(settings, value) - case float64: - return schema.visitJSONNumber(settings, value) - case string: - return schema.visitJSONString(settings, value) - case []interface{}: - return schema.visitJSONArray(settings, value) - case map[string]interface{}: - return schema.visitJSONObject(settings, value) - default: - return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "type", - Reason: fmt.Sprintf("unhandled value of type %T", value), - } +func (s *SchemaRef) fromOpenAPISchema(settings *schemaValidationSettings) (schema schemaJSON) { + if ref := s.Ref; ref != "" { + return schemaJSON{"$ref": ref} } + return s.Value.fromOpenAPISchema(settings) } -func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, value interface{}) (err error) { - if enum := schema.Enum; len(enum) != 0 { - for _, v := range enum { - if value == v { - return - } - } - if settings.failfast { - return errSchema - } - return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "enum", - Reason: "value is not one of the allowed values", - } - } +func (s *Schema) fromOpenAPISchema(settings *schemaValidationSettings) (schema schemaJSON) { + schema = make(schemaJSON) - if ref := schema.Not; ref != nil { - v := ref.Value - if v == nil { - return foundUnresolvedRef(ref.Ref) - } - var oldfailfast bool - oldfailfast, settings.failfast = settings.failfast, true - err := v.visitJSON(settings, value) - settings.failfast = oldfailfast - if err == nil { - if settings.failfast { - return errSchema - } - return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "not", - } - } + if sEnum := s.Enum; len(sEnum) != 0 { + schema["enum"] = sEnum } - if v := schema.OneOf; len(v) > 0 { - ok := 0 - for _, item := range v { - v := item.Value - if v == nil { - return foundUnresolvedRef(item.Ref) - } - var oldfailfast bool - oldfailfast, settings.failfast = settings.failfast, true - err := v.visitJSON(settings, value) - settings.failfast = oldfailfast - if err == nil { - if schema.Discriminator != nil { - pn := schema.Discriminator.PropertyName - if valuemap, okcheck := value.(map[string]interface{}); okcheck { - if discriminatorVal, okcheck := valuemap[pn]; okcheck == true { - mapref, okcheck := schema.Discriminator.Mapping[discriminatorVal.(string)] - if okcheck && mapref == item.Ref { - ok++ - } - } - } - } else { - ok++ - } - } - } - if ok != 1 { - if settings.failfast { - return errSchema - } - e := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "oneOf", - } - if ok > 1 { - e.Origin = ErrOneOfConflict - } - return e - } - } - - if v := schema.AnyOf; len(v) > 0 { - ok := false - for _, item := range v { - v := item.Value - if v == nil { - return foundUnresolvedRef(item.Ref) - } - var oldfailfast bool - oldfailfast, settings.failfast = settings.failfast, true - err := v.visitJSON(settings, value) - settings.failfast = oldfailfast - if err == nil { - ok = true - break - } - } - if !ok { - if settings.failfast { - return errSchema - } - return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "anyOf", - } - } + if sMinLength := s.MinLength; sMinLength != 0 { + schema["minLength"] = sMinLength } - - for _, item := range schema.AllOf { - v := item.Value - if v == nil { - return foundUnresolvedRef(item.Ref) - } - var oldfailfast bool - oldfailfast, settings.failfast = settings.failfast, false - err := v.visitJSON(settings, value) - settings.failfast = oldfailfast - if err != nil { - if settings.failfast { - return errSchema - } - return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "allOf", - Origin: err, - } - } + if sMaxLength := s.MaxLength; nil != sMaxLength { + schema["maxLength"] = *sMaxLength } - return -} -func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err error) { - if schema.Nullable { - return - } - if settings.failfast { - return errSchema + if sFormat := s.Format; sFormat != "" { + schema["format"] = sFormat } - return &SchemaError{ - Value: nil, - Schema: schema, - SchemaField: "nullable", - Reason: "Value is not nullable", - } -} - -func (schema *Schema) VisitJSONBoolean(value bool) error { - settings := newSchemaValidationSettings() - return schema.visitJSONBoolean(settings, value) -} - -func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) { - if schemaType := schema.Type; schemaType != "" && schemaType != "boolean" { - return schema.expectedType(settings, "boolean") + if sPattern := s.Pattern; sPattern != "" { + schema["pattern"] = sPattern } - return -} -func (schema *Schema) VisitJSONNumber(value float64) error { - settings := newSchemaValidationSettings() - return schema.visitJSONNumber(settings, value) -} - -func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) error { - var me MultiError - schemaType := schema.Type - if schemaType == "integer" { - if bigFloat := big.NewFloat(value); !bigFloat.IsInt() { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "type", - Reason: "Value must be an integer", - } - if !settings.multiError { - return err - } - me = append(me, err) - } - } else if schemaType != "" && schemaType != "number" { - return schema.expectedType(settings, "number, integer") + if nil != s.Min { + schema["minimum"] = *s.Min } - - // "exclusiveMinimum" - if v := schema.ExclusiveMin; v && !(*schema.Min < value) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "exclusiveMinimum", - Reason: fmt.Sprintf("number must be more than %g", *schema.Min), - } - if !settings.multiError { - return err - } - me = append(me, err) + if nil != s.Max { + schema["maximum"] = *s.Max } - - // "exclusiveMaximum" - if v := schema.ExclusiveMax; v && !(*schema.Max > value) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "exclusiveMaximum", - Reason: fmt.Sprintf("number must be less than %g", *schema.Max), - } - if !settings.multiError { - return err - } - me = append(me, err) + if sExMin := s.ExclusiveMin; sExMin { + schema["exclusiveMinimum"] = sExMin } - - // "minimum" - if v := schema.Min; v != nil && !(*v <= value) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "minimum", - Reason: fmt.Sprintf("number must be at least %g", *v), - } - if !settings.multiError { - return err - } - me = append(me, err) + if sExMax := s.ExclusiveMax; sExMax { + schema["exclusiveMaximum"] = sExMax } - - // "maximum" - if v := schema.Max; v != nil && !(*v >= value) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "maximum", - Reason: fmt.Sprintf("number must be most %g", *v), - } - if !settings.multiError { - return err - } - me = append(me, err) + if nil != s.MultipleOf { + schema["multipleOf"] = *s.MultipleOf } - // "multipleOf" - if v := schema.MultipleOf; v != nil { - // "A numeric instance is valid only if division by this keyword's - // value results in an integer." - if bigFloat := big.NewFloat(value / *v); !bigFloat.IsInt() { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "multipleOf", - } - if !settings.multiError { - return err - } - me = append(me, err) - } + if sUniq := s.UniqueItems; sUniq { + schema["uniqueItems"] = sUniq } - - if len(me) > 0 { - return me + if sMinItems := s.MinItems; sMinItems != 0 { + schema["minItems"] = sMinItems } - - return nil -} - -func (schema *Schema) VisitJSONString(value string) error { - settings := newSchemaValidationSettings() - return schema.visitJSONString(settings, value) -} - -func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error { - if schemaType := schema.Type; schemaType != "" && schemaType != "string" { - return schema.expectedType(settings, "string") + if nil != s.MaxItems { + schema["maxItems"] = *s.MaxItems } - - var me MultiError - - // "minLength" and "maxLength" - minLength := schema.MinLength - maxLength := schema.MaxLength - if minLength != 0 || maxLength != nil { - // JSON schema string lengths are UTF-16, not UTF-8! - length := int64(0) - for _, r := range value { - if utf16.IsSurrogate(r) { - length += 2 - } else { - length++ - } - } - if minLength != 0 && length < int64(minLength) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "minLength", - Reason: fmt.Sprintf("minimum string length is %d", minLength), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - if maxLength != nil && length > int64(*maxLength) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "maxLength", - Reason: fmt.Sprintf("maximum string length is %d", *maxLength), - } - if !settings.multiError { - return err - } - me = append(me, err) + if sItems := s.Items; nil != sItems { + if sItems.Value != nil && sItems.Value.IsEmpty() { + schema["items"] = []schemaJSON{} + } else { + schema["items"] = []schemaJSON{sItems.fromOpenAPISchema(settings)} } } - // "pattern" - if pattern := schema.Pattern; pattern != "" && schema.compiledPattern == nil { - var err error - if schema.compiledPattern, err = regexp.Compile(pattern); err != nil { - err = &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "pattern", - Reason: fmt.Sprintf("cannot compile pattern %q: %v", pattern, err), - } - if !settings.multiError { - return err - } - me = append(me, err) - } + if sMinProps := s.MinProps; sMinProps != 0 { + schema["minProperties"] = sMinProps } - if cp := schema.compiledPattern; cp != nil && !cp.MatchString(value) { - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "pattern", - Reason: fmt.Sprintf("string doesn't match the regular expression %q", schema.Pattern), - } - if !settings.multiError { - return err - } - me = append(me, err) + if nil != s.MaxProps { + schema["maxProperties"] = *s.MaxProps } - // "format" - var formatErr string - if format := schema.Format; format != "" { - if f, ok := SchemaStringFormats[format]; ok { + if sRequired := s.Required; len(sRequired) != 0 { + required := make([]string, 0, len(sRequired)) + for _, propName := range sRequired { + prop := s.Properties[propName] switch { - case f.regexp != nil && f.callback == nil: - if cp := f.regexp; !cp.MatchString(value) { - formatErr = fmt.Sprintf("string doesn't match the format %q (regular expression %q)", format, cp.String()) - } - case f.regexp == nil && f.callback != nil: - if err := f.callback(value); err != nil { - formatErr = err.Error() - } + case settings.asreq && prop != nil && prop.Value.ReadOnly: + case settings.asrep && prop != nil && prop.Value.WriteOnly: default: - formatErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format) + required = append(required, propName) } } - } - if formatErr != "" { - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "format", - Reason: formatErr, - } - if !settings.multiError { - return err - } - me = append(me, err) - + schema["required"] = required } - if len(me) > 0 { - return me + if count := len(s.Properties); count != 0 { + properties := make(schemasJSON, count) + for propName, prop := range s.Properties { + properties[propName] = prop.fromOpenAPISchema(settings) + } + schema["properties"] = properties } - return nil -} - -func (schema *Schema) VisitJSONArray(value []interface{}) error { - settings := newSchemaValidationSettings() - return schema.visitJSONArray(settings, value) -} - -func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) error { - if schemaType := schema.Type; schemaType != "" && schemaType != "array" { - return schema.expectedType(settings, "array") + if sAddProps := s.AdditionalPropertiesAllowed; sAddProps != nil { + // TODO: complete handling + schema["additionalProperties"] = sAddProps } - var me MultiError - - lenValue := int64(len(value)) - - // "minItems" - if v := schema.MinItems; v != 0 && lenValue < int64(v) { - if settings.failfast { - return errSchema + if sAllOf := s.AllOf; len(sAllOf) != 0 { + allOf := make([]schemaJSON, 0, len(sAllOf)) + for _, sOf := range sAllOf { + allOf = append(allOf, sOf.fromOpenAPISchema(settings)) } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "minItems", - Reason: fmt.Sprintf("minimum number of items is %d", v), - } - if !settings.multiError { - return err - } - me = append(me, err) + schema["allOf"] = allOf } - - // "maxItems" - if v := schema.MaxItems; v != nil && lenValue > int64(*v) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "maxItems", - Reason: fmt.Sprintf("maximum number of items is %d", *v), + if sAnyOf := s.AnyOf; len(sAnyOf) != 0 { + anyOf := make([]schemaJSON, 0, len(sAnyOf)) + for _, sOf := range sAnyOf { + anyOf = append(anyOf, sOf.fromOpenAPISchema(settings)) } - if !settings.multiError { - return err + schema["anyOf"] = anyOf + } + if sOneOf := s.OneOf; len(sOneOf) != 0 { + oneOf := make([]schemaJSON, 0, len(sOneOf)) + for _, sOf := range sOneOf { + oneOf = append(oneOf, sOf.fromOpenAPISchema(settings)) } - me = append(me, err) + schema["oneOf"] = oneOf } - // "uniqueItems" - if sliceUniqueItemsChecker == nil { - sliceUniqueItemsChecker = isSliceOfUniqueItems + if sType := s.Type; sType != "" { + schema["type"] = []string{s.Type} } - if v := schema.UniqueItems; v && !sliceUniqueItemsChecker(value) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "uniqueItems", - Reason: "duplicate items found", - } - if !settings.multiError { - return err - } - me = append(me, err) + + if sNot := s.Not; sNot != nil { + schema["not"] = sNot.fromOpenAPISchema(settings) } - // "items" - if itemSchemaRef := schema.Items; itemSchemaRef != nil { - itemSchema := itemSchemaRef.Value - if itemSchema == nil { - return foundUnresolvedRef(itemSchemaRef.Ref) - } - for i, item := range value { - if err := itemSchema.visitJSON(settings, item); err != nil { - err = markSchemaErrorIndex(err, i) - if !settings.multiError { - return err - } - if itemMe, ok := err.(MultiError); ok { - me = append(me, itemMe...) - } else { - me = append(me, err) - } - } - } + if s.IsEmpty() { + schema = schemaJSON{"not": schemaJSON{"type": "null"}} } - if len(me) > 0 { - return me + if s.Nullable { + schema = schemaJSON{"anyOf": []schemaJSON{ + {"type": "null"}, + schema, + }} } - return nil + schema["$schema"] = "http://json-schema.org/draft-04/schema#" + //FIXME + //https://github.com/openapi-contrib/openapi-schema-to-json-schema/blob/45c080c38027c30652263b4cc44cd3534f5ccc1b/lib/converters/schema.js + //https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#schemaObject + return } -func (schema *Schema) VisitJSONObject(value map[string]interface{}) error { - settings := newSchemaValidationSettings() - return schema.visitJSONObject(settings, value) +// VisitJSON validates given data against schema only. +func (schema *Schema) VisitJSON(value interface{}, opts ...SchemaValidationOption) error { + return schema.VisitData(nil, value, opts...) } -func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) error { - if schemaType := schema.Type; schemaType != "" && schemaType != "object" { - return schema.expectedType(settings, "object") - } - - var me MultiError - - // "properties" - properties := schema.Properties - lenValue := int64(len(value)) - - // "minProperties" - if v := schema.MinProps; v != 0 && lenValue < int64(v) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "minProperties", - Reason: fmt.Sprintf("there must be at least %d properties", v), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - - // "maxProperties" - if v := schema.MaxProps; v != nil && lenValue > int64(*v) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "maxProperties", - Reason: fmt.Sprintf("there must be at most %d properties", *v), +// VisitData validates given data against schema, using components from doc if given. +// It will use #/components/schemas if given doc is non-nil and doc.CompileSchemas() was called. +func (schema *Schema) VisitData(doc *T, data interface{}, opts ...SchemaValidationOption) (err error) { + settings := newSchemaValidationSettings(opts...) + ls := gojsonschema.NewGoLoader(schema.fromOpenAPISchema(settings)) + ld := gojsonschema.NewGoLoader(data) + + var res *gojsonschema.Result + if doc != nil { + if doc.refdAsReq == nil || doc.refdAsRep == nil || doc.refd == nil { + panic(`func (*T) CompileSchemas() error must be called first`) + } + var whole *gojsonschema.Schema + switch { + case settings.asreq: + whole, err = doc.refdAsReq.Compile(ls) + case settings.asrep: + whole, err = doc.refdAsRep.Compile(ls) + default: + whole, err = doc.refd.Compile(ls) } - if !settings.multiError { - return err + if err != nil { + return } - me = append(me, err) + res, err = whole.Validate(ld) + } else { + res, err = gojsonschema.Validate(ls, ld) } - - // "additionalProperties" - var additionalProperties *Schema - if ref := schema.AdditionalProperties; ref != nil { - additionalProperties = ref.Value - } - for k, v := range value { - if properties != nil { - propertyRef := properties[k] - if propertyRef != nil { - p := propertyRef.Value - if p == nil { - return foundUnresolvedRef(propertyRef.Ref) - } - if err := p.visitJSON(settings, v); err != nil { - if settings.failfast { - return errSchema - } - err = markSchemaErrorKey(err, k) - if !settings.multiError { - return err - } - if v, ok := err.(MultiError); ok { - me = append(me, v...) - continue - } - me = append(me, err) - } - continue - } - } - allowed := schema.AdditionalPropertiesAllowed - if additionalProperties != nil || allowed == nil || (allowed != nil && *allowed) { - if additionalProperties != nil { - if err := additionalProperties.visitJSON(settings, v); err != nil { - if settings.failfast { - return errSchema - } - err = markSchemaErrorKey(err, k) - if !settings.multiError { - return err - } - if v, ok := err.(MultiError); ok { - me = append(me, v...) - continue - } - me = append(me, err) - } - } - continue - } - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "properties", - Reason: fmt.Sprintf("property %q is unsupported", k), - } - if !settings.multiError { - return err - } - me = append(me, err) + if err != nil { + return } - // "required" - for _, k := range schema.Required { - if _, ok := value[k]; !ok { - if s := schema.Properties[k]; s != nil && s.Value.ReadOnly && settings.asreq { - continue - } - if s := schema.Properties[k]; s != nil && s.Value.WriteOnly && settings.asrep { - continue - } - if settings.failfast { - return errSchema - } - err := markSchemaErrorKey(&SchemaError{ - Value: value, - Schema: schema, - SchemaField: "required", - Reason: fmt.Sprintf("property %q is missing", k), - }, k) - if !settings.multiError { - return err - } - me = append(me, err) + if !res.Valid() { + err := SchemaValidationError(res.Errors()) + if settings.multiError { + return err.asMultiError() } + return err } - - if len(me) > 0 { - return me - } - - return nil -} - -func (schema *Schema) expectedType(settings *schemaValidationSettings, typ string) error { - if settings.failfast { - return errSchema - } - return &SchemaError{ - Value: typ, - Schema: schema, - SchemaField: "type", - Reason: "Field must be set to " + schema.Type + " or not be present", - } + return } type SchemaError struct { Value interface{} - reversePath []string Schema *Schema SchemaField string Reason string - Origin error -} - -func markSchemaErrorKey(err error, key string) error { - if v, ok := err.(*SchemaError); ok { - v.reversePath = append(v.reversePath, key) - return v - } - if v, ok := err.(MultiError); ok { - for _, e := range v { - _ = markSchemaErrorKey(e, key) - } - return v - } - return err -} - -func markSchemaErrorIndex(err error, index int) error { - if v, ok := err.(*SchemaError); ok { - v.reversePath = append(v.reversePath, strconv.FormatInt(int64(index), 10)) - return v - } - if v, ok := err.(MultiError); ok { - for _, e := range v { - _ = markSchemaErrorIndex(e, index) - } - return v - } - return err -} - -func (err *SchemaError) JSONPointer() []string { - reversePath := err.reversePath - path := append([]string(nil), reversePath...) - for left, right := 0, len(path)-1; left < right; left, right = left+1, right-1 { - path[left], path[right] = path[right], path[left] - } - return path + // Origin error } func (err *SchemaError) Error() string { - if err.Origin != nil { - return err.Origin.Error() - } + // if err.Origin != nil { + // return err.Origin.Error() + // } buf := bytes.NewBuffer(make([]byte, 0, 256)) - if len(err.reversePath) > 0 { - buf.WriteString(`Error at "`) - reversePath := err.reversePath - for i := len(reversePath) - 1; i >= 0; i-- { - buf.WriteByte('/') - buf.WriteString(reversePath[i]) - } - buf.WriteString(`": `) - } + // if len(err.reversePath) > 0 { + // buf.WriteString(`Error at "`) + // reversePath := err.reversePath + // for i := len(reversePath) - 1; i >= 0; i-- { + // buf.WriteByte('/') + // buf.WriteString(reversePath[i]) + // } + // buf.WriteString(`": `) + // } reason := err.Reason if reason == "" { buf.WriteString(`Doesn't match schema "`) @@ -1530,7 +892,7 @@ func (err *SchemaError) Error() string { } else { buf.WriteString(reason) } - if !SchemaErrorDetailsDisabled { + { // if !SchemaErrorDetailsDisabled { buf.WriteString("\nSchema:\n ") encoder := json.NewEncoder(buf) encoder.SetIndent(" ", " ") @@ -1544,34 +906,3 @@ func (err *SchemaError) Error() string { } return buf.String() } - -func isSliceOfUniqueItems(xs []interface{}) bool { - s := len(xs) - m := make(map[string]struct{}, s) - for _, x := range xs { - // The input slice is coverted from a JSON string, there shall - // have no error when covert it back. - key, _ := json.Marshal(&x) - m[string(key)] = struct{}{} - } - return s == len(m) -} - -// SliceUniqueItemsChecker is an function used to check if an given slice -// have unique items. -type SliceUniqueItemsChecker func(items []interface{}) bool - -// By default using predefined func isSliceOfUniqueItems which make use of -// json.Marshal to generate a key for map used to check if a given slice -// have unique items. -var sliceUniqueItemsChecker SliceUniqueItemsChecker = isSliceOfUniqueItems - -// RegisterArrayUniqueItemsChecker is used to register a customized function -// used to check if JSON array have unique items. -func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) { - sliceUniqueItemsChecker = fn -} - -func unsupportedFormat(format string) error { - return fmt.Errorf("unsupported 'format' value %q", format) -} diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index 1eb41509e..879bfd5ad 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -1,105 +1,50 @@ package openapi3 import ( - "fmt" - "net" "regexp" -) -const ( - // FormatOfStringForUUIDOfRFC4122 is an optional predefined format for UUID v1-v5 as specified by RFC4122 - FormatOfStringForUUIDOfRFC4122 = `^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$` + "github.com/xeipuuv/gojsonschema" + // https://github.com/xeipuuv/gojsonschema/pull/297/files discriminator support ) -//FormatCallback custom check on exotic formats -type FormatCallback func(Val string) error - -type Format struct { - regexp *regexp.Regexp - callback FormatCallback -} - -//SchemaStringFormats allows for validating strings format -var SchemaStringFormats = make(map[string]Format, 8) - -//DefineStringFormat Defines a new regexp pattern for a given format -func DefineStringFormat(name string, pattern string) { - re, err := regexp.Compile(pattern) - if err != nil { - err := fmt.Errorf("format %q has invalid pattern %q: %v", name, pattern, err) - panic(err) - } - SchemaStringFormats[name] = Format{regexp: re} +func init() { + // gojsonschema.FormatCheckers = gojsonschema.FormatCheckerChain{} FIXME https://github.com/xeipuuv/gojsonschema/pull/326 + gojsonschema.FormatCheckers.Add("byte", byteFormatChecker{}) + gojsonschema.FormatCheckers.Add("date", gojsonschema.DateFormatChecker{}) + gojsonschema.FormatCheckers.Add("date-time", gojsonschema.DateTimeFormatChecker{}) } -// DefineStringFormatCallback adds a validation function for a specific schema format entry -func DefineStringFormatCallback(name string, callback FormatCallback) { - SchemaStringFormats[name] = Format{callback: callback} -} +type byteFormatChecker struct{} -func validateIP(ip string) (*net.IP, error) { - parsed := net.ParseIP(ip) - if parsed == nil { - return nil, &SchemaError{ - Value: ip, - Reason: "Not an IP address", - } - } - return &parsed, nil -} +var _ gojsonschema.FormatChecker = (*byteFormatChecker)(nil) +var reByteFormatChecker = regexp.MustCompile(`(^$|^[a-zA-Z0-9+/\-_]*=*$)`) -func validateIPv4(ip string) error { - parsed, err := validateIP(ip) - if err != nil { - return err +// IsFormat supports base64 and base64url. Padding ('=') is supported. +func (byteFormatChecker) IsFormat(input interface{}) bool { + asString, ok := input.(string) + if !ok { + return true } - if parsed.To4() == nil { - return &SchemaError{ - Value: ip, - Reason: "Not an IPv4 address (it's IPv6)", - } - } - return nil + return reByteFormatChecker.MatchString(asString) } -func validateIPv6(ip string) error { - parsed, err := validateIP(ip) - if err != nil { - return err - } - if parsed.To4() != nil { - return &SchemaError{ - Value: ip, - Reason: "Not an IPv6 address (it's IPv4)", - } - } - return nil +// DefineEmailFormat opts-in to checking email format (outside of OpenAPIv3 spec) +func DefineEmailFormat() { + gojsonschema.FormatCheckers.Add("email", gojsonschema.EmailFormatChecker{}) } -func init() { - // This pattern catches only some suspiciously wrong-looking email addresses. - // Use DefineStringFormat(...) if you need something stricter. - DefineStringFormat("email", `^[^@]+@[^@<>",\s]+$`) - - // Base64 - // The pattern supports base64 and b./ase64url. Padding ('=') is supported. - DefineStringFormat("byte", `(^$|^[a-zA-Z0-9+/\-_]*=*$)`) - - // date - DefineStringFormat("date", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$`) - - // date-time - DefineStringFormat("date-time", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`) - +// DefineUUIDFormat opts-in to checking uuid format v1-v5 as specified by RFC4122 (outside of OpenAPIv3 spec) +func DefineUUIDFormat() { + gojsonschema.FormatCheckers.Add("uuid", gojsonschema.UUIDFormatChecker{}) } // DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec func DefineIPv4Format() { - DefineStringFormatCallback("ipv4", validateIPv4) + gojsonschema.FormatCheckers.Add("ipv4", gojsonschema.IPV4FormatChecker{}) } // DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec func DefineIPv6Format() { - DefineStringFormatCallback("ipv6", validateIPv6) + gojsonschema.FormatCheckers.Add("ipv6", gojsonschema.IPV6FormatChecker{}) } diff --git a/openapi3/schema_issue289_test.go b/openapi3/schema_issue289_test.go index 6ab6b63d5..ac8994929 100644 --- a/openapi3/schema_issue289_test.go +++ b/openapi3/schema_issue289_test.go @@ -31,9 +31,13 @@ openapi: "3.0.1" s, err := NewLoader().LoadFromData(spec) require.NoError(t, err) - err = s.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{ + + err = s.CompileSchemas() + require.NoError(t, err) + + err = s.Components.Schemas["Server"].Value.VisitData(s, map[string]interface{}{ "name": "kin-openapi", "address": "127.0.0.1", }) - require.EqualError(t, err, ErrOneOfConflict.Error()) + require.EqualError(t, err, "address: Must validate one and only one schema (oneOf)") } diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index f621ca7c9..8d8bdba3f 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -311,7 +311,7 @@ var schemaExamples = []schemaExample{ }, { - Title: "STRING: format 'date-time'", + Title: `STRING: format "byte"`, Schema: NewBytesSchema(), Serialization: map[string]interface{}{ "type": "string", @@ -1207,14 +1207,20 @@ components: type: boolean type: object ` - data := map[string]interface{}{ - "name": "kin-openapi", - "ownerName": true, - } s, err := NewLoader().LoadFromData([]byte(api)) require.NoError(t, err) require.NotNil(t, s) - err = s.Components.Schemas["Test"].Value.VisitJSON(data) + + err = s.Validate(context.Background()) + require.NoError(t, err) + + err = s.CompileSchemas() + require.NoError(t, err) + + err = s.Components.Schemas["Test"].Value.VisitData(s, map[string]interface{}{ + "name": "kin-openapi", + "ownerName": true, + }) require.NotNil(t, err) require.NotEqual(t, errSchema, err) require.Contains(t, err.Error(), `Error at "/ownerName": Doesn't match schema "not"`) diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index 71db5f237..c85171b84 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -4,16 +4,10 @@ package openapi3 type SchemaValidationOption func(*schemaValidationSettings) type schemaValidationSettings struct { - failfast bool multiError bool asreq, asrep bool // exclusive (XOR) fields } -// FailFast returns schema validation errors quicker. -func FailFast() SchemaValidationOption { - return func(s *schemaValidationSettings) { s.failfast = true } -} - func MultiErrors() SchemaValidationOption { return func(s *schemaValidationSettings) { s.multiError = true } } diff --git a/openapi3/unique_items_checker_test.go b/openapi3/unique_items_checker_test.go deleted file mode 100644 index 85147c67a..000000000 --- a/openapi3/unique_items_checker_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package openapi3_test - -import ( - "strings" - "testing" - - "github.com/getkin/kin-openapi/openapi3" - "github.com/stretchr/testify/require" -) - -func TestRegisterArrayUniqueItemsChecker(t *testing.T) { - var ( - schema = openapi3.Schema{ - Type: "array", - UniqueItems: true, - Items: openapi3.NewStringSchema().NewRef(), - } - val = []interface{}{"1", "2", "3"} - ) - - // Fist checked by predefined function - err := schema.VisitJSON(val) - require.NoError(t, err) - - // Register a function will always return false when check if a - // slice has unique items, then use a slice indeed has unique - // items to verify that check unique items will failed. - openapi3.RegisterArrayUniqueItemsChecker(func(items []interface{}) bool { - return false - }) - defer openapi3.RegisterArrayUniqueItemsChecker(nil) // Reset for other tests - - err = schema.VisitJSON(val) - require.Error(t, err) - require.True(t, strings.HasPrefix(err.Error(), "duplicate items found")) -} diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_test.go index 454a927e9..efba32b0d 100644 --- a/openapi3filter/validate_readonly_test.go +++ b/openapi3filter/validate_readonly_test.go @@ -73,6 +73,9 @@ func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) + err = sl.CompileSchemas() + require.NoError(t, err) + b, err := json.Marshal(Request{ID: "bt6kdc3d0cvp6u8u3ft0"}) require.NoError(t, err) diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 2f9a5f14c..a4f28fea1 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -154,7 +154,13 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param opts = make([]openapi3.SchemaValidationOption, 0, 1) opts = append(opts, openapi3.MultiErrors()) } - if err = schema.VisitJSON(value, opts...); err != nil { + + var spec *openapi3.T + if input.Route != nil { + spec = input.Route.Spec + } + + if err = schema.VisitData(spec, value, opts...); err != nil { return &RequestError{Input: input, Parameter: parameter, Err: err} } return nil @@ -237,8 +243,12 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req opts = append(opts, openapi3.MultiErrors()) } - // Validate JSON with the schema - if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { + var spec *openapi3.T + if input.Route != nil { + spec = input.Route.Spec + } + + if err := contentType.Schema.Value.VisitData(spec, value, opts...); err != nil { return &RequestError{ Input: input, RequestBody: requestBody, diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index b70938e7c..37e9b0c96 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -126,8 +126,12 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error opts = append(opts, openapi3.MultiErrors()) } - // Validate data with the schema. - if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { + var spec *openapi3.T + if input.RequestValidationInput.Route != nil { + spec = input.RequestValidationInput.Route.Spec + } + + if err := contentType.Schema.Value.VisitData(spec, value, opts...); err != nil { return &ResponseError{ Input: input, Reason: "response body doesn't match the schema", diff --git a/openapi3filter/validation_handler.go b/openapi3filter/validation_handler.go index eeb1ca1ea..19259749b 100644 --- a/openapi3filter/validation_handler.go +++ b/openapi3filter/validation_handler.go @@ -32,6 +32,9 @@ func (h *ValidationHandler) Load() error { if err := doc.Validate(loader.Context); err != nil { return err } + if err := doc.CompileSchemas(); err != nil { + return err + } if h.router, err = legacyrouter.NewRouter(doc); err != nil { return err } diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index d4d0a12f1..2cfc726c9 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -155,10 +155,13 @@ func TestFilter(t *testing.T) { }, } - err := doc.Validate(context.Background()) + err := doc.CompileSchemas() + require.NoError(t, err) + err = doc.Validate(context.Background()) require.NoError(t, err) router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) + expectWithDecoder := func(req ExampleRequest, resp ExampleResponse, decoder ContentParameterDecoder) error { t.Logf("Request: %s %s", req.Method, req.URL) httpReq, err := http.NewRequest(req.Method, req.URL, marshalReader(req.Body)) @@ -198,6 +201,7 @@ func TestFilter(t *testing.T) { require.NoError(t, err) return err } + expect := func(req ExampleRequest, resp ExampleResponse) error { return expectWithDecoder(req, resp, nil) } @@ -570,7 +574,9 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test } } - err := doc.Validate(context.Background()) + err := doc.CompileSchemas() + require.NoError(t, err) + err = doc.Validate(context.Background()) require.NoError(t, err) router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) @@ -701,7 +707,9 @@ func TestAnySecurityRequirementMet(t *testing.T) { } } - err := doc.Validate(context.Background()) + err := doc.CompileSchemas() + require.NoError(t, err) + err = doc.Validate(context.Background()) require.NoError(t, err) router, err := legacyrouter.NewRouter(&doc) require.NoError(t, err) @@ -803,7 +811,9 @@ func TestAllSchemesMet(t *testing.T) { } } - err := doc.Validate(context.Background()) + err := doc.CompileSchemas() + require.NoError(t, err) + err = doc.Validate(context.Background()) require.NoError(t, err) router, err := legacyrouter.NewRouter(&doc) require.NoError(t, err) From fb769cba79ab3a67dae2b6b0e4ac8012428223be Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 31 May 2021 09:46:27 +0200 Subject: [PATCH 2/3] wip Signed-off-by: Pierre Fenoll --- openapi3/schema_test.go | 42 ++++++------- openapi3filter/unpack_errors_test.go | 6 +- openapi3filter/validation_error_encoder.go | 14 ++--- openapi3filter/validation_error_test.go | 68 +++++++++++----------- 4 files changed, 66 insertions(+), 64 deletions(-) diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 8d8bdba3f..eb8f7c2a8 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -4,9 +4,9 @@ import ( "context" "encoding/base64" "encoding/json" - "fmt" + // "fmt" "math" - "reflect" + // "reflect" "strings" "testing" @@ -22,8 +22,10 @@ type schemaExample struct { } func TestSchemas(t *testing.T) { - DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) for _, example := range schemaExamples { + if example.Title == "STRING: optional format 'uuid'" { + // DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) + } t.Run(example.Title, testSchema(t, example)) } } @@ -1035,11 +1037,11 @@ var schemaErrorExamples = []schemaErrorExample{ Value: 1, Schema: &Schema{}, Reason: "PARENT", - Origin: &SchemaError{ - Value: 1, - Schema: &Schema{}, - Reason: "NEST", - }, + // Origin: &SchemaError{ FIXME + // Value: 1, + // Schema: &Schema{}, + // Reason: "NEST", + // }, }, Want: "NEST", }, @@ -1076,13 +1078,13 @@ func testSchemaMultiError(t *testing.T, example schemaMultiErrorExample) func(*t scherr, _ := e.(*SchemaError) for _, expectedErr := range expected { expectedScherr, _ := expectedErr.(*SchemaError) - if reflect.DeepEqual(expectedScherr.reversePath, scherr.reversePath) && - expectedScherr.SchemaField == scherr.SchemaField { + if /*reflect.DeepEqual(expectedScherr.reversePath, scherr.reversePath) &&*/ + expectedScherr.SchemaField == scherr.SchemaField { found = true break } } - require.True(t, found, fmt.Sprintf("missing %s error on %s", scherr.SchemaField, strings.Join(scherr.JSONPointer(), "."))) + require.True(t, found /*fmt.Sprintf("missing %s error on %s", scherr.SchemaField, strings.Join(scherr.JSONPointer(), "."))*/) } } } @@ -1132,13 +1134,13 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ ExpectedErrors: []MultiError{ { &SchemaError{SchemaField: "minItems"}, - &SchemaError{SchemaField: "pattern", reversePath: []string{"0"}}, + &SchemaError{SchemaField: "pattern" /*reversePath: []string{"0"}*/}, }, { &SchemaError{SchemaField: "maxItems"}, - &SchemaError{SchemaField: "pattern", reversePath: []string{"0"}}, - &SchemaError{SchemaField: "pattern", reversePath: []string{"1"}}, - &SchemaError{SchemaField: "pattern", reversePath: []string{"2"}}, + &SchemaError{SchemaField: "pattern" /*reversePath: []string{"0"}*/}, + &SchemaError{SchemaField: "pattern" /*reversePath: []string{"1"}*/}, + &SchemaError{SchemaField: "pattern" /*reversePath: []string{"2"}*/}, }, }, }, @@ -1161,8 +1163,8 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ }, ExpectedErrors: []MultiError{ { - &SchemaError{SchemaField: "type", reversePath: []string{"key1", "0"}}, - &SchemaError{SchemaField: "type", reversePath: []string{"key2", "0"}}, + &SchemaError{SchemaField: "type" /*reversePath: []string{"key1", "0"}*/}, + &SchemaError{SchemaField: "type" /*reversePath: []string{"key2", "0"}*/}, }, }, }, @@ -1185,9 +1187,9 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ }, ExpectedErrors: []MultiError{ { - &SchemaError{SchemaField: "type", reversePath: []string{"key1"}}, - &SchemaError{SchemaField: "type", reversePath: []string{"key2"}}, - &SchemaError{SchemaField: "pattern", reversePath: []string{"1", "key3"}}, + &SchemaError{SchemaField: "type" /*reversePath: []string{"key1"}*/}, + &SchemaError{SchemaField: "type" /*reversePath: []string{"key2"}*/}, + &SchemaError{SchemaField: "pattern" /*reversePath: []string{"1", "key3"}*/}, }, }, }, diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go index 4242177f9..a11b3abc3 100644 --- a/openapi3filter/unpack_errors_test.go +++ b/openapi3filter/unpack_errors_test.go @@ -123,9 +123,9 @@ func convertError(me openapi3.MultiError) map[string][]string { case *openapi3.SchemaError: // Can inspect schema validation errors here, e.g. err.Value field := prefixBody - if path := err.JSONPointer(); len(path) > 0 { - field = fmt.Sprintf("%s.%s", field, strings.Join(path, ".")) - } + // if path := err.JSONPointer(); len(path) > 0 { + // field = fmt.Sprintf("%s.%s", field, strings.Join(path, ".")) + // } if _, ok := issues[field]; !ok { issues[field] = make([]string, 0, 3) } diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go index 707b22d4a..449603197 100644 --- a/openapi3filter/validation_error_encoder.go +++ b/openapi3filter/validation_error_encoder.go @@ -119,10 +119,10 @@ func convertParseError(e *RequestError, innerErr *ParseError) *ValidationError { func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *ValidationError { cErr := &ValidationError{Title: innerErr.Reason} - // Handle "Origin" error - if originErr, ok := innerErr.Origin.(*openapi3.SchemaError); ok { - cErr = convertSchemaError(e, originErr) - } + // // Handle "Origin" error + // if originErr, ok := innerErr.Origin.(*openapi3.SchemaError); ok { + // cErr = convertSchemaError(e, originErr) + // } // Add http status code if e.Parameter != nil { @@ -136,8 +136,8 @@ func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *Valida // We have a JSONPointer in the query param too so need to // make sure 'Parameter' check takes priority over 'Pointer' cErr.Source = &ValidationErrorSource{Parameter: e.Parameter.Name} - } else if ptr := innerErr.JSONPointer(); ptr != nil { - cErr.Source = &ValidationErrorSource{Pointer: toJSONPointer(ptr)} + // } else if ptr := innerErr.JSONPointer(); ptr != nil { + // cErr.Source = &ValidationErrorSource{Pointer: toJSONPointer(ptr)} } // Add details on allowed values for enums @@ -148,7 +148,7 @@ func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *Valida } cErr.Detail = fmt.Sprintf("value %v at %s must be one of: %s", innerErr.Value, - toJSONPointer(innerErr.JSONPointer()), + "toJSONPointer(innerErr.JSONPointer())", strings.Join(enums, ", ")) value := fmt.Sprintf("%v", innerErr.Value) if e.Parameter != nil && diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index d539c3f7e..8b30eaff5 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -35,24 +35,24 @@ type validationArgs struct { r *http.Request } type validationTest struct { - name string - fields validationFields - args validationArgs - wantErr bool - wantErrBody string - wantErrReason string - wantErrSchemaReason string - wantErrSchemaPath string - wantErrSchemaValue interface{} - wantErrSchemaOriginReason string - wantErrSchemaOriginPath string - wantErrSchemaOriginValue interface{} - wantErrParam string - wantErrParamIn string - wantErrParseKind ParseErrorKind - wantErrParseValue interface{} - wantErrParseReason string - wantErrResponse *ValidationError + name string + fields validationFields + args validationArgs + wantErr bool + wantErrBody string + wantErrReason string + wantErrSchemaReason string + wantErrSchemaPath string + wantErrSchemaValue interface{} + // wantErrSchemaOriginReason string + // wantErrSchemaOriginPath string + // wantErrSchemaOriginValue interface{} + wantErrParam string + wantErrParamIn string + wantErrParseKind ParseErrorKind + wantErrParseValue interface{} + wantErrParseReason string + wantErrResponse *ValidationError } func getValidationTests(t *testing.T) []*validationTest { @@ -382,12 +382,12 @@ func getValidationTests(t *testing.T) []*validationTest { args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet2", bytes.NewBufferString(`{"name":"Bahama"}`)), }, - wantErrReason: "doesn't match the schema", - wantErrSchemaPath: "/", - wantErrSchemaValue: map[string]string{"name": "Bahama"}, - wantErrSchemaOriginReason: `property "photoUrls" is missing`, - wantErrSchemaOriginValue: map[string]string{"name": "Bahama"}, - wantErrSchemaOriginPath: "/photoUrls", + wantErrReason: "doesn't match the schema", + wantErrSchemaPath: "/", + wantErrSchemaValue: map[string]string{"name": "Bahama"}, + // wantErrSchemaOriginReason: `property "photoUrls" is missing`, + // wantErrSchemaOriginValue: map[string]string{"name": "Bahama"}, + // wantErrSchemaOriginPath: "/photoUrls", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, Title: `property "photoUrls" is missing`, Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, @@ -487,21 +487,21 @@ func TestValidationHandler_validateRequest(t *testing.T) { if innerErr, ok := e.Err.(*openapi3.SchemaError); ok { req.Equal(tt.wantErrSchemaReason, innerErr.Reason) - pointer := toJSONPointer(innerErr.JSONPointer()) - req.Equal(tt.wantErrSchemaPath, pointer) + // pointer := toJSONPointer(innerErr.JSONPointer()) + // req.Equal(tt.wantErrSchemaPath, pointer) req.Equal(fmt.Sprintf("%v", tt.wantErrSchemaValue), fmt.Sprintf("%v", innerErr.Value)) - if originErr, ok := innerErr.Origin.(*openapi3.SchemaError); ok { - req.Equal(tt.wantErrSchemaOriginReason, originErr.Reason) - pointer := toJSONPointer(originErr.JSONPointer()) - req.Equal(tt.wantErrSchemaOriginPath, pointer) - req.Equal(fmt.Sprintf("%v", tt.wantErrSchemaOriginValue), fmt.Sprintf("%v", originErr.Value)) - } + // if originErr, ok := innerErr.Origin.(*openapi3.SchemaError); ok { + // req.Equal(tt.wantErrSchemaOriginReason, originErr.Reason) + // pointer := toJSONPointer(originErr.JSONPointer()) + // req.Equal(tt.wantErrSchemaOriginPath, pointer) + // req.Equal(fmt.Sprintf("%v", tt.wantErrSchemaOriginValue), fmt.Sprintf("%v", originErr.Value)) + // } } else { req.False(tt.wantErrSchemaReason != "" || tt.wantErrSchemaPath != "", "error = %v, not a SchemaError -- %#v", e.Err, e.Err) - req.False(tt.wantErrSchemaOriginReason != "" || tt.wantErrSchemaOriginPath != "", - "error = %v, not a SchemaError with Origin -- %#v", e.Err, e.Err) + // req.False(tt.wantErrSchemaOriginReason != "" || tt.wantErrSchemaOriginPath != "", + // "error = %v, not a SchemaError with Origin -- %#v", e.Err, e.Err) } if innerErr, ok := e.Err.(*ParseError); ok { From caf915813d5b0d55540925a879d0e2907c806c91 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 31 May 2021 09:46:42 +0200 Subject: [PATCH 3/3] fix some crashes in tests Signed-off-by: Pierre Fenoll --- openapi3filter/unpack_errors_test.go | 9 ++++++++- openapi3filter/validate_readonly_test.go | 6 ++++-- openapi3filter/validation_discriminator_test.go | 3 +++ routers/gorillamux/example_test.go | 3 +++ routers/legacy/validate_request_test.go | 3 +++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go index a11b3abc3..7463f2cc5 100644 --- a/openapi3filter/unpack_errors_test.go +++ b/openapi3filter/unpack_errors_test.go @@ -13,10 +13,17 @@ import ( ) func Example() { - doc, err := openapi3.NewLoader().LoadFromFile("./testdata/petstore.yaml") + loader := openapi3.NewLoader() + doc, err := loader.LoadFromFile("./testdata/petstore.yaml") if err != nil { panic(err) } + if err = doc.Validate(loader.Context); err != nil { + panic(err) + } + if err = doc.CompileSchemas(); err != nil { + panic(err) + } router, err := gorillamux.NewRouter(doc) if err != nil { diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_test.go index efba32b0d..25a38904e 100644 --- a/openapi3filter/validate_readonly_test.go +++ b/openapi3filter/validate_readonly_test.go @@ -68,12 +68,14 @@ func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { sl := openapi3.NewLoader() doc, err := sl.LoadFromData([]byte(spec)) require.NoError(t, err) + err = doc.Validate(sl.Context) require.NoError(t, err) - router, err := legacyrouter.NewRouter(doc) + + err = doc.CompileSchemas() require.NoError(t, err) - err = sl.CompileSchemas() + router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) b, err := json.Marshal(Request{ID: "bt6kdc3d0cvp6u8u3ft0"}) diff --git a/openapi3filter/validation_discriminator_test.go b/openapi3filter/validation_discriminator_test.go index c7d614403..9b1d2d895 100644 --- a/openapi3filter/validation_discriminator_test.go +++ b/openapi3filter/validation_discriminator_test.go @@ -79,6 +79,9 @@ components: doc, err := loader.LoadFromData([]byte(spec)) require.NoError(t, err) + err = doc.CompileSchemas() + require.NoError(t, err) + router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) diff --git a/routers/gorillamux/example_test.go b/routers/gorillamux/example_test.go index 33ce98e6a..9e9f04e16 100644 --- a/routers/gorillamux/example_test.go +++ b/routers/gorillamux/example_test.go @@ -20,6 +20,9 @@ func Example() { if err = doc.Validate(ctx); err != nil { panic(err) } + if err = doc.CompileSchemas(); err != nil { + panic(err) + } router, err := gorillamux.NewRouter(doc) if err != nil { panic(err) diff --git a/routers/legacy/validate_request_test.go b/routers/legacy/validate_request_test.go index 7737f5028..cdd3d66b0 100644 --- a/routers/legacy/validate_request_test.go +++ b/routers/legacy/validate_request_test.go @@ -72,6 +72,9 @@ func Example() { if err := doc.Validate(loader.Context); err != nil { panic(err) } + if err = doc.CompileSchemas(); err != nil { + panic(err) + } router, err := legacy.NewRouter(doc) if err != nil {