Skip to content

Commit

Permalink
Add support for pluggable Regular Express Engines.
Browse files Browse the repository at this point in the history
By default the underlying JSONSchema validator uses the standard Go
regular expression engine; this engine supports the RE2 syntax which
may result in failures during validation of OAS specifcations that
use non-RE2 compliant patterns.

The change introduces a configuration construct for each validator
that may be passed at time of creation to affect the way it operates.

Initially this construct allows for the regular expression engine to
be overriden by a user-supplied variant.

Closes #111
  • Loading branch information
JemDay committed Dec 10, 2024
1 parent c7a822d commit c99382e
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea/
17 changes: 15 additions & 2 deletions parameters/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package parameters

import (
"github.com/santhosh-tekuri/jsonschema/v6"
"net/http"

v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
Expand Down Expand Up @@ -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 &paramValidator{document: document}
func NewParameterValidator(document *v3.Document, cfg ...Config) ParameterValidator {

config := Config{}

if len(cfg) > 0 {
config = cfg[0]
}

return &paramValidator{config, document}
}

type paramValidator struct {
Config
document *v3.Document
}
17 changes: 15 additions & 2 deletions requests/request_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package requests

import (
"github.com/santhosh-tekuri/jsonschema/v6"
"net/http"
"sync"

Expand All @@ -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 {
Expand All @@ -41,6 +53,7 @@ type schemaCache struct {
}

type requestBodyValidator struct {
Config
document *v3.Document
schemaCache *sync.Map
}
2 changes: 1 addition & 1 deletion requests/validate_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 8 additions & 0 deletions requests/validate_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 15 additions & 2 deletions responses/response_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package responses

import (
"github.com/santhosh-tekuri/jsonschema/v6"
"net/http"
"sync"

Expand All @@ -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 {
Expand All @@ -42,6 +54,7 @@ type schemaCache struct {
}

type responseBodyValidator struct {
Config
document *v3.Document
schemaCache *sync.Map
}
2 changes: 1 addition & 1 deletion responses/validate_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
}
Expand Down
9 changes: 9 additions & 0 deletions responses/validate_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)))
Expand Down
9 changes: 8 additions & 1 deletion schema_validation/validate_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
21 changes: 17 additions & 4 deletions schema_validation/validate_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)))
Expand Down
27 changes: 21 additions & 6 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package validator

import (
"github.com/santhosh-tekuri/jsonschema/v6"
"net/http"
"sync"

Expand Down Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down

0 comments on commit c99382e

Please sign in to comment.