diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..1b10724 --- /dev/null +++ b/config/config.go @@ -0,0 +1,34 @@ +package config + +import "github.com/santhosh-tekuri/jsonschema/v6" + +// ValidationOptions A container for validation configuration. +// +// Generally fluent With... style functions are used to establish the desired behavior. +type ValidationOptions struct { + RegexEngine jsonschema.RegexpEngine +} + +// Option Enables an 'Options pattern' approach +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{} + + // Apply any supplied overrides + for _, opt := range opts { + opt(o) + } + + // Done + return o +} + +// WithRegexEngine Assigns a custom regular-expression engine to be used during validation. +func WithRegexEngine(engine jsonschema.RegexpEngine) Option { + return func(o *ValidationOptions) { + o.RegexEngine = engine + } +} diff --git a/parameters/parameters.go b/parameters/parameters.go index 6ea572b..2fa168a 100644 --- a/parameters/parameters.go +++ b/parameters/parameters.go @@ -8,6 +8,7 @@ import ( v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" ) @@ -66,10 +67,13 @@ type ParameterValidator interface { } // NewParameterValidator will create a new ParameterValidator from an OpenAPI 3+ document -func NewParameterValidator(document *v3.Document) ParameterValidator { - return ¶mValidator{document: document} +func NewParameterValidator(document *v3.Document, opts ...config.Option) ParameterValidator { + options := config.NewValidationOptions(opts...) + + return ¶mValidator{options: options, document: document} } type paramValidator struct { + options *config.ValidationOptions document *v3.Document } diff --git a/requests/request_body.go b/requests/request_body.go index 3c534a6..2fc0835 100644 --- a/requests/request_body.go +++ b/requests/request_body.go @@ -10,6 +10,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" ) @@ -30,8 +31,10 @@ type RequestBodyValidator interface { } // NewRequestBodyValidator will create a new RequestBodyValidator from an OpenAPI 3+ document -func NewRequestBodyValidator(document *v3.Document) RequestBodyValidator { - return &requestBodyValidator{document: document, schemaCache: &sync.Map{}} +func NewRequestBodyValidator(document *v3.Document, opts ...config.Option) RequestBodyValidator { + options := config.NewValidationOptions(opts...) + + return &requestBodyValidator{options: options, document: document, schemaCache: &sync.Map{}} } type schemaCache struct { @@ -41,6 +44,7 @@ type schemaCache struct { } type requestBodyValidator struct { + options *config.ValidationOptions document *v3.Document schemaCache *sync.Map } diff --git a/requests/validate_body.go b/requests/validate_body.go index f36abad..f40a55f 100644 --- a/requests/validate_body.go +++ b/requests/validate_body.go @@ -13,6 +13,7 @@ import ( v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" @@ -109,7 +110,7 @@ func (v *requestBodyValidator) ValidateRequestBodyWithPathItem(request *http.Req } // render the schema, to be used for validation - validationSucceeded, validationErrors := ValidateRequestSchema(request, schema, renderedInline, renderedJSON) + validationSucceeded, validationErrors := ValidateRequestSchema(request, schema, renderedInline, renderedJSON, config.WithRegexEngine(v.options.RegexEngine)) errors.PopulateValidationErrors(validationErrors, request, pathValue) diff --git a/requests/validate_request.go b/requests/validate_request.go index 22730d8..1e0eace 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -20,6 +20,7 @@ import ( "golang.org/x/text/message" "gopkg.in/yaml.v3" + "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/schema_validation" @@ -34,7 +35,10 @@ func ValidateRequestSchema( schema *base.Schema, renderedSchema, jsonSchema []byte, + opts ...config.Option, ) (bool, []*errors.ValidationError) { + options := config.NewValidationOptions(opts...) + var validationErrors []*errors.ValidationError var requestBody []byte @@ -107,6 +111,7 @@ func ValidateRequestSchema( } compiler := jsonschema.NewCompiler() + compiler.UseRegexpEngine(options.RegexEngine) // Ensure any configured regex engine is used. compiler.UseLoader(helpers.NewCompilerLoader()) decodedSchema, _ := jsonschema.UnmarshalJSON(strings.NewReader(string(jsonSchema))) _ = compiler.AddResource("requestBody.json", decodedSchema) diff --git a/responses/response_body.go b/responses/response_body.go index 9ad8c4a..f8b8da5 100644 --- a/responses/response_body.go +++ b/responses/response_body.go @@ -11,6 +11,7 @@ import ( v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" ) @@ -31,8 +32,10 @@ type ResponseBodyValidator interface { } // NewResponseBodyValidator will create a new ResponseBodyValidator from an OpenAPI 3+ document -func NewResponseBodyValidator(document *v3.Document) ResponseBodyValidator { - return &responseBodyValidator{document: document, schemaCache: &sync.Map{}} +func NewResponseBodyValidator(document *v3.Document, opts ...config.Option) ResponseBodyValidator { + options := config.NewValidationOptions(opts...) + + return &responseBodyValidator{options: options, document: document, schemaCache: &sync.Map{}} } type schemaCache struct { @@ -42,6 +45,7 @@ type schemaCache struct { } type responseBodyValidator struct { + options *config.ValidationOptions document *v3.Document schemaCache *sync.Map } diff --git a/responses/validate_body.go b/responses/validate_body.go index 855c643..c848e83 100644 --- a/responses/validate_body.go +++ b/responses/validate_body.go @@ -15,6 +15,7 @@ import ( v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" @@ -156,7 +157,7 @@ func (v *responseBodyValidator) checkResponseSchema( } // render the schema, to be used for validation - valid, vErrs := ValidateResponseSchema(request, response, schema, renderedInline, renderedJSON) + valid, vErrs := ValidateResponseSchema(request, response, schema, renderedInline, renderedJSON, config.WithRegexEngine(v.options.RegexEngine)) if !valid { validationErrors = append(validationErrors, vErrs...) } diff --git a/responses/validate_response.go b/responses/validate_response.go index 6e2d9a4..a93fe7b 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -20,6 +20,7 @@ import ( "golang.org/x/text/message" "gopkg.in/yaml.v3" + "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/schema_validation" @@ -38,7 +39,10 @@ func ValidateResponseSchema( schema *base.Schema, renderedSchema, jsonSchema []byte, + opts ...config.Option, ) (bool, []*errors.ValidationError) { + options := config.NewValidationOptions(opts...) + var validationErrors []*errors.ValidationError if response == nil || response.Body == nil { @@ -126,6 +130,7 @@ func ValidateResponseSchema( // create a new jsonschema compiler and add in the rendered JSON schema. compiler := jsonschema.NewCompiler() + compiler.UseRegexpEngine(options.RegexEngine) compiler.UseLoader(helpers.NewCompilerLoader()) fName := fmt.Sprintf("%s.json", helpers.ResponseBodyValidation) decodedSchema, _ := jsonschema.UnmarshalJSON(strings.NewReader(string(jsonSchema))) diff --git a/schema_validation/validate_document.go b/schema_validation/validate_document.go index a207462..4a72afd 100644 --- a/schema_validation/validate_document.go +++ b/schema_validation/validate_document.go @@ -14,19 +14,23 @@ import ( "golang.org/x/text/message" "gopkg.in/yaml.v3" + "github.com/pb33f/libopenapi-validator/config" liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" ) // ValidateOpenAPIDocument will validate an OpenAPI document against the OpenAPI 2, 3.0 and 3.1 schemas (depending on version) // It will return true if the document is valid, false if it is not and a slice of ValidationError pointers. -func ValidateOpenAPIDocument(doc libopenapi.Document) (bool, []*liberrors.ValidationError) { +func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bool, []*liberrors.ValidationError) { + options := config.NewValidationOptions(opts...) + info := doc.GetSpecInfo() loadedSchema := info.APISchema var validationErrors []*liberrors.ValidationError decodedDocument := *info.SpecJSON compiler := jsonschema.NewCompiler() + compiler.UseRegexpEngine(options.RegexEngine) compiler.UseLoader(helpers.NewCompilerLoader()) decodedSchema, _ := jsonschema.UnmarshalJSON(strings.NewReader(string(loadedSchema))) diff --git a/schema_validation/validate_schema.go b/schema_validation/validate_schema.go index f56ee7f..6dec2a6 100644 --- a/schema_validation/validate_schema.go +++ b/schema_validation/validate_schema.go @@ -24,6 +24,7 @@ import ( _ "embed" + "github.com/pb33f/libopenapi-validator/config" liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" ) @@ -50,21 +51,24 @@ type SchemaValidator interface { var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`) type schemaValidator struct { - logger *slog.Logger - lock sync.Mutex + options *config.ValidationOptions + logger *slog.Logger + lock sync.Mutex } // NewSchemaValidatorWithLogger will create a new SchemaValidator instance, ready to accept schemas and payloads to validate. -func NewSchemaValidatorWithLogger(logger *slog.Logger) SchemaValidator { - return &schemaValidator{logger: logger, lock: sync.Mutex{}} +func NewSchemaValidatorWithLogger(logger *slog.Logger, opts ...config.Option) SchemaValidator { + options := config.NewValidationOptions(opts...) + + return &schemaValidator{options: options, logger: logger, lock: sync.Mutex{}} } // NewSchemaValidator will create a new SchemaValidator instance, ready to accept schemas and payloads to validate. -func NewSchemaValidator() SchemaValidator { +func NewSchemaValidator(opts ...config.Option) SchemaValidator { logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelError, })) - return NewSchemaValidatorWithLogger(logger) + return NewSchemaValidatorWithLogger(logger, opts...) } func (s *schemaValidator) ValidateSchemaString(schema *base.Schema, payload string) (bool, []*liberrors.ValidationError) { @@ -125,6 +129,7 @@ func (s *schemaValidator) validateSchema(schema *base.Schema, payload []byte, de } compiler := jsonschema.NewCompiler() + compiler.UseRegexpEngine(s.options.RegexEngine) compiler.UseLoader(helpers.NewCompilerLoader()) decodedSchema, _ := jsonschema.UnmarshalJSON(strings.NewReader(string(jsonSchema))) diff --git a/validator.go b/validator.go index f90af9c..8d2f547 100644 --- a/validator.go +++ b/validator.go @@ -11,6 +11,7 @@ import ( v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/parameters" "github.com/pb33f/libopenapi-validator/paths" @@ -63,33 +64,32 @@ type Validator interface { } // NewValidator will create a new Validator from an OpenAPI 3+ document -func NewValidator(document libopenapi.Document) (Validator, []error) { +func NewValidator(document libopenapi.Document, opts ...config.Option) (Validator, []error) { m, errs := document.BuildV3Model() if errs != nil { return nil, errs } - v := NewValidatorFromV3Model(&m.Model) + v := NewValidatorFromV3Model(&m.Model, opts...) v.(*validator).document = document return v, nil } // NewValidatorFromV3Model will create a new Validator from an OpenAPI Model -func NewValidatorFromV3Model(m *v3.Document) Validator { +func NewValidatorFromV3Model(m *v3.Document, opts ...config.Option) Validator { + options := config.NewValidationOptions(opts...) + + v := &validator{options: options, v3Model: m} + // create a new parameter validator - paramValidator := parameters.NewParameterValidator(m) + v.paramValidator = parameters.NewParameterValidator(m, opts...) - // create a new request body validator - reqBodyValidator := requests.NewRequestBodyValidator(m) + // create aq new request body validator + v.requestValidator = requests.NewRequestBodyValidator(m, opts...) // create a response body validator - respBodyValidator := responses.NewResponseBodyValidator(m) + v.responseValidator = responses.NewResponseBodyValidator(m, opts...) - return &validator{ - v3Model: m, - requestValidator: reqBodyValidator, - responseValidator: respBodyValidator, - paramValidator: paramValidator, - } + return v } func (v *validator) GetParameterValidator() parameters.ParameterValidator { @@ -305,6 +305,7 @@ func (v *validator) ValidateHttpRequestSyncWithPathItem(request *http.Request, p } type validator struct { + options *config.ValidationOptions v3Model *v3.Document document libopenapi.Document paramValidator parameters.ParameterValidator diff --git a/validator_test.go b/validator_test.go index 0013735..e7b3ab9 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13,11 +13,13 @@ import ( "testing" "github.com/pb33f/libopenapi" + "github.com/santhosh-tekuri/jsonschema/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/helpers" ) @@ -136,6 +138,33 @@ func TestNewValidator_ValidateDocument(t *testing.T) { assert.Len(t, errs, 0) } +type alwaysMatchesRegex jsonschema.RegexpEngine + +func (dr *alwaysMatchesRegex) MatchString(s string) bool { + return true +} + +func (dr *alwaysMatchesRegex) String() string { + return "" +} + +func fakeRegexEngine(s string) (jsonschema.Regexp, error) { + return (*alwaysMatchesRegex)(nil), nil +} + +func TestNewValidator_WithRegex(t *testing.T) { + doc, err := libopenapi.NewDocument(petstoreBytes) + require.Nil(t, err, "Failed to load spec") + + v, errs := NewValidator(doc, config.WithRegexEngine(fakeRegexEngine)) + require.Empty(t, errs, "Failed to build validator") + require.NotNil(t, v, "Failed to build validator") + + valid, valErrs := v.ValidateDocument() + assert.True(t, valid) + assert.Empty(t, valErrs) +} + func TestNewValidator_BadDoc(t *testing.T) { spec := `swagger: 2.0`