diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/parameters/parameters.go b/parameters/parameters.go index 6ea572b..17b8784 100644 --- a/parameters/parameters.go +++ b/parameters/parameters.go @@ -4,6 +4,7 @@ package parameters import ( + "github.com/santhosh-tekuri/jsonschema/v6" "net/http" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" @@ -65,11 +66,23 @@ type ParameterValidator interface { ValidateSecurityWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) } +type Config struct { + RegexEngine jsonschema.RegexpEngine +} + // 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, cfg ...Config) ParameterValidator { + + config := Config{} + + if len(cfg) > 0 { + config = cfg[0] + } + + return ¶mValidator{config, document} } type paramValidator struct { + Config document *v3.Document } diff --git a/requests/request_body.go b/requests/request_body.go index 3c534a6..589d2ef 100644 --- a/requests/request_body.go +++ b/requests/request_body.go @@ -4,6 +4,7 @@ package requests import ( + "github.com/santhosh-tekuri/jsonschema/v6" "net/http" "sync" @@ -29,9 +30,20 @@ type RequestBodyValidator interface { ValidateRequestBodyWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) } +type Config struct { + RegexEngine jsonschema.RegexpEngine +} + // 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, cfg ...Config) RequestBodyValidator { + + config := Config{} // Default + + if len(cfg) > 0 { + config = cfg[0] + } + + return &requestBodyValidator{Config: config, document: document, schemaCache: &sync.Map{}} } type schemaCache struct { @@ -41,6 +53,7 @@ type schemaCache struct { } type requestBodyValidator struct { + Config document *v3.Document schemaCache *sync.Map } diff --git a/requests/validate_body.go b/requests/validate_body.go index f36abad..f1de59a 100644 --- a/requests/validate_body.go +++ b/requests/validate_body.go @@ -109,7 +109,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, v.Config) errors.PopulateValidationErrors(validationErrors, request, pathValue) diff --git a/requests/validate_request.go b/requests/validate_request.go index 22730d8..66676a8 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -34,7 +34,14 @@ func ValidateRequestSchema( schema *base.Schema, renderedSchema, jsonSchema []byte, + cfg ...Config, ) (bool, []*errors.ValidationError) { + + config := Config{} // Default + if len(cfg) > 0 { + config = cfg[0] + } + var validationErrors []*errors.ValidationError var requestBody []byte @@ -107,6 +114,7 @@ func ValidateRequestSchema( } compiler := jsonschema.NewCompiler() + compiler.UseRegexpEngine(config.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..9ccbafd 100644 --- a/responses/response_body.go +++ b/responses/response_body.go @@ -4,6 +4,7 @@ package responses import ( + "github.com/santhosh-tekuri/jsonschema/v6" "net/http" "sync" @@ -30,9 +31,20 @@ type ResponseBodyValidator interface { ValidateResponseBodyWithPathItem(request *http.Request, response *http.Response, pathItem *v3.PathItem, pathFound string) (bool, []*errors.ValidationError) } +type Config struct { + RegexEngine jsonschema.RegexpEngine +} + // 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, cfg ...Config) ResponseBodyValidator { + + config := Config{} // Default + + if len(cfg) > 0 { + config = cfg[0] + } + + return &responseBodyValidator{Config: config, document: document, schemaCache: &sync.Map{}} } type schemaCache struct { @@ -42,6 +54,7 @@ type schemaCache struct { } type responseBodyValidator struct { + Config document *v3.Document schemaCache *sync.Map } diff --git a/responses/validate_body.go b/responses/validate_body.go index 855c643..f101a47 100644 --- a/responses/validate_body.go +++ b/responses/validate_body.go @@ -156,7 +156,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, v.Config) if !valid { validationErrors = append(validationErrors, vErrs...) } diff --git a/responses/validate_response.go b/responses/validate_response.go index 6e2d9a4..23dbcb1 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -38,7 +38,15 @@ func ValidateResponseSchema( schema *base.Schema, renderedSchema, jsonSchema []byte, + cfg ...Config, ) (bool, []*errors.ValidationError) { + + config := Config{} // A Default + + if len(cfg) > 0 { + config = cfg[0] + } + var validationErrors []*errors.ValidationError if response == nil || response.Body == nil { @@ -126,6 +134,7 @@ func ValidateResponseSchema( // create a new jsonschema compiler and add in the rendered JSON schema. compiler := jsonschema.NewCompiler() + compiler.UseRegexpEngine(config.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..04487dc 100644 --- a/schema_validation/validate_document.go +++ b/schema_validation/validate_document.go @@ -20,13 +20,20 @@ import ( // 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, config ...Config) (bool, []*liberrors.ValidationError) { + + cfg := Config{} // Default Configuration + if len(config) > 0 { + cfg = config[0] + } + info := doc.GetSpecInfo() loadedSchema := info.APISchema var validationErrors []*liberrors.ValidationError decodedDocument := *info.SpecJSON compiler := jsonschema.NewCompiler() + compiler.UseRegexpEngine(cfg.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..d1a5230 100644 --- a/schema_validation/validate_schema.go +++ b/schema_validation/validate_schema.go @@ -47,24 +47,36 @@ type SchemaValidator interface { ValidateSchemaBytes(schema *base.Schema, payload []byte) (bool, []*liberrors.ValidationError) } +// Config Holds Schema Validation options +type Config struct { + RegexEngine jsonschema.RegexpEngine // Use the given Regular Expression Engine +} + var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`) type schemaValidator struct { + Config 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, config ...Config) SchemaValidator { + + cfg := Config{} // Default Config + if len(config) > 0 { + cfg = config[0] + } + + return &schemaValidator{Config: cfg, 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(config ...Config) SchemaValidator { logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelError, })) - return NewSchemaValidatorWithLogger(logger) + return NewSchemaValidatorWithLogger(logger, config...) } func (s *schemaValidator) ValidateSchemaString(schema *base.Schema, payload string) (bool, []*liberrors.ValidationError) { @@ -125,6 +137,7 @@ func (s *schemaValidator) validateSchema(schema *base.Schema, payload []byte, de } compiler := jsonschema.NewCompiler() + compiler.UseRegexpEngine(s.RegexEngine) compiler.UseLoader(helpers.NewCompilerLoader()) decodedSchema, _ := jsonschema.UnmarshalJSON(strings.NewReader(string(jsonSchema))) diff --git a/validator.go b/validator.go index f90af9c..db3307c 100644 --- a/validator.go +++ b/validator.go @@ -4,6 +4,7 @@ package validator import ( + "github.com/santhosh-tekuri/jsonschema/v6" "net/http" "sync" @@ -62,27 +63,41 @@ type Validator interface { GetResponseBodyValidator() responses.ResponseBodyValidator } +// Configuration Holds any Validator configuration overrides. +type Configuration struct { + // Use this regex engine in place of the standard Go (RE2) pattern processor + RegexEngine jsonschema.RegexpEngine +} + // NewValidator will create a new Validator from an OpenAPI 3+ document -func NewValidator(document libopenapi.Document) (Validator, []error) { +func NewValidator(document libopenapi.Document, config ...Configuration) (Validator, []error) { m, errs := document.BuildV3Model() if errs != nil { return nil, errs } - v := NewValidatorFromV3Model(&m.Model) + v := NewValidatorFromV3Model(&m.Model, config...) 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, config ...Configuration) Validator { + + // Assume a default configuration + cfg := Configuration{} + + if len(config) > 0 { + cfg = config[0] + } + // create a new parameter validator - paramValidator := parameters.NewParameterValidator(m) + paramValidator := parameters.NewParameterValidator(m, parameters.Config{RegexEngine: cfg.RegexEngine}) // create a new request body validator - reqBodyValidator := requests.NewRequestBodyValidator(m) + reqBodyValidator := requests.NewRequestBodyValidator(m, requests.Config{RegexEngine: cfg.RegexEngine}) // create a response body validator - respBodyValidator := responses.NewResponseBodyValidator(m) + respBodyValidator := responses.NewResponseBodyValidator(m, responses.Config{RegexEngine: cfg.RegexEngine}) return &validator{ v3Model: m, diff --git a/validator_test.go b/validator_test.go index 0013735..8bc55e4 100644 --- a/validator_test.go +++ b/validator_test.go @@ -136,6 +136,19 @@ func TestNewValidator_ValidateDocument(t *testing.T) { assert.Len(t, errs, 0) } +func TestNewValidator_WithConfig(t *testing.T) { + + // This needs work. + validatorConfig := Configuration{} + + doc, _ := libopenapi.NewDocument(petstoreBytes) + v, _ := NewValidator(doc, validatorConfig) + require.NotNil(t, v, "Failed to build validator") + valid, errs := v.ValidateDocument() + assert.True(t, valid) + assert.Len(t, errs, 0) +} + func TestNewValidator_BadDoc(t *testing.T) { spec := `swagger: 2.0`