Skip to content

Commit

Permalink
feat: support validation options specifically for disabling pattern v…
Browse files Browse the repository at this point in the history
…alidation (#590)
  • Loading branch information
TristanSpeakEasy authored Sep 14, 2022
1 parent 5a61040 commit de022f1
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 17 deletions.
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

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 {
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())
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)
}

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

0 comments on commit de022f1

Please sign in to comment.