Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support validation options specifically for disabling pattern validation #590

Merged
merged 6 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
6 changes: 4 additions & 2 deletions openapi3/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,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()
}
Expand Down Expand Up @@ -406,7 +410,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) {
Expand Down Expand Up @@ -837,7 +840,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")
}
Expand Down
9 changes: 8 additions & 1 deletion openapi3/openapi3.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,14 @@ 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 {
// 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 {
opt(validationOpts)
}
ctx = WithValidationOptions(ctx, validationOpts)

if doc.OpenAPI == "" {
return errors.New("value of openapi must be a non-empty string")
}
Expand Down
18 changes: 9 additions & 9 deletions openapi3/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ var (
// SchemaErrorDetailsDisabled disables printing of details about schema errors.
SchemaErrorDetailsDisabled = false

//SchemaFormatValidationDisabled disables validation of schema type formats.
SchemaFormatValidationDisabled = false
fenollp marked this conversation as resolved.
Show resolved Hide resolved

errSchema = errors.New("input does not match the schema")

// ErrOneOfConflict is the SchemaError Origin when data matches more than one oneOf schema
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.SchemaFormatValidationEnabled {
return unsupportedFormat(format)
}
}
Expand All @@ -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.SchemaFormatValidationEnabled {
return unsupportedFormat(format)
}
}
Expand All @@ -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.SchemaFormatValidationEnabled {
return unsupportedFormat(format)
}
}
}
if schema.Pattern != "" {
if schema.Pattern != "" && !validationOpts.SchemaPatternValidationDisabled {
if err = schema.compilePattern(); err != nil {
return err
}
Expand Down Expand Up @@ -1063,7 +1063,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
formatMin = formatMinInt64
formatMax = formatMaxInt64
default:
if !SchemaFormatValidationDisabled {
if settings.formatValidationEnabled {
return unsupportedFormat(schema.Format)
}
}
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions openapi3/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
19 changes: 16 additions & 3 deletions openapi3/schema_validation_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
formatValidationEnabled bool
patternValidationDisabled bool

onceSettingDefaults sync.Once
defaultsSet func()
Expand All @@ -28,10 +30,21 @@ 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 }
}

// 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 {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
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 }
Expand Down
21 changes: 21 additions & 0 deletions openapi3/testdata/issue409.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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":
description: OK
50 changes: 50 additions & 0 deletions openapi3/validation_issue409_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package openapi3_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/getkin/kin-openapi/openapi3"
)

func TestIssue409PatternIgnored(t *testing.T) {
l := openapi3.NewLoader()
s, err := l.LoadFromFile("testdata/issue409.yml")
require.NoError(t, err)

err = s.Validate(l.Context, openapi3.DisableSchemaPatternValidation())
fenollp marked this conversation as resolved.
Show resolved Hide resolved
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)
}
fenollp marked this conversation as resolved.
Show resolved Hide resolved

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)
}
40 changes: 40 additions & 0 deletions openapi3/validation_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package openapi3

import "context"

// ValidationOption allows the modification of how the OpenAPI document is validated.
type ValidationOption func(options *ValidationOptions)

// 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
}
}

// DisableSchemaPatternValidation makes Validate not return an error when validating patterns that are not supported by the Go regexp engine.
func DisableSchemaPatternValidation() ValidationOption {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
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 {
return context.WithValue(ctx, validationOptionsKey{}, options)
}

func getValidationOptions(ctx context.Context) *ValidationOptions {
if options, ok := ctx.Value(validationOptionsKey{}).(*ValidationOptions); ok {
return options
}
return &ValidationOptions{}
}