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

Enable 'Format' and 'Content' assertions during schema validation #116

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
27 changes: 24 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import "github.com/santhosh-tekuri/jsonschema/v6"
//
// Generally fluent With... style functions are used to establish the desired behavior.
type ValidationOptions struct {
RegexEngine jsonschema.RegexpEngine
RegexEngine jsonschema.RegexpEngine
FormatAssertions bool
ContentAssertions bool
}

// Option Enables an 'Options pattern' approach
Expand All @@ -15,11 +17,16 @@ type Option func(*ValidationOptions)
// NewValidationOptions creates a new ValidationOptions instance with default values.
func NewValidationOptions(opts ...Option) *ValidationOptions {
// Create the set of default values
o := &ValidationOptions{}
o := &ValidationOptions{
FormatAssertions: false,
ContentAssertions: false,
}

// Apply any supplied overrides
for _, opt := range opts {
opt(o)
if opt != nil {
opt(o)
}
}

// Done
Expand All @@ -32,3 +39,17 @@ func WithRegexEngine(engine jsonschema.RegexpEngine) Option {
o.RegexEngine = engine
}
}

// WithFormatAssertions enables checks for 'format' assertions (such as date, date-time, uuid, etc)
func WithFormatAssertions() Option {
return func(o *ValidationOptions) {
o.FormatAssertions = true
}
}

// WithContentAssertions enables checks for contentType, contentEncoding, etc
func WithContentAssertions() Option {
return func(o *ValidationOptions) {
o.ContentAssertions = true
}
}
74 changes: 74 additions & 0 deletions helpers/schema_compiler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package helpers

import (
"bytes"
"fmt"

"github.com/santhosh-tekuri/jsonschema/v6"

"github.com/pb33f/libopenapi-validator/config"
)

// ConfigureCompiler configures a JSON Schema compiler with the desired behavior.
func ConfigureCompiler(c *jsonschema.Compiler, o *config.ValidationOptions) {
if o == nil {
// Sanity
return
}

// nil is the default so this is OK.
c.UseRegexpEngine(o.RegexEngine)

// Enable Format assertions if required.
if o.FormatAssertions {
c.AssertFormat()
}

// Content Assertions
if o.ContentAssertions {
c.AssertContent()
}
}

// NewCompilerWithOptions mints a new JSON schema compiler with custom configuration.
func NewCompilerWithOptions(o *config.ValidationOptions) *jsonschema.Compiler {
// Build it
c := jsonschema.NewCompiler()

// Configure it
ConfigureCompiler(c, o)

// Return it
return c
}

// NewCompiledSchema establishes a programmatic representation of a JSON Schema document that is used for validation.
func NewCompiledSchema(name string, jsonSchema []byte, o *config.ValidationOptions) (*jsonschema.Schema, error) {
// Fake-Up a resource name for the schema
resourceName := fmt.Sprintf("%s.json", name)

// Establish a compiler with the desired configuration
compiler := NewCompilerWithOptions(o)
compiler.UseLoader(NewCompilerLoader())

// Decode the JSON Schema into a JSON blob.
decodedSchema, err := jsonschema.UnmarshalJSON(bytes.NewReader(jsonSchema))
if err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON schema: %w", err)
}

// Give our schema to the compiler.
err = compiler.AddResource(resourceName, decodedSchema)
if err != nil {
return nil, fmt.Errorf("failed to add resource to schema compiler: %w", err)
}

Check warning on line 64 in helpers/schema_compiler.go

View check run for this annotation

Codecov / codecov/patch

helpers/schema_compiler.go#L63-L64

Added lines #L63 - L64 were not covered by tests

// Try to compile it.
jsch, err := compiler.Compile(resourceName)
if err != nil {
return nil, fmt.Errorf("failed to compile JSON schema: %w", err)
}

// Done.
return jsch, nil
}
113 changes: 113 additions & 0 deletions helpers/schema_compiler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package helpers

import (
"testing"

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

"github.com/pb33f/libopenapi-validator/config"
)

// A few simple JSON Schemas
const stringSchema = `{
"type": "string",
"format": "date",
"minLength": 10
}`

const objectSchema = `{
"type": "object",
"title" : "Fish",
"properties" : {
"name" : {
"type": "string",
"description": "The given name of the fish"
},
"species" : {
"type" : "string",
"enum" : [ "OTHER", "GUPPY", "PIKE", "BASS" ]
}
}
}`

func Test_SchemaWithNilOptions(t *testing.T) {
jsch, err := NewCompiledSchema("test", []byte(stringSchema), nil)

require.NoError(t, err, "Failed to compile Schema")
require.NotNil(t, jsch, "Did not return a compiled schema")
}

func Test_SchemaWithDefaultOptions(t *testing.T) {
valOptions := config.NewValidationOptions()
jsch, err := NewCompiledSchema("test", []byte(stringSchema), valOptions)

require.NoError(t, err, "Failed to compile Schema")
require.NotNil(t, jsch, "Did not return a compiled schema")
}

func Test_SchemaWithOptions(t *testing.T) {
valOptions := config.NewValidationOptions(config.WithFormatAssertions(), config.WithContentAssertions())

jsch, err := NewCompiledSchema("test", []byte(stringSchema), valOptions)

require.NoError(t, err, "Failed to compile Schema")
require.NotNil(t, jsch, "Did not return a compiled schema")
}

func Test_ObjectSchema(t *testing.T) {
valOptions := config.NewValidationOptions()
jsch, err := NewCompiledSchema("test", []byte(objectSchema), valOptions)

require.NoError(t, err, "Failed to compile Schema")
require.NotNil(t, jsch, "Did not return a compiled schema")
}

func Test_ValidJSONSchemaWithInvalidContent(t *testing.T) {
// An example of a dubious JSON Schema
const badSchema = `{
"type": "you-dont-know-me",
"format": "date",
"minLength": 10
}`

jsch, err := NewCompiledSchema("test", []byte(badSchema), nil)

assert.Error(t, err, "Expected an error to be thrown")
assert.Nil(t, jsch, "invalid schema compiled!")
}

func Test_MalformedSONSchema(t *testing.T) {
// An example of a JSON schema with malformed JSON
const badSchema = `{
"type": "you-dont-know-me",
"format": "date"
"minLength": 10
}`

jsch, err := NewCompiledSchema("test", []byte(badSchema), nil)

assert.Error(t, err, "Expected an error to be thrown")
assert.Nil(t, jsch, "invalid schema compiled!")
}

func Test_ValidJSONSchemaWithIncompleteContent(t *testing.T) {
// An example of a dJSON schema with references to non-existent stuff
const incompleteSchema = `{
"type": "object",
"title" : "unresolvable",
"properties" : {
"name" : {
"type": "string",
},
"species" : {
"$ref": "#/$defs/speciesEnum"
}
}
}`

jsch, err := NewCompiledSchema("test", []byte(incompleteSchema), nil)

assert.Error(t, err, "Expected an error to be thrown")
assert.Nil(t, jsch, "invalid schema compiled!")
}
3 changes: 2 additions & 1 deletion parameters/cookie_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request,
"The cookie parameter",
p.Name,
helpers.ParameterValidation,
helpers.ParameterValidationQuery)...)
helpers.ParameterValidationQuery,
v.options)...)
}
}
case helpers.Array:
Expand Down
2 changes: 1 addition & 1 deletion parameters/header_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request,
"The header parameter",
p.Name,
helpers.ParameterValidation,
helpers.ParameterValidationQuery)...)
helpers.ParameterValidationQuery, v.options)...)
}

case helpers.Array:
Expand Down
4 changes: 3 additions & 1 deletion parameters/path_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p
p.Name,
helpers.ParameterValidation,
helpers.ParameterValidationPath,
v.options,
)...)

case helpers.Integer, helpers.Number:
Expand All @@ -161,6 +162,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p
p.Name,
helpers.ParameterValidation,
helpers.ParameterValidationPath,
v.options,
)...)

case helpers.Boolean:
Expand Down Expand Up @@ -221,7 +223,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p
"The path parameter",
p.Name,
helpers.ParameterValidation,
helpers.ParameterValidationPath)...)
helpers.ParameterValidationPath, v.options)...)
}

case helpers.Array:
Expand Down
7 changes: 4 additions & 3 deletions parameters/query_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ doneLooking:
"The query parameter",
params[p].Name,
helpers.ParameterValidation,
helpers.ParameterValidationQuery)...)
helpers.ParameterValidationQuery, v.options)...)
if len(validationErrors) > numErrors {
// we've already added an error for this, so we can skip the rest of the values
break skipValues
Expand All @@ -185,7 +185,7 @@ doneLooking:
// only check if items is a schema, not a boolean
if sch.Items != nil && sch.Items.IsA() {
validationErrors = append(validationErrors,
ValidateQueryArray(sch, params[p], ef, contentWrapped)...)
ValidateQueryArray(sch, params[p], ef, contentWrapped, v.options)...)
}
}
}
Expand All @@ -209,7 +209,7 @@ doneLooking:
"The query parameter (which is an array)",
params[p].Name,
helpers.ParameterValidation,
helpers.ParameterValidationQuery)...)
helpers.ParameterValidationQuery, v.options)...)
break doneLooking
}
}
Expand Down Expand Up @@ -252,5 +252,6 @@ func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string,
parameter.Name,
helpers.ParameterValidation,
helpers.ParameterValidationQuery,
v.options,
)
}
Loading
Loading