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 6 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
28 changes: 25 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,17 @@ 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)
// Sanity
if opt != nil {
opt(o)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this

Suggested change
// Sanity
if opt != nil {
opt(o)
}
if opt == nil {
// Sanity
continue
}
opt(o)

The sanity is in the nil check. The comment is misleading otherwise

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opted (no pun intended) to remove the comment but leave the logic as-is ... seemed more logical to me.

}

// Done
Expand All @@ -32,3 +40,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) {
// Sanity
if o == nil {
return
}
JemDay marked this conversation as resolved.
Show resolved Hide resolved

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

// 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.NotNil(t, err, "Expected an error to be thrown")
JemDay marked this conversation as resolved.
Show resolved Hide resolved
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.NotNil(t, err, "Expected an error to be thrown")
JemDay marked this conversation as resolved.
Show resolved Hide resolved
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.NotNil(t, err, "Expected an error to be thrown")
JemDay marked this conversation as resolved.
Show resolved Hide resolved
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