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 3 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
6 changes: 3 additions & 3 deletions openapi3/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ type Loader struct {

// NewLoader returns an empty Loader
func NewLoader() *Loader {
return &Loader{}
return &Loader{
Context: context.Background(),
fenollp marked this conversation as resolved.
Show resolved Hide resolved
}
}

func (loader *Loader) resetVisitedPathItemRefs() {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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")
}
Expand Down
2 changes: 2 additions & 0 deletions openapi3/loader_read_from_uri_func_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package openapi3

import (
"context"
"fmt"
"io/ioutil"
"net/url"
Expand Down Expand Up @@ -59,6 +60,7 @@ func TestResolveSchemaExternalRef(t *testing.T) {
loader := &Loader{
IsExternalRefsAllowed: true,
ReadFromURIFunc: multipleSourceLoader.LoadFromURI,
Context: context.Background(),
fenollp marked this conversation as resolved.
Show resolved Hide resolved
}

doc, err := loader.LoadFromURI(rootLocation)
Expand Down
8 changes: 7 additions & 1 deletion openapi3/openapi3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
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.schemaFormatValidationDisabled {
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.schemaFormatValidationDisabled {
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.schemaFormatValidationDisabled {
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.formatValidationDisabled {
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
17 changes: 14 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
formatValidationDisabled bool
patternValidationDisabled bool

onceSettingDefaults sync.Once
defaultsSet func()
Expand All @@ -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 {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
return func(s *schemaValidationSettings) { s.formatValidationDisabled = true }
}

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
106 changes: 106 additions & 0 deletions openapi3/testdata/issue409.yml
Original file line number Diff line number Diff line change
@@ -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
fenollp marked this conversation as resolved.
Show resolved Hide resolved
19 changes: 19 additions & 0 deletions openapi3/validation_issue409_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package openapi3_test

import (
"testing"

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

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

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())
fenollp marked this conversation as resolved.
Show resolved Hide resolved
assert.NoError(t, err)
}
39 changes: 39 additions & 0 deletions openapi3/validation_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package openapi3

import "context"

type ValidationOption func(options *validationOptions)
fenollp marked this conversation as resolved.
Show resolved Hide resolved

type validationOptions struct {
schemaFormatValidationDisabled bool
schemaPatternValidationDisabled bool
}

type contextKey string

const (
contextKeyValidationOptions contextKey = "validationOptions"
fenollp marked this conversation as resolved.
Show resolved Hide resolved
)

func DisableSchemaFormatValidation() ValidationOption {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
return func(options *validationOptions) {
options.schemaFormatValidationDisabled = true
}
}

func DisableSchemaPatternValidation() ValidationOption {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
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{}
}