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 all 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
34 changes: 34 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -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
}
}
8 changes: 6 additions & 2 deletions parameters/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 &paramValidator{document: document}
func NewParameterValidator(document *v3.Document, opts ...config.Option) ParameterValidator {
options := config.NewValidationOptions(opts...)

return &paramValidator{options: options, document: document}
}

type paramValidator struct {
options *config.ValidationOptions
document *v3.Document
}
8 changes: 6 additions & 2 deletions requests/request_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

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

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

Expand Down
5 changes: 5 additions & 0 deletions requests/validate_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions responses/response_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

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

type responseBodyValidator struct {
options *config.ValidationOptions
document *v3.Document
schemaCache *sync.Map
}
3 changes: 2 additions & 1 deletion responses/validate_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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...)
}
Expand Down
5 changes: 5 additions & 0 deletions responses/validate_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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)))
Expand Down
6 changes: 5 additions & 1 deletion schema_validation/validate_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
17 changes: 11 additions & 6 deletions schema_validation/validate_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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) {
Expand Down Expand Up @@ -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)))
Expand Down
27 changes: 14 additions & 13 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with FromV3Model, that would become a WithV3Model(m)

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 {
Expand Down Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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`

Expand Down
Loading