From ef5e945b7f58c16b42a81ae4a456266343a99b3e Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Tue, 6 Sep 2022 14:16:24 +0100 Subject: [PATCH 1/6] feat: support validation options specifically for disabling pattern validation --- openapi3/loader.go | 6 ++-- openapi3/loader_read_from_uri_func_test.go | 2 ++ openapi3/openapi3.go | 8 ++++- openapi3/schema.go | 18 +++++----- openapi3/schema_validation_settings.go | 17 ++++++++-- openapi3/validation_options.go | 39 ++++++++++++++++++++++ 6 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 openapi3/validation_options.go diff --git a/openapi3/loader.go b/openapi3/loader.go index 806b819df..fad5a0d76 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -51,7 +51,9 @@ type Loader struct { // NewLoader returns an empty Loader func NewLoader() *Loader { - return &Loader{} + return &Loader{ + Context: context.Background(), + } } func (loader *Loader) resetVisitedPathItemRefs() { @@ -406,7 +408,6 @@ func (loader *Loader) documentPathForRecursiveRef(current *url.URL, resolvedRef return current } return &url.URL{Path: path.Join(loader.rootDir, resolvedRef)} - } func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) { @@ -837,7 +838,6 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP } func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documentPath *url.URL) (err error) { - if component == nil { return errors.New("invalid callback: value MUST be an object") } diff --git a/openapi3/loader_read_from_uri_func_test.go b/openapi3/loader_read_from_uri_func_test.go index 8fee2f4c2..0901c5526 100644 --- a/openapi3/loader_read_from_uri_func_test.go +++ b/openapi3/loader_read_from_uri_func_test.go @@ -1,6 +1,7 @@ package openapi3 import ( + "context" "fmt" "io/ioutil" "net/url" @@ -59,6 +60,7 @@ func TestResolveSchemaExternalRef(t *testing.T) { loader := &Loader{ IsExternalRefsAllowed: true, ReadFromURIFunc: multipleSourceLoader.LoadFromURI, + Context: context.Background(), } doc, err := loader.LoadFromURI(rootLocation) diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index cb9183d47..af58cf89e 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -54,7 +54,13 @@ func (doc *T) AddServer(server *Server) { } // Validate returns an error if T does not comply with the OpenAPI spec. -func (doc *T) Validate(ctx context.Context) error { +func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { + validationOpts := &validationOptions{} + for _, opt := range opts { + opt(validationOpts) + } + ctx = withValidationOptions(ctx, validationOpts) + if doc.OpenAPI == "" { return errors.New("value of openapi must be a non-empty string") } diff --git a/openapi3/schema.go b/openapi3/schema.go index 17f547e66..67afca6c6 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -36,9 +36,6 @@ 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 @@ -403,6 +400,7 @@ func (schema *Schema) WithMax(value float64) *Schema { schema.Max = &value return schema } + func (schema *Schema) WithExclusiveMin(value bool) *Schema { schema.ExclusiveMin = value return schema @@ -606,6 +604,8 @@ func (schema *Schema) Validate(ctx context.Context) error { } func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) { + validationOpts := getValidationOptions(ctx) + for _, existing := range stack { if existing == schema { return @@ -666,7 +666,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "float", "double": default: - if !SchemaFormatValidationDisabled { + if !validationOpts.schemaFormatValidationDisabled { return unsupportedFormat(format) } } @@ -676,7 +676,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "int32", "int64": default: - if !SchemaFormatValidationDisabled { + if !validationOpts.schemaFormatValidationDisabled { return unsupportedFormat(format) } } @@ -698,12 +698,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference": default: // Try to check for custom defined formats - if _, ok := SchemaStringFormats[format]; !ok && !SchemaFormatValidationDisabled { + if _, ok := SchemaStringFormats[format]; !ok && !validationOpts.schemaFormatValidationDisabled { return unsupportedFormat(format) } } } - if schema.Pattern != "" { + if schema.Pattern != "" && !validationOpts.schemaPatternValidationDisabled { if err = schema.compilePattern(); err != nil { return err } @@ -1063,7 +1063,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value formatMin = formatMinInt64 formatMax = formatMaxInt64 default: - if !SchemaFormatValidationDisabled { + if !settings.formatValidationDisabled { return unsupportedFormat(schema.Format) } } @@ -1237,7 +1237,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value } // "pattern" - if schema.Pattern != "" && schema.compiledPattern == nil { + if schema.Pattern != "" && schema.compiledPattern == nil && !settings.patternValidationDisabled { var err error if err = schema.compilePattern(); err != nil { if !settings.multiError { diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index cb4c142a4..3e86bc6ca 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -8,9 +8,11 @@ import ( type SchemaValidationOption func(*schemaValidationSettings) type schemaValidationSettings struct { - failfast bool - multiError bool - asreq, asrep bool // exclusive (XOR) fields + failfast bool + multiError bool + asreq, asrep bool // exclusive (XOR) fields + formatValidationDisabled bool + patternValidationDisabled bool onceSettingDefaults sync.Once defaultsSet func() @@ -28,10 +30,19 @@ func MultiErrors() SchemaValidationOption { func VisitAsRequest() SchemaValidationOption { return func(s *schemaValidationSettings) { s.asreq, s.asrep = true, false } } + func VisitAsResponse() SchemaValidationOption { return func(s *schemaValidationSettings) { s.asreq, s.asrep = false, true } } +func DisableFormatValidation() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.formatValidationDisabled = true } +} + +func DisablePatternValidation() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.patternValidationDisabled = true } +} + // DefaultsSet executes the given callback (once) IFF schema validation set default values. func DefaultsSet(f func()) SchemaValidationOption { return func(s *schemaValidationSettings) { s.defaultsSet = f } diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go new file mode 100644 index 000000000..20c9ad86d --- /dev/null +++ b/openapi3/validation_options.go @@ -0,0 +1,39 @@ +package openapi3 + +import "context" + +type ValidationOption func(options *validationOptions) + +type validationOptions struct { + schemaFormatValidationDisabled bool + schemaPatternValidationDisabled bool +} + +type contextKey string + +const ( + contextKeyValidationOptions contextKey = "validationOptions" +) + +func DisableSchemaFormatValidation() ValidationOption { + return func(options *validationOptions) { + options.schemaFormatValidationDisabled = true + } +} + +func DisableSchemaPatternValidation() ValidationOption { + return func(options *validationOptions) { + options.schemaPatternValidationDisabled = true + } +} + +func withValidationOptions(ctx context.Context, options *validationOptions) context.Context { + return context.WithValue(ctx, contextKeyValidationOptions, options) +} + +func getValidationOptions(ctx context.Context) *validationOptions { + if options, ok := ctx.Value(contextKeyValidationOptions).(*validationOptions); ok { + return options + } + return &validationOptions{} +} From bd874978e98df4818a2de0b7a3da8cfbe3dec6d0 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Tue, 6 Sep 2022 14:24:18 +0100 Subject: [PATCH 2/6] testing: add test to validate new behaviour --- openapi3/testdata/issue409.yml | 106 +++++++++++++++++++++++++++ openapi3/validation_issue409_test.go | 18 +++++ 2 files changed, 124 insertions(+) create mode 100644 openapi3/testdata/issue409.yml create mode 100644 openapi3/validation_issue409_test.go diff --git a/openapi3/testdata/issue409.yml b/openapi3/testdata/issue409.yml new file mode 100644 index 000000000..0aa4642dc --- /dev/null +++ b/openapi3/testdata/issue409.yml @@ -0,0 +1,106 @@ +openapi: 3.0.3 +info: + description: Contains Patterns that can't be compiled by the go regexp engine + title: Issue 409 + version: 0.0.1 +paths: + /v1/apis/{apiID}: + get: + description: Get a list of all Apis and there versions for a given workspace + operationId: getApisV1 + parameters: + - description: The ID of the API + in: path + name: apiID + required: true + schema: + type: string + pattern: ^[a-zA-Z0-9]{0,4096}$ + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Api" + type: array + description: A list of Apis for the given workspace + description: OK + default: + $ref: "#/components/responses/default" + tags: + - Apis +components: + responses: + default: + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Default error response + schemas: + Api: + description: + An Api is representation of a API (a collection of API Endpoints) + within the Speakeasy Platform. + properties: + api_id: + description: + The ID of this Api. This is a human-readable name (subject + to change). + type: string + created_at: + description: Creation timestamp. + format: date-time + readOnly: true + type: string + description: + description: A detailed description of the Api. + type: string + meta_data: + additionalProperties: + items: + type: string + type: array + description: + A set of values associated with a meta_data key. This field + is only set on get requests. + type: object + updated_at: + description: Last update timestamp. + format: date-time + readOnly: true + type: string + version_id: + description: The version ID of this Api. This is semantic version identifier. + type: string + workspace_id: + description: The workspace ID this Api belongs to. + readOnly: true + type: string + matched: + description: Determines if all the endpoints within the Api are found in the OpenAPI spec associated with the Api. + readOnly: true + type: boolean + required: + - workspace_id + - api_id + - version_id + - description + - created_at + - updated_at + type: object + Error: + description: The `Status` type defines a logical error model + properties: + message: + description: A developer-facing error message. + type: string + status_code: + description: The HTTP status code + format: int32 + type: integer + type: object + required: + - message + - status_code diff --git a/openapi3/validation_issue409_test.go b/openapi3/validation_issue409_test.go new file mode 100644 index 000000000..487563450 --- /dev/null +++ b/openapi3/validation_issue409_test.go @@ -0,0 +1,18 @@ +package openapi3_test + +import ( + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIssue409(t *testing.T) { + l := openapi3.NewLoader() + s, err := l.LoadFromFile("testdata/issue409.yml") + require.NoError(t, err) + + err = s.Validate(l.Context, openapi3.DisableSchemaPatternValidation()) + assert.NoError(t, err) +} From 1c405578170f833db7290a728b8e288798c2d166 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Tue, 6 Sep 2022 14:27:46 +0100 Subject: [PATCH 3/6] fix: attempt to fix import order --- openapi3/validation_issue409_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openapi3/validation_issue409_test.go b/openapi3/validation_issue409_test.go index 487563450..8e1cebc23 100644 --- a/openapi3/validation_issue409_test.go +++ b/openapi3/validation_issue409_test.go @@ -3,9 +3,10 @@ package openapi3_test import ( "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" ) func TestIssue409(t *testing.T) { From 32ed4b5bf7b32e084f0c122564d84ff8d95b6a43 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Wed, 7 Sep 2022 08:58:14 +0100 Subject: [PATCH 4/6] fix: address PR feedback --- README.md | 3 + openapi3/loader.go | 8 +- openapi3/loader_read_from_uri_func_test.go | 2 - openapi3/openapi3.go | 1 + openapi3/schema.go | 8 +- openapi3/schema_test.go | 7 +- openapi3/schema_validation_settings.go | 8 +- openapi3/testdata/issue409.yml | 85 ---------------------- openapi3/validation_issue409_test.go | 11 ++- openapi3/validation_options.go | 19 +++-- 10 files changed, 42 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index a829e5983..ae87783a2 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,9 @@ func arrayUniqueItemsChecker(items []interface{}) bool { ## Sub-v0 breaking API changes +### v0.101.0 +* `openapi3.SchemaFormatValidationDisabled` has been removed in favour of an option `openapi3.EnableSchemaFormatValidation()` passed to `openapi3.T.Validate`. The default behaviour is also now to not validate formats, as the OpenAPI spec mentions the `format` is an open value. + ### v0.84.0 * The prototype of `openapi3gen.NewSchemaRefForValue` changed: * It no longer returns a map but that is still accessible under the field `(*Generator).SchemaRefs`. diff --git a/openapi3/loader.go b/openapi3/loader.go index fad5a0d76..87c9f8684 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -51,9 +51,7 @@ type Loader struct { // NewLoader returns an empty Loader func NewLoader() *Loader { - return &Loader{ - Context: context.Background(), - } + return &Loader{} } func (loader *Loader) resetVisitedPathItemRefs() { @@ -168,6 +166,10 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR // ResolveRefsIn expands references if for instance spec was just unmarshalled func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { + if loader.Context == nil { + loader.Context = context.Background() + } + if loader.visitedPathItemRefs == nil { loader.resetVisitedPathItemRefs() } diff --git a/openapi3/loader_read_from_uri_func_test.go b/openapi3/loader_read_from_uri_func_test.go index 0901c5526..8fee2f4c2 100644 --- a/openapi3/loader_read_from_uri_func_test.go +++ b/openapi3/loader_read_from_uri_func_test.go @@ -1,7 +1,6 @@ package openapi3 import ( - "context" "fmt" "io/ioutil" "net/url" @@ -60,7 +59,6 @@ func TestResolveSchemaExternalRef(t *testing.T) { loader := &Loader{ IsExternalRefsAllowed: true, ReadFromURIFunc: multipleSourceLoader.LoadFromURI, - Context: context.Background(), } doc, err := loader.LoadFromURI(rootLocation) diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index af58cf89e..782fc1f77 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -54,6 +54,7 @@ func (doc *T) AddServer(server *Server) { } // Validate returns an error if T does not comply with the OpenAPI spec. +// Validations Options can be provided to modify the validation behavior. func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { validationOpts := &validationOptions{} for _, opt := range opts { diff --git a/openapi3/schema.go b/openapi3/schema.go index 67afca6c6..fbdcf95c4 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -666,7 +666,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "float", "double": default: - if !validationOpts.schemaFormatValidationDisabled { + if validationOpts.schemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -676,7 +676,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "int32", "int64": default: - if !validationOpts.schemaFormatValidationDisabled { + if validationOpts.schemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -698,7 +698,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference": default: // Try to check for custom defined formats - if _, ok := SchemaStringFormats[format]; !ok && !validationOpts.schemaFormatValidationDisabled { + if _, ok := SchemaStringFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -1063,7 +1063,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value formatMin = formatMinInt64 formatMax = formatMaxInt64 default: - if !settings.formatValidationDisabled { + if settings.formatValidationEnabled { return unsupportedFormat(schema.Format) } } diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 593fa17aa..8f98eb502 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1028,7 +1028,10 @@ func testType(t *testing.T, example schemaTypeExample) func(*testing.T) { } for _, typ := range example.AllInvalid { schema := baseSchema.WithFormat(typ) - err := schema.Validate(context.Background()) + ctx := withValidationOptions(context.Background(), &validationOptions{ + schemaFormatValidationEnabled: true, + }) + err := schema.Validate(ctx) require.Error(t, err) } } @@ -1308,6 +1311,6 @@ func TestValidationFailsOnInvalidPattern(t *testing.T) { Type: "string", } - var err = schema.Validate(context.Background()) + err := schema.Validate(context.Background()) require.Error(t, err) } diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index 3e86bc6ca..854ae8480 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -11,7 +11,7 @@ type schemaValidationSettings struct { failfast bool multiError bool asreq, asrep bool // exclusive (XOR) fields - formatValidationDisabled bool + formatValidationEnabled bool patternValidationDisabled bool onceSettingDefaults sync.Once @@ -35,10 +35,12 @@ func VisitAsResponse() SchemaValidationOption { return func(s *schemaValidationSettings) { s.asreq, s.asrep = false, true } } -func DisableFormatValidation() SchemaValidationOption { - return func(s *schemaValidationSettings) { s.formatValidationDisabled = true } +// EnableFormatValidation setting makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification. +func EnableFormatValidation() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.formatValidationEnabled = true } } +// DisablePatternValidation setting makes Validate not return an error when validating patterns that are not supported by the Go regexp engine. func DisablePatternValidation() SchemaValidationOption { return func(s *schemaValidationSettings) { s.patternValidationDisabled = true } } diff --git a/openapi3/testdata/issue409.yml b/openapi3/testdata/issue409.yml index 0aa4642dc..88394904e 100644 --- a/openapi3/testdata/issue409.yml +++ b/openapi3/testdata/issue409.yml @@ -18,89 +18,4 @@ paths: pattern: ^[a-zA-Z0-9]{0,4096}$ responses: "200": - content: - application/json: - schema: - items: - $ref: "#/components/schemas/Api" - type: array - description: A list of Apis for the given workspace description: OK - default: - $ref: "#/components/responses/default" - tags: - - Apis -components: - responses: - default: - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - description: Default error response - schemas: - Api: - description: - An Api is representation of a API (a collection of API Endpoints) - within the Speakeasy Platform. - properties: - api_id: - description: - The ID of this Api. This is a human-readable name (subject - to change). - type: string - created_at: - description: Creation timestamp. - format: date-time - readOnly: true - type: string - description: - description: A detailed description of the Api. - type: string - meta_data: - additionalProperties: - items: - type: string - type: array - description: - A set of values associated with a meta_data key. This field - is only set on get requests. - type: object - updated_at: - description: Last update timestamp. - format: date-time - readOnly: true - type: string - version_id: - description: The version ID of this Api. This is semantic version identifier. - type: string - workspace_id: - description: The workspace ID this Api belongs to. - readOnly: true - type: string - matched: - description: Determines if all the endpoints within the Api are found in the OpenAPI spec associated with the Api. - readOnly: true - type: boolean - required: - - workspace_id - - api_id - - version_id - - description - - created_at - - updated_at - type: object - Error: - description: The `Status` type defines a logical error model - properties: - message: - description: A developer-facing error message. - type: string - status_code: - description: The HTTP status code - format: int32 - type: integer - type: object - required: - - message - - status_code diff --git a/openapi3/validation_issue409_test.go b/openapi3/validation_issue409_test.go index 8e1cebc23..cec714cec 100644 --- a/openapi3/validation_issue409_test.go +++ b/openapi3/validation_issue409_test.go @@ -9,7 +9,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" ) -func TestIssue409(t *testing.T) { +func TestIssue409PatternIgnored(t *testing.T) { l := openapi3.NewLoader() s, err := l.LoadFromFile("testdata/issue409.yml") require.NoError(t, err) @@ -17,3 +17,12 @@ func TestIssue409(t *testing.T) { err = s.Validate(l.Context, openapi3.DisableSchemaPatternValidation()) assert.NoError(t, err) } + +func TestIssue409PatternNotIgnored(t *testing.T) { + l := openapi3.NewLoader() + s, err := l.LoadFromFile("testdata/issue409.yml") + require.NoError(t, err) + + err = s.Validate(l.Context) + assert.Error(t, err) +} diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index 20c9ad86d..be1f1b62f 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -2,25 +2,24 @@ package openapi3 import "context" +// ValidationOption allows the modification of how the OpenAPI document is validated. type ValidationOption func(options *validationOptions) type validationOptions struct { - schemaFormatValidationDisabled bool + schemaFormatValidationEnabled bool schemaPatternValidationDisabled bool } -type contextKey string +type validationOptionsKey struct{} -const ( - contextKeyValidationOptions contextKey = "validationOptions" -) - -func DisableSchemaFormatValidation() ValidationOption { +// EnableSchemaFormatValidation makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification. +func EnableSchemaFormatValidation() ValidationOption { return func(options *validationOptions) { - options.schemaFormatValidationDisabled = true + options.schemaFormatValidationEnabled = true } } +// DisableSchemaPatternValidation makes Validate not return an error when validating patterns that are not supported by the Go regexp engine. func DisableSchemaPatternValidation() ValidationOption { return func(options *validationOptions) { options.schemaPatternValidationDisabled = true @@ -28,11 +27,11 @@ func DisableSchemaPatternValidation() ValidationOption { } func withValidationOptions(ctx context.Context, options *validationOptions) context.Context { - return context.WithValue(ctx, contextKeyValidationOptions, options) + return context.WithValue(ctx, validationOptionsKey{}, options) } func getValidationOptions(ctx context.Context) *validationOptions { - if options, ok := ctx.Value(contextKeyValidationOptions).(*validationOptions); ok { + if options, ok := ctx.Value(validationOptionsKey{}).(*validationOptions); ok { return options } return &validationOptions{} From b74648b2481aacf469b1c274b9c10097fcd0d702 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Thu, 8 Sep 2022 09:58:03 +0100 Subject: [PATCH 5/6] fix: add additional tests and make WithValidationOptions public --- openapi3/openapi3.go | 2 +- openapi3/schema_test.go | 2 +- openapi3/validation_issue409_test.go | 22 ++++++++++++++++++++++ openapi3/validation_options.go | 3 ++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 782fc1f77..9c0133114 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -60,7 +60,7 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { for _, opt := range opts { opt(validationOpts) } - ctx = withValidationOptions(ctx, validationOpts) + ctx = WithValidationOptions(ctx, validationOpts) if doc.OpenAPI == "" { return errors.New("value of openapi must be a non-empty string") diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 8f98eb502..2f91dc56d 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1028,7 +1028,7 @@ func testType(t *testing.T, example schemaTypeExample) func(*testing.T) { } for _, typ := range example.AllInvalid { schema := baseSchema.WithFormat(typ) - ctx := withValidationOptions(context.Background(), &validationOptions{ + ctx := WithValidationOptions(context.Background(), &validationOptions{ schemaFormatValidationEnabled: true, }) err := schema.Validate(ctx) diff --git a/openapi3/validation_issue409_test.go b/openapi3/validation_issue409_test.go index cec714cec..561594fca 100644 --- a/openapi3/validation_issue409_test.go +++ b/openapi3/validation_issue409_test.go @@ -26,3 +26,25 @@ func TestIssue409PatternNotIgnored(t *testing.T) { err = s.Validate(l.Context) assert.Error(t, err) } + +func TestIssue409HygienicUseOfCtx(t *testing.T) { + l := openapi3.NewLoader() + doc, err := l.LoadFromFile("testdata/issue409.yml") + require.NoError(t, err) + + err = doc.Validate(l.Context, openapi3.DisableSchemaPatternValidation()) + assert.NoError(t, err) + err = doc.Validate(l.Context) + assert.Error(t, err) + + // and the other way + + l = openapi3.NewLoader() + doc, err = l.LoadFromFile("testdata/issue409.yml") + require.NoError(t, err) + + err = doc.Validate(l.Context) + assert.Error(t, err) + err = doc.Validate(l.Context, openapi3.DisableSchemaPatternValidation()) + assert.NoError(t, err) +} diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index be1f1b62f..754eb151e 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -26,7 +26,8 @@ func DisableSchemaPatternValidation() ValidationOption { } } -func withValidationOptions(ctx context.Context, options *validationOptions) context.Context { +// WithValidationOptions allows adding validation options to a context object that can be used when validationg any OpenAPI type. +func WithValidationOptions(ctx context.Context, options *validationOptions) context.Context { return context.WithValue(ctx, validationOptionsKey{}, options) } From db1120ae8633bce4b8dc49e06dc1fd5ac6de3d4f Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Thu, 8 Sep 2022 13:00:34 +0100 Subject: [PATCH 6/6] fix: make validation options public --- openapi3/openapi3.go | 2 +- openapi3/schema.go | 8 ++++---- openapi3/schema_test.go | 4 ++-- openapi3/validation_options.go | 25 +++++++++++++------------ 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 9c0133114..20549c2b7 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -56,7 +56,7 @@ func (doc *T) AddServer(server *Server) { // Validate returns an error if T does not comply with the OpenAPI spec. // Validations Options can be provided to modify the validation behavior. func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { - validationOpts := &validationOptions{} + validationOpts := &ValidationOptions{} for _, opt := range opts { opt(validationOpts) } diff --git a/openapi3/schema.go b/openapi3/schema.go index fbdcf95c4..57f63fbd8 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -666,7 +666,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "float", "double": default: - if validationOpts.schemaFormatValidationEnabled { + if validationOpts.SchemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -676,7 +676,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "int32", "int64": default: - if validationOpts.schemaFormatValidationEnabled { + if validationOpts.SchemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -698,12 +698,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference": default: // Try to check for custom defined formats - if _, ok := SchemaStringFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled { + if _, ok := SchemaStringFormats[format]; !ok && validationOpts.SchemaFormatValidationEnabled { return unsupportedFormat(format) } } } - if schema.Pattern != "" && !validationOpts.schemaPatternValidationDisabled { + if schema.Pattern != "" && !validationOpts.SchemaPatternValidationDisabled { if err = schema.compilePattern(); err != nil { return err } diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 2f91dc56d..4c14dcb10 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1028,8 +1028,8 @@ func testType(t *testing.T, example schemaTypeExample) func(*testing.T) { } for _, typ := range example.AllInvalid { schema := baseSchema.WithFormat(typ) - ctx := WithValidationOptions(context.Background(), &validationOptions{ - schemaFormatValidationEnabled: true, + ctx := WithValidationOptions(context.Background(), &ValidationOptions{ + SchemaFormatValidationEnabled: true, }) err := schema.Validate(ctx) require.Error(t, err) diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index 754eb151e..f6038ed10 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -3,37 +3,38 @@ package openapi3 import "context" // ValidationOption allows the modification of how the OpenAPI document is validated. -type ValidationOption func(options *validationOptions) +type ValidationOption func(options *ValidationOptions) -type validationOptions struct { - schemaFormatValidationEnabled bool - schemaPatternValidationDisabled bool +// ValidationOptions provide configuration for validating OpenAPI documents. +type ValidationOptions struct { + SchemaFormatValidationEnabled bool + SchemaPatternValidationDisabled bool } type validationOptionsKey struct{} // EnableSchemaFormatValidation makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification. func EnableSchemaFormatValidation() ValidationOption { - return func(options *validationOptions) { - options.schemaFormatValidationEnabled = true + return func(options *ValidationOptions) { + options.SchemaFormatValidationEnabled = true } } // DisableSchemaPatternValidation makes Validate not return an error when validating patterns that are not supported by the Go regexp engine. func DisableSchemaPatternValidation() ValidationOption { - return func(options *validationOptions) { - options.schemaPatternValidationDisabled = true + return func(options *ValidationOptions) { + options.SchemaPatternValidationDisabled = true } } // WithValidationOptions allows adding validation options to a context object that can be used when validationg any OpenAPI type. -func WithValidationOptions(ctx context.Context, options *validationOptions) context.Context { +func WithValidationOptions(ctx context.Context, options *ValidationOptions) context.Context { return context.WithValue(ctx, validationOptionsKey{}, options) } -func getValidationOptions(ctx context.Context) *validationOptions { - if options, ok := ctx.Value(validationOptionsKey{}).(*validationOptions); ok { +func getValidationOptions(ctx context.Context) *ValidationOptions { + if options, ok := ctx.Value(validationOptionsKey{}).(*ValidationOptions); ok { return options } - return &validationOptions{} + return &ValidationOptions{} }