From ea021d43fc2495fa32f26b7c7a82ed7deb1e8d20 Mon Sep 17 00:00:00 2001 From: Jem Day Date: Fri, 3 Jan 2025 14:39:06 -0800 Subject: [PATCH] Enhance validation configuration options to include assertions for 'format' and 'content' --- config/config.go | 23 +++++- helpers/schema_compiler.go | 55 +++++++++++++++ parameters/path_parameters.go | 2 + parameters/query_parameters.go | 1 + parameters/query_parameters_test.go | 105 ++++++++++++++++++++++++++++ parameters/validate_parameter.go | 14 +--- 6 files changed, 187 insertions(+), 13 deletions(-) create mode 100644 helpers/schema_compiler.go diff --git a/config/config.go b/config/config.go index 1b10724..f91e5a7 100644 --- a/config/config.go +++ b/config/config.go @@ -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 @@ -15,7 +17,10 @@ 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 { @@ -32,3 +37,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 + } +} diff --git a/helpers/schema_compiler.go b/helpers/schema_compiler.go new file mode 100644 index 0000000..188a743 --- /dev/null +++ b/helpers/schema_compiler.go @@ -0,0 +1,55 @@ +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) { + // 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 { + // Establish a compiler with the desired configuration + compiler := NewCompilerWithOptions(o) + compiler.UseLoader(NewCompilerLoader()) + + // Decode the JSON Schema into a JSON blob. + decodedSchema, _ := jsonschema.UnmarshalJSON(bytes.NewReader(jsonSchema)) + _ = compiler.AddResource(fmt.Sprintf("%s.json", name), decodedSchema) + + // Try to compile it. + jsch, _ := compiler.Compile(fmt.Sprintf("%s.json", name)) + + // Done. + return jsch +} diff --git a/parameters/path_parameters.go b/parameters/path_parameters.go index 41ae277..f424ca5 100644 --- a/parameters/path_parameters.go +++ b/parameters/path_parameters.go @@ -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: @@ -161,6 +162,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p p.Name, helpers.ParameterValidation, helpers.ParameterValidationPath, + v.options, )...) case helpers.Boolean: diff --git a/parameters/query_parameters.go b/parameters/query_parameters.go index 940e068..f04d56f 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -252,5 +252,6 @@ func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string, parameter.Name, helpers.ParameterValidation, helpers.ParameterValidationQuery, + v.options, ) } diff --git a/parameters/query_parameters_test.go b/parameters/query_parameters_test.go index 3a83870..31d29fe 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/paths" ) @@ -669,6 +670,110 @@ paths: assert.Len(t, errors, 0) } +func TestNewValidator_QueryParamValidDateFormat(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /a/fishy/on/a/dishy: + get: + parameters: + - name: fishy + in: query + required: true + schema: + type: string + format: date` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + + v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) + + request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=2024-12-25", nil) + + valid, errors := v.ValidateQueryParams(request) + assert.True(t, valid) + assert.Len(t, errors, 0) +} + +func TestNewValidator_QueryParamInvalidDateFormat(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /a/fishy/on/a/dishy: + get: + parameters: + - name: fishy + in: query + required: true + schema: + type: string + format: date` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + + v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) + + request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=12/25/2024", nil) + + valid, errors := v.ValidateQueryParams(request) + assert.False(t, valid) + assert.Len(t, errors, 1) +} + +func TestNewValidator_QueryParamValidDateTimeFormat(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /a/fishy/on/a/dishy: + get: + parameters: + - name: fishy + in: query + required: true + schema: + type: string + format: date-time` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + + v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) + + request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=2024-12-25T13:42:42Z", nil) + + valid, errors := v.ValidateQueryParams(request) + assert.True(t, valid) + assert.Len(t, errors, 0) +} + +func TestNewValidator_QueryParamInvalidDateTimeFormat(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /a/fishy/on/a/dishy: + get: + parameters: + - name: fishy + in: query + required: true + schema: + type: string + format: date-time` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + + v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) + + request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=2024-12-25", nil) + + valid, errors := v.ValidateQueryParams(request) + assert.False(t, valid) + assert.Len(t, errors, 1) +} + func TestNewValidator_QueryParamValidTypeArrayString(t *testing.T) { spec := `openapi: 3.1.0 paths: diff --git a/parameters/validate_parameter.go b/parameters/validate_parameter.go index 5a5eb8b..ef5ceef 100644 --- a/parameters/validate_parameter.go +++ b/parameters/validate_parameter.go @@ -18,6 +18,7 @@ import ( stdError "errors" + "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" ) @@ -30,8 +31,9 @@ func ValidateSingleParameterSchema( name string, validationType string, subValType string, + o *config.ValidationOptions, ) (validationErrors []*errors.ValidationError) { - jsch := compileSchema(name, buildJsonRender(schema)) + jsch := helpers.NewCompiledSchema(name, buildJsonRender(schema), o) scErrs := jsch.Validate(rawObject) var werras *jsonschema.ValidationError @@ -41,16 +43,6 @@ func ValidateSingleParameterSchema( return validationErrors } -// compileSchema create a new json schema compiler and add the schema to it. -func compileSchema(name string, jsonSchema []byte) *jsonschema.Schema { - compiler := jsonschema.NewCompiler() - compiler.UseLoader(helpers.NewCompilerLoader()) - decodedSchema, _ := jsonschema.UnmarshalJSON(strings.NewReader(string(jsonSchema))) // decode the schema into a json blob - _ = compiler.AddResource(fmt.Sprintf("%s.json", name), decodedSchema) - jsch, _ := compiler.Compile(fmt.Sprintf("%s.json", name)) - return jsch -} - // buildJsonRender build a JSON render of the schema. func buildJsonRender(schema *base.Schema) []byte { renderedSchema, _ := schema.Render()