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

Add support for pluggable Regex Engines #112

Merged
merged 8 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
26 changes: 23 additions & 3 deletions parameters/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package parameters
import (
"net/http"

"github.com/santhosh-tekuri/jsonschema/v6"

v3 "github.com/pb33f/libopenapi/datamodel/high/v3"

"github.com/pb33f/libopenapi-validator/errors"
Expand Down Expand Up @@ -65,11 +67,29 @@ type ParameterValidator interface {
ValidateSecurityWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError)
}

// Option supports the 'Options Pattern' to define the behavior of a ParameterValidator
type Option func(validator *paramValidator)

// WithRegexEngine allows for a custom regular expression engine to be used during validation.
func WithRegexEngine(engine jsonschema.RegexpEngine) Option {
return func(pv *paramValidator) {
pv.regexEngine = engine
}
}

// 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, opts ...Option) ParameterValidator {

pv := paramValidator{document: document}

for _, opt := range opts {
opt(&pv)
}

return &pv
}

type paramValidator struct {
document *v3.Document
regexEngine jsonschema.RegexpEngine
document *v3.Document
}
27 changes: 25 additions & 2 deletions requests/request_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/pb33f/libopenapi/datamodel/high/v3"
"github.com/santhosh-tekuri/jsonschema/v6"

"github.com/pb33f/libopenapi-validator/errors"
)
Expand All @@ -29,9 +30,30 @@ type RequestBodyValidator interface {
ValidateRequestBodyWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError)
}

type configOptions struct {
regexEngine jsonschema.RegexpEngine
}

// Option supports the 'Options Pattern' to define the behavior of a RequestBodyValidator
type Option func(options *configOptions)

// WithRegexEngine allows for a custom regular expression engine to be used during validation.
func WithRegexEngine(engine jsonschema.RegexpEngine) Option {
return func(rbv *configOptions) {
rbv.regexEngine = engine
}
}

// 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, opt ...Option) RequestBodyValidator {

cfg := configOptions{} // Default Options
for _, o := range opt {
JemDay marked this conversation as resolved.
Show resolved Hide resolved
o(&cfg)
}

return &requestBodyValidator{configOptions: cfg, document: document, schemaCache: &sync.Map{}}
JemDay marked this conversation as resolved.
Show resolved Hide resolved

}

type schemaCache struct {
Expand All @@ -41,6 +63,7 @@ type schemaCache struct {
}

type requestBodyValidator struct {
configOptions
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, WithRegexEngine(v.regexEngine))

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,
opts ...Option,
) (bool, []*errors.ValidationError) {

config := configOptions{}
for _, opt := range opts {
opt(&config)
}
JemDay marked this conversation as resolved.
Show resolved Hide resolved

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
26 changes: 24 additions & 2 deletions responses/response_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"

"github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/santhosh-tekuri/jsonschema/v6"

v3 "github.com/pb33f/libopenapi/datamodel/high/v3"

Expand All @@ -30,9 +31,29 @@ type ResponseBodyValidator interface {
ValidateResponseBodyWithPathItem(request *http.Request, response *http.Response, pathItem *v3.PathItem, pathFound string) (bool, []*errors.ValidationError)
}

type Option func(validator *configOptions)

func WithRegexEngine(engine jsonschema.RegexpEngine) Option {
return func(v *configOptions) {
v.regexEngine = engine
}
}

type configOptions struct {
regexEngine jsonschema.RegexpEngine
}

JemDay marked this conversation as resolved.
Show resolved Hide resolved
// 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 ...Option) ResponseBodyValidator {

cfg := configOptions{} // Default Config

for _, opt := range opts {
opt(&cfg)
}

return &responseBodyValidator{configOptions: cfg, document: document, schemaCache: &sync.Map{}}

}

type schemaCache struct {
Expand All @@ -42,6 +63,7 @@ type schemaCache struct {
}

type responseBodyValidator struct {
configOptions
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, WithRegexEngine(v.regexEngine))
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 ...Option,
) (bool, []*errors.ValidationError) {

// Apply config options
config := configOptions{} // Default config
for _, opt := range cfg {
opt(&config)
}
JemDay marked this conversation as resolved.
Show resolved Hide resolved

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
10 changes: 9 additions & 1 deletion schema_validation/validate_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@ 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, opts ...Option) (bool, []*liberrors.ValidationError) {

// Apply any configuration options
cfg := config{} // Default Configuration
for _, opt := range opts {
opt(&cfg)
}

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
32 changes: 28 additions & 4 deletions schema_validation/validate_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,47 @@ type SchemaValidator interface {
ValidateSchemaBytes(schema *base.Schema, payload []byte) (bool, []*liberrors.ValidationError)
}

// Option supports the 'Options Pattern' to define the behavior of a SchemaValidator
type Option func(*config)

// WithRegexEngine allows for a custom regular expression engine to be used during validation.
func WithRegexEngine(engine jsonschema.RegexpEngine) Option {
return func(c *config) {
c.regexEngine = engine
}
}

type config struct {
regexEngine jsonschema.RegexpEngine
}
JemDay marked this conversation as resolved.
Show resolved Hide resolved

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, opts ...Option) SchemaValidator {

cfg := config{}
for _, opt := range opts {
opt(&cfg)
}

// Build a validator
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(opts ...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) {
Expand Down Expand Up @@ -125,6 +148,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
40 changes: 27 additions & 13 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"

"github.com/pb33f/libopenapi"
"github.com/santhosh-tekuri/jsonschema/v6"

v3 "github.com/pb33f/libopenapi/datamodel/high/v3"

Expand Down Expand Up @@ -62,34 +63,46 @@ type Validator interface {
GetResponseBodyValidator() responses.ResponseBodyValidator
}

// Option supports the 'Options Pattern' to define the behavior of a Validator
type Option func(*validator)

// WithRegexEngine allows for a custom regular expression engine to be used during validation.
func WithRegexEngine(engine jsonschema.RegexpEngine) Option {
return func(v *validator) {
v.regexEngine = engine
}
}

// NewValidator will create a new Validator from an OpenAPI 3+ document
func NewValidator(document libopenapi.Document) (Validator, []error) {
func NewValidator(document libopenapi.Document, opts ...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 ...Option) Validator {

v := &validator{v3Model: m}

for _, opt := range opts {
opt(v)
}

// create a new parameter validator
paramValidator := parameters.NewParameterValidator(m)
v.paramValidator = parameters.NewParameterValidator(m, parameters.WithRegexEngine(v.regexEngine))

// create a new request body validator
reqBodyValidator := requests.NewRequestBodyValidator(m)
// create aq new request body validator
v.requestValidator = requests.NewRequestBodyValidator(m, requests.WithRegexEngine(v.regexEngine))

// create a response body validator
respBodyValidator := responses.NewResponseBodyValidator(m)
v.responseValidator = responses.NewResponseBodyValidator(m, responses.WithRegexEngine(v.regexEngine))

return &validator{
v3Model: m,
requestValidator: reqBodyValidator,
responseValidator: respBodyValidator,
paramValidator: paramValidator,
}
return v
}

func (v *validator) GetParameterValidator() parameters.ParameterValidator {
Expand Down Expand Up @@ -310,6 +323,7 @@ type validator struct {
paramValidator parameters.ParameterValidator
requestValidator requests.RequestBodyValidator
responseValidator responses.ResponseBodyValidator
regexEngine jsonschema.RegexpEngine
}

func runValidation(control, doneChan chan struct{},
Expand Down
Loading