diff --git a/config/config.go b/config/config.go index f01d8ba..316bc05 100644 --- a/config/config.go +++ b/config/config.go @@ -1,12 +1,24 @@ package config -import "github.com/santhosh-tekuri/jsonschema/v6" +import ( + "github.com/santhosh-tekuri/jsonschema/v6" +) + +// RegexCache can be set to enable compiled regex caching. +// It can be just a sync.Map, or a custom implementation with possible cleanup. +// +// Be aware that the cache should be thread safe +type RegexCache interface { + Load(key any) (value any, ok bool) // Get a compiled regex from the cache + Store(key, value any) // Set a compiled regex to the cache +} // ValidationOptions A container for validation configuration. // // Generally fluent With... style functions are used to establish the desired behavior. type ValidationOptions struct { RegexEngine jsonschema.RegexpEngine + RegexCache RegexCache // Enable compiled regex caching FormatAssertions bool ContentAssertions bool SecurityValidation bool @@ -44,6 +56,7 @@ func WithExistingOpts(options *ValidationOptions) Option { return func(o *ValidationOptions) { if options != nil { o.RegexEngine = options.RegexEngine + o.RegexCache = options.RegexCache o.FormatAssertions = options.FormatAssertions o.ContentAssertions = options.ContentAssertions o.SecurityValidation = options.SecurityValidation @@ -61,6 +74,14 @@ func WithRegexEngine(engine jsonschema.RegexpEngine) Option { } } +// WithRegexCache assigns a cache for compiled regular expressions. +// A sync.Map should be sufficient for most use cases. It does not implement any cleanup +func WithRegexCache(regexCache RegexCache) Option { + return func(o *ValidationOptions) { + o.RegexCache = regexCache + } +} + // WithFormatAssertions enables checks for 'format' assertions (such as date, date-time, uuid, etc) func WithFormatAssertions() Option { return func(o *ValidationOptions) { diff --git a/config/config_test.go b/config/config_test.go index 462a0f2..6066297 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -4,6 +4,7 @@ package config import ( + "sync" "testing" "github.com/santhosh-tekuri/jsonschema/v6" @@ -20,6 +21,7 @@ func TestNewValidationOptions_Defaults(t *testing.T) { assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) + assert.Nil(t, opts.RegexCache) } func TestNewValidationOptions_WithNilOption(t *testing.T) { @@ -32,6 +34,7 @@ func TestNewValidationOptions_WithNilOption(t *testing.T) { assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) + assert.Nil(t, opts.RegexCache) } func TestWithFormatAssertions(t *testing.T) { @@ -43,6 +46,7 @@ func TestWithFormatAssertions(t *testing.T) { assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) + assert.Nil(t, opts.RegexCache) } func TestWithContentAssertions(t *testing.T) { @@ -54,6 +58,7 @@ func TestWithContentAssertions(t *testing.T) { assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) + assert.Nil(t, opts.RegexCache) } func TestWithoutSecurityValidation(t *testing.T) { @@ -65,6 +70,7 @@ func TestWithoutSecurityValidation(t *testing.T) { assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) + assert.Nil(t, opts.RegexCache) } func TestWithRegexEngine(t *testing.T) { @@ -79,6 +85,7 @@ func TestWithRegexEngine(t *testing.T) { assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) + assert.Nil(t, opts.RegexCache) } func TestWithExistingOpts(t *testing.T) { @@ -86,6 +93,7 @@ func TestWithExistingOpts(t *testing.T) { var testEngine jsonschema.RegexpEngine = nil original := &ValidationOptions{ RegexEngine: testEngine, + RegexCache: &sync.Map{}, FormatAssertions: true, ContentAssertions: true, SecurityValidation: false, @@ -95,6 +103,7 @@ func TestWithExistingOpts(t *testing.T) { opts := NewValidationOptions(WithExistingOpts(original)) assert.Nil(t, opts.RegexEngine) // Both should be nil + assert.NotNil(t, opts.RegexCache) assert.Equal(t, original.FormatAssertions, opts.FormatAssertions) assert.Equal(t, original.ContentAssertions, opts.ContentAssertions) assert.Equal(t, original.SecurityValidation, opts.SecurityValidation) @@ -112,6 +121,7 @@ func TestWithExistingOpts_NilSource(t *testing.T) { assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) + assert.Nil(t, opts.RegexCache) } func TestMultipleOptions(t *testing.T) { @@ -126,6 +136,7 @@ func TestMultipleOptions(t *testing.T) { assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) + assert.Nil(t, opts.RegexCache) } func TestOptionOverride(t *testing.T) { @@ -142,6 +153,7 @@ func TestOptionOverride(t *testing.T) { assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) + assert.Nil(t, opts.RegexCache) } func TestWithExistingOpts_PartialOverride(t *testing.T) { @@ -160,7 +172,8 @@ func TestWithExistingOpts_PartialOverride(t *testing.T) { WithContentAssertions(), // This should still be true (no change) ) - assert.Nil(t, opts.RegexEngine) // Both should be nil + assert.Nil(t, opts.RegexEngine) // Both should be nil + assert.Nil(t, opts.RegexCache) assert.True(t, opts.FormatAssertions) // From original assert.True(t, opts.ContentAssertions) // Reapplied, but same value assert.False(t, opts.SecurityValidation) // From original @@ -189,6 +202,7 @@ func TestComplexScenario(t *testing.T) { assert.True(t, opts.ContentAssertions) // Added assert.False(t, opts.SecurityValidation) // From base assert.Nil(t, opts.RegexEngine) // Should be nil + assert.Nil(t, opts.RegexCache) } func TestMultipleOptionsWithSecurityDisabled(t *testing.T) { @@ -202,6 +216,7 @@ func TestMultipleOptionsWithSecurityDisabled(t *testing.T) { assert.True(t, opts.ContentAssertions) assert.False(t, opts.SecurityValidation) assert.Nil(t, opts.RegexEngine) + assert.Nil(t, opts.RegexCache) } func TestWithExistingOpts_SecurityValidationCopied(t *testing.T) { @@ -296,6 +311,7 @@ func TestComplexOpenAPIScenario(t *testing.T) { assert.True(t, opts.ContentAssertions) assert.False(t, opts.SecurityValidation) assert.Nil(t, opts.RegexEngine) + assert.Nil(t, opts.RegexCache) } func TestWithExistingOpts_OpenAPIFields(t *testing.T) { @@ -329,3 +345,11 @@ func TestWithCustomFormat(t *testing.T) { assert.Contains(t, opts.Formats, "test-format") assert.NotNil(t, opts.Formats["test-format"]) } + +func TestWithRegexpCache(t *testing.T) { + syncMap := &sync.Map{} + + opts := NewValidationOptions(WithRegexCache(syncMap)) + + assert.NotNil(t, opts.RegexCache) +} diff --git a/helpers/regex_maker.go b/helpers/regex_maker.go index dc00fb0..3fcbc2e 100644 --- a/helpers/regex_maker.go +++ b/helpers/regex_maker.go @@ -7,6 +7,12 @@ import ( "strings" ) +var ( + baseDefaultPattern = "[^/]*" + DefaultPatternRegex = regexp.MustCompile("^([^/]*)$") + DefaultPatternRegexString = DefaultPatternRegex.String() +) + // GetRegexForPath returns a compiled regular expression for the given path template. // // This function takes a path template string `tpl` and generates a regular expression @@ -42,9 +48,6 @@ func GetRegexForPath(tpl string) (*regexp.Regexp, error) { // Backup the original. template := tpl - // Now let's parse it. - defaultPattern := "[^/]*" - pattern := bytes.NewBufferString("^") var end int @@ -55,7 +58,7 @@ func GetRegexForPath(tpl string) (*regexp.Regexp, error) { end = idxs[i+1] parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) name := parts[0] - patt := defaultPattern + patt := baseDefaultPattern if len(parts) == 2 { patt = parts[1] } @@ -79,8 +82,14 @@ func GetRegexForPath(tpl string) (*regexp.Regexp, error) { pattern.WriteByte('$') + patternString := pattern.String() + + if patternString == DefaultPatternRegexString { + return DefaultPatternRegex, nil + } + // Compile full regexp. - reg, errCompile := regexp.Compile(pattern.String()) + reg, errCompile := regexp.Compile(patternString) if errCompile != nil { return nil, errCompile } diff --git a/helpers/regex_maker_test.go b/helpers/regex_maker_test.go index ced295c..b2cff44 100644 --- a/helpers/regex_maker_test.go +++ b/helpers/regex_maker_test.go @@ -2,6 +2,8 @@ package helpers import ( "testing" + + "github.com/stretchr/testify/assert" ) func TestGetRegexForPath(t *testing.T) { @@ -61,6 +63,12 @@ func TestGetRegexForPath(t *testing.T) { wantErr: false, wantExpr: "^/entities\\('([0-9]+)'\\)$", }, + { + name: "get default pattern", + tpl: "/{param}", + wantErr: false, + wantExpr: "^/([^/]*)$", + }, } for _, tt := range tests { @@ -128,6 +136,14 @@ func TestBraceIndices(t *testing.T) { } } +func TestDefaultPatternCompileCache(t *testing.T) { + res, err := GetRegexForPath("{param}") + + assert.Nil(t, err) + assert.Equal(t, res, DefaultPatternRegex) + assert.Equal(t, res.String(), DefaultPatternRegexString) +} + func equal(a, b []int) bool { if len(a) != len(b) { return false diff --git a/parameters/cookie_parameters.go b/parameters/cookie_parameters.go index 3b86521..8f74b9f 100644 --- a/parameters/cookie_parameters.go +++ b/parameters/cookie_parameters.go @@ -19,7 +19,7 @@ import ( ) func (v *paramValidator) ValidateCookieParams(request *http.Request) (bool, []*errors.ValidationError) { - pathItem, errs, foundPath := paths.FindPath(request, v.document) + pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options.RegexCache) if len(errs) > 0 { return false, errs } diff --git a/parameters/cookie_parameters_test.go b/parameters/cookie_parameters_test.go index 804d9e9..8014bdd 100644 --- a/parameters/cookie_parameters_test.go +++ b/parameters/cookie_parameters_test.go @@ -5,6 +5,7 @@ package parameters import ( "net/http" + "sync" "testing" "github.com/pb33f/libopenapi" @@ -690,7 +691,7 @@ paths: request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2500"}) // too many dude. // preset the path - path, _, pv := paths.FindPath(request, &m.Model) + path, _, pv := paths.FindPath(request, &m.Model, &sync.Map{}) valid, errors := v.ValidateCookieParamsWithPathItem(request, path, pv) @@ -720,7 +721,7 @@ paths: request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2500"}) // too many dude. // preset the path - path, _, pv := paths.FindPath(request, &m.Model) + path, _, pv := paths.FindPath(request, &m.Model, nil) valid, errors := v.ValidateCookieParamsWithPathItem(request, path, pv) diff --git a/parameters/header_parameters.go b/parameters/header_parameters.go index 7042efe..a4c56a1 100644 --- a/parameters/header_parameters.go +++ b/parameters/header_parameters.go @@ -21,7 +21,7 @@ import ( ) func (v *paramValidator) ValidateHeaderParams(request *http.Request) (bool, []*errors.ValidationError) { - pathItem, errs, foundPath := paths.FindPath(request, v.document) + pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options.RegexCache) if len(errs) > 0 { return false, errs } diff --git a/parameters/header_parameters_test.go b/parameters/header_parameters_test.go index b37f563..6c23967 100644 --- a/parameters/header_parameters_test.go +++ b/parameters/header_parameters_test.go @@ -5,6 +5,7 @@ package parameters import ( "net/http" + "sync" "testing" "github.com/pb33f/libopenapi" @@ -717,7 +718,7 @@ paths: request.Header.Set("coffeecups", "1200") // that's a lot of cups dude, we only have one dishwasher. // preset the path - path, _, pv := paths.FindPath(request, &m.Model) + path, _, pv := paths.FindPath(request, &m.Model, nil) valid, errors := v.ValidateHeaderParamsWithPathItem(request, path, pv) @@ -748,7 +749,7 @@ paths: request.Header.Set("coffeecups", "1200") // that's a lot of cups dude, we only have one dishwasher. // preset the path - path, _, pv := paths.FindPath(request, &m.Model) + path, _, pv := paths.FindPath(request, &m.Model, &sync.Map{}) valid, errors := v.ValidateHeaderParamsWithPathItem(request, path, pv) diff --git a/parameters/path_parameters.go b/parameters/path_parameters.go index 82ac29a..31f7e3c 100644 --- a/parameters/path_parameters.go +++ b/parameters/path_parameters.go @@ -21,7 +21,7 @@ import ( ) func (v *paramValidator) ValidatePathParams(request *http.Request) (bool, []*errors.ValidationError) { - pathItem, errs, foundPath := paths.FindPath(request, v.document) + pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options.RegexCache) if len(errs) > 0 { return false, errs } @@ -57,13 +57,29 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p continue } - r, err := helpers.GetRegexForPath(pathSegments[x]) - if err != nil { - continue + var rgx *regexp.Regexp + + if v.options.RegexCache != nil { + if cachedRegex, found := v.options.RegexCache.Load(pathSegments[x]); found { + rgx = cachedRegex.(*regexp.Regexp) + } + } + + if rgx == nil { + + r, err := helpers.GetRegexForPath(pathSegments[x]) + if err != nil { + continue + } + + rgx = r + + if v.options.RegexCache != nil { + v.options.RegexCache.Store(pathSegments[x], r) + } } - re := regexp.MustCompile(r.String()) - matches := re.FindStringSubmatch(submittedSegments[x]) + matches := rgx.FindStringSubmatch(submittedSegments[x]) matches = matches[1:] // Check if it is well-formed. diff --git a/parameters/path_parameters_test.go b/parameters/path_parameters_test.go index 8f3c7ef..a12a82b 100644 --- a/parameters/path_parameters_test.go +++ b/parameters/path_parameters_test.go @@ -5,12 +5,17 @@ package parameters import ( "net/http" + "regexp" + "sync" + "sync/atomic" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/pb33f/libopenapi-validator/config" + "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" ) @@ -2037,7 +2042,7 @@ paths: request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burgerId=22334/locate", nil) // preset the path - path, _, pv := paths.FindPath(request, &m.Model) + path, _, pv := paths.FindPath(request, &m.Model, nil) valid, errors := v.ValidatePathParamsWithPathItem(request, path, pv) @@ -2070,7 +2075,7 @@ paths: request, _ := http.NewRequest(http.MethodGet, "https://things.com/pizza/;burgerId=22334/locate", nil) // preset the path - path, _, pv := paths.FindPath(request, &m.Model) + path, _, pv := paths.FindPath(request, &m.Model, &sync.Map{}) valid, errors := v.ValidatePathParamsWithPathItem(request, path, pv) @@ -2242,3 +2247,136 @@ paths: assert.False(t, valid) assert.NotEmpty(t, errors) } + +type regexCacheWatcher struct { + inner *sync.Map + missCount int64 + hitCount int64 + storeCount int64 +} + +func (c *regexCacheWatcher) Load(key any) (value any, ok bool) { + data, found := c.inner.Load(key) + if found { + atomic.AddInt64(&c.hitCount, 1) + } else { + atomic.AddInt64(&c.missCount, 1) + } + + return data, found +} + +func (c *regexCacheWatcher) Store(key, value any) { + atomic.AddInt64(&c.storeCount, 1) + c.inner.Store(key, value) +} + +func TestNewValidator_CacheCompiledRegex(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /pizza: + get: + operationId: getPizza` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + + cache := ®exCacheWatcher{inner: &sync.Map{}} + v := NewParameterValidator(&m.Model, config.WithRegexCache(cache)) + + compiledPizza := regexp.MustCompile("^pizza$") + cache.inner.Store("pizza", compiledPizza) + + assert.EqualValues(t, 0, cache.storeCount) + assert.EqualValues(t, 0, cache.hitCount+cache.missCount) + + request, _ := http.NewRequest(http.MethodGet, "https://things.com/pizza", nil) + v.ValidatePathParams(request) + + assert.EqualValues(t, 0, cache.storeCount) + assert.EqualValues(t, 0, cache.missCount) + assert.EqualValues(t, 1, cache.hitCount) + + mapLength := 0 + + cache.inner.Range(func(key, value any) bool { + mapLength += 1 + return true + }) + + assert.Equal(t, 1, mapLength) + + cache.inner.Clear() + + v.ValidatePathParams(request) + + assert.EqualValues(t, 1, cache.storeCount) + assert.EqualValues(t, 1, cache.missCount) + assert.EqualValues(t, 1, cache.hitCount) +} + +func TestValidatePathParamsWithPathItem_RegexCache_WithOneCached(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /burgers/{burgerId}/locate: + parameters: + - in: path + name: burgerId + schema: + type: integer + get: + operationId: locateBurgers` + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() + + cache := ®exCacheWatcher{inner: &sync.Map{}} + + segment := "{burgerId}" + r, err := helpers.GetRegexForPath(segment) + require.NoError(t, err) + cache.inner.Store(segment, r) + + v := NewParameterValidator(&m.Model, config.WithRegexCache(cache)) + + request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/123/locate", nil) + + pathItem, _, foundPath := paths.FindPath(request, &m.Model, nil) + v.ValidatePathParamsWithPathItem(request, pathItem, foundPath) + + // Only "{burgerId}" regex was cached + assert.EqualValues(t, 2, cache.storeCount) // Stores "burgers" and "locate" regex + assert.EqualValues(t, 2, cache.missCount) + assert.EqualValues(t, 1, cache.hitCount) +} + +func TestValidatePathParamsWithPathItem_RegexCache_MissOnceThenHit(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /burgers/{burgerId}/locate: + parameters: + - in: path + name: burgerId + schema: + type: integer + get: + operationId: locateBurgers` + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() + + cache := ®exCacheWatcher{inner: &sync.Map{}} + + v := NewParameterValidator(&m.Model, config.WithRegexCache(cache)) + + request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/123/locate", nil) + pathItem, _, foundPath := paths.FindPath(request, &m.Model, cache) + + v.ValidatePathParamsWithPathItem(request, pathItem, foundPath) + + assert.EqualValues(t, 3, cache.storeCount) + assert.EqualValues(t, 3, cache.missCount) + assert.EqualValues(t, 3, cache.hitCount) + + _, found := cache.inner.Load("{burgerId}") + assert.True(t, found) +} diff --git a/parameters/query_parameters.go b/parameters/query_parameters.go index 98fbc8c..888cbc8 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -26,7 +26,7 @@ const rx = `[:\/\?#\[\]\@!\$&'\(\)\*\+,;=]` var rxRxp = regexp.MustCompile(rx) func (v *paramValidator) ValidateQueryParams(request *http.Request) (bool, []*errors.ValidationError) { - pathItem, errs, foundPath := paths.FindPath(request, v.document) + pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options.RegexCache) if len(errs) > 0 { return false, errs } diff --git a/parameters/query_parameters_test.go b/parameters/query_parameters_test.go index b25a32a..137969c 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "strings" + "sync" "testing" "github.com/pb33f/libopenapi" @@ -3028,7 +3029,7 @@ paths: "https://things.com/a/fishy/on/a/dishy?fishy[ocean]=atlantic&fishy[salt]=12", nil) // preset the path - path, _, pv := paths.FindPath(request, &m.Model) + path, _, pv := paths.FindPath(request, &m.Model, &sync.Map{}) valid, errors := v.ValidateQueryParamsWithPathItem(request, path, pv) assert.False(t, valid) @@ -3067,7 +3068,7 @@ paths: "https://things.com/a/beef/on/a/dishy?fishy[ocean]=atlantic&fishy[salt]=12", nil) // preset the path - path, _, pv := paths.FindPath(request, &m.Model) + path, _, pv := paths.FindPath(request, &m.Model, nil) valid, errors := v.ValidateQueryParamsWithPathItem(request, path, pv) assert.False(t, valid) diff --git a/parameters/validate_parameter.go b/parameters/validate_parameter.go index 12dbd83..bdd8034 100644 --- a/parameters/validate_parameter.go +++ b/parameters/validate_parameter.go @@ -37,13 +37,13 @@ func ValidateSingleParameterSchema( // Get the JSON Schema for the parameter definition. jsonSchema, err := buildJsonRender(schema) if err != nil { - return + return validationErrors } // Attempt to compile the JSON Schema jsch, err := helpers.NewCompiledSchema(name, jsonSchema, o) if err != nil { - return + return validationErrors } // Validate the object and report any errors. diff --git a/parameters/validate_parameter_test.go b/parameters/validate_parameter_test.go index 171b2e9..33e9616 100644 --- a/parameters/validate_parameter_test.go +++ b/parameters/validate_parameter_test.go @@ -2,6 +2,7 @@ package parameters import ( "net/http" + "sync" "testing" "github.com/pb33f/libopenapi-validator/config" @@ -575,3 +576,107 @@ func TestValidateParameterSchema_SchemaCompilationFailure(t *testing.T) { t.Logf("Schema compiled and validated successfully") } } + +func preparePathsBenchmark(b *testing.B, cache config.RegexCache) (ParameterValidator, *http.Request) { + bytes := []byte(`{ + "openapi": "3.0.0", + "info": { + "title": "API Spec With Complex Regex Pattern", + "version": "1.0.0" + }, + "paths": { + "/test/other/path": { + "get": {"responses": {"200": {"description": "test"}}} + }, + "/static/test/{imageName}": { + "get": {"responses": {"200": {"description": "test"}}} + }, + "/request/to/my/image.png": { + "get": {"responses": {"200": {"description": "test"}}} + }, + "/api/v2/{url}/{other}/{oncemore}/{url}": { + "get": {"responses": {"200": {"description": "test"}}} + }, + "/api/v1/{path}": { + "get": {"responses": {"200": {"description": "test"}}} + }, + "/each/url/{is}/{a_new_regex}": { + "get": {"responses": {"200": {"description": "test"}}} + }, + "/my-test/with-so-many/urls": { + "get": {"responses": {"200": {"description": "test"}}} + }, + "/test/other/path": { + "get": {"responses": {"200": {"description": "test"}}} + }, + "/api/endpoint/{address}/{domain}": { + "get": { + "summary": "API Endpoint with complex regex", + "parameters": [ + { + "name": "complexParam", + "in": "query", + "required": true, + "schema": { + "type": "string", + "pattern": "[\\w\\W]{1,1024}$" + } + } + ], + "responses": { + "200": { + "description": "Successful response" + } + } + } + } + } +}`) + + doc, err := libopenapi.NewDocument(bytes) + if err != nil { + b.Fatalf("error while creating open api spec document: %v", err) + } + + req, err := http.NewRequest("GET", "/api/endpoint/127.0.0.1/domain.com?complexParam=testvalue", nil) + if err != nil { + b.Fatalf("error while creating request: %v", err) + } + + v3Model, errs := doc.BuildV3Model() + if errs != nil { + b.Fatalf("error while building v3 model: %v", errs) + } + + validator := NewParameterValidator(&v3Model.Model, config.WithRegexCache(cache)) + + return validator, req +} + +func BenchmarkValidationWithoutCache(b *testing.B) { + validator, req := preparePathsBenchmark(b, nil) + + b.ResetTimer() + + for b.Loop() { + validator.ValidateHeaderParams(req) + validator.ValidateCookieParams(req) + validator.ValidateQueryParams(req) + validator.ValidateSecurity(req) + validator.ValidatePathParams(req) + } +} + +func BenchmarkValidationWithRegexCache(b *testing.B) { + validator, req := preparePathsBenchmark(b, &sync.Map{}) + + b.ResetTimer() + + for b.Loop() { + validator.ValidateHeaderParams(req) + validator.ValidateCookieParams(req) + validator.ValidateQueryParams(req) + validator.ValidateSecurity(req) + validator.ValidatePathParams(req) + } +} diff --git a/parameters/validate_security.go b/parameters/validate_security.go index 0fda56a..8135f74 100644 --- a/parameters/validate_security.go +++ b/parameters/validate_security.go @@ -18,7 +18,7 @@ import ( ) func (v *paramValidator) ValidateSecurity(request *http.Request) (bool, []*errors.ValidationError) { - pathItem, errs, foundPath := paths.FindPath(request, v.document) + pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options.RegexCache) if len(errs) > 0 { return false, errs } diff --git a/parameters/validate_security_test.go b/parameters/validate_security_test.go index 50e87fc..3957613 100644 --- a/parameters/validate_security_test.go +++ b/parameters/validate_security_test.go @@ -5,6 +5,7 @@ package parameters import ( "net/http" + "sync" "testing" "github.com/pb33f/libopenapi" @@ -358,7 +359,7 @@ paths: v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) - pathItem, errs, pv := paths.FindPath(request, &m.Model) + pathItem, errs, pv := paths.FindPath(request, &m.Model, nil) assert.Nil(t, errs) valid, errors := v.ValidateSecurityWithPathItem(request, pathItem, pv) @@ -380,7 +381,7 @@ paths: v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/beef", nil) - pathItem, _, pv := paths.FindPath(request, &m.Model) + pathItem, _, pv := paths.FindPath(request, &m.Model, &sync.Map{}) valid, errors := v.ValidateSecurityWithPathItem(request, pathItem, pv) assert.False(t, valid) @@ -643,7 +644,7 @@ components: v := NewParameterValidator(&m.Model, config.WithoutSecurityValidation()) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) - pathItem, errs, pv := paths.FindPath(request, &m.Model) + pathItem, errs, pv := paths.FindPath(request, &m.Model, &sync.Map{}) assert.Nil(t, errs) valid, errors := v.ValidateSecurityWithPathItem(request, pathItem, pv) diff --git a/paths/paths.go b/paths/paths.go index 4cb5871..0db194b 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -8,12 +8,14 @@ import ( "net/http" "net/url" "path/filepath" + "regexp" "strings" "github.com/pb33f/libopenapi/orderedmap" 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" ) @@ -23,7 +25,7 @@ import ( // that were picked up when locating the path. // The third return value will be the path that was found in the document, as it pertains to the contract, so all path // parameters will not have been replaced with their values from the request - allowing model lookups. -func FindPath(request *http.Request, document *v3.Document) (*v3.PathItem, []*errors.ValidationError, string) { +func FindPath(request *http.Request, document *v3.Document, regexCache config.RegexCache) (*v3.PathItem, []*errors.ValidationError, string) { basePaths := getBasePaths(document) stripped := StripRequestPath(request, document) @@ -51,7 +53,7 @@ func FindPath(request *http.Request, document *v3.Document) (*v3.PathItem, []*er segs = segs[1:] } - ok := comparePaths(segs, reqPathSegments, basePaths) + ok := comparePaths(segs, reqPathSegments, basePaths, regexCache) if !ok { continue } @@ -188,18 +190,35 @@ func stripBaseFromPath(path string, basePaths []string) string { return path } -func comparePaths(mapped, requested, basePaths []string) bool { +func comparePaths(mapped, requested, basePaths []string, regexCache config.RegexCache) bool { if len(mapped) != len(requested) { return false // short circuit out } var imploded []string for i, seg := range mapped { s := seg - r, err := helpers.GetRegexForPath(seg) - if err != nil { - return false + var rgx *regexp.Regexp + + if regexCache != nil { + if cachedRegex, found := regexCache.Load(s); found { + rgx = cachedRegex.(*regexp.Regexp) + } + } + + if rgx == nil { + r, err := helpers.GetRegexForPath(seg) + if err != nil { + return false + } + + rgx = r + + if regexCache != nil { + regexCache.Store(seg, r) + } } - if r.MatchString(requested[i]) { + + if rgx.MatchString(requested[i]) { s = requested[i] } imploded = append(imploded, s) diff --git a/paths/paths_test.go b/paths/paths_test.go index 69f6567..d7650c8 100644 --- a/paths/paths_test.go +++ b/paths/paths_test.go @@ -4,8 +4,11 @@ package paths import ( + "fmt" "net/http" "os" + "regexp" + "sync" "testing" "github.com/pb33f/libopenapi" @@ -21,7 +24,7 @@ func TestNewValidator_BadParam(t *testing.T) { m, _ := doc.BuildV3Model() - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) } @@ -32,7 +35,7 @@ func TestNewValidator_GoodParamFloat(t *testing.T) { doc, _ := libopenapi.NewDocument(b) m, _ := doc.BuildV3Model() - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) } @@ -43,7 +46,7 @@ func TestNewValidator_GoodParamInt(t *testing.T) { doc, _ := libopenapi.NewDocument(b) m, _ := doc.BuildV3Model() - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) } @@ -61,7 +64,7 @@ paths: request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/1,2,3,4,5/locate", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) } @@ -80,7 +83,7 @@ paths: request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/bish=bosh,wish=wash/locate", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, &sync.Map{}) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) } @@ -98,7 +101,7 @@ paths: m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/.1.2.3.4.5/locate", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) } @@ -112,7 +115,7 @@ func TestNewValidator_FindPathPost(t *testing.T) { request, _ := http.NewRequest(http.MethodPost, "https://things.com/pet/12334", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) } @@ -124,7 +127,7 @@ func TestNewValidator_FindPathDelete(t *testing.T) { m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodDelete, "https://things.com/pet/12334", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, &sync.Map{}) assert.NotNil(t, pathItem) } @@ -141,7 +144,7 @@ paths: request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/12345", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, &sync.Map{}) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) } @@ -159,7 +162,7 @@ paths: m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodOptions, "https://things.com/burgers/12345", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Options.OperationId) } @@ -177,7 +180,7 @@ paths: request, _ := http.NewRequest(http.MethodTrace, "https://things.com/burgers/12345", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, &sync.Map{}) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Trace.OperationId) } @@ -196,7 +199,7 @@ paths: request, _ := http.NewRequest(http.MethodPut, "https://things.com/burgers/12345", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, &sync.Map{}) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Put.OperationId) } @@ -214,7 +217,7 @@ paths: request, _ := http.NewRequest(http.MethodHead, "https://things.com/burgers/12345", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Head.OperationId) } @@ -236,19 +239,19 @@ paths: // check against base1 request, _ := http.NewRequest(http.MethodPost, "https://things.com/base1/user", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, &sync.Map{}) assert.NotNil(t, pathItem) assert.Equal(t, "addUser", pathItem.Post.OperationId) // check against base2 request, _ = http.NewRequest(http.MethodPost, "https://things.com/base2/user", nil) - pathItem, _, _ = FindPath(request, &m.Model) + pathItem, _, _ = FindPath(request, &m.Model, &sync.Map{}) assert.NotNil(t, pathItem) assert.Equal(t, "addUser", pathItem.Post.OperationId) // check against a deeper base request, _ = http.NewRequest(http.MethodPost, "https://things.com/base3/base4/base5/base6/user", nil) - pathItem, _, _ = FindPath(request, &m.Model) + pathItem, _, _ = FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "addUser", pathItem.Post.OperationId) } @@ -268,7 +271,7 @@ paths: // check against a deeper base request, _ := http.NewRequest(http.MethodPost, "https://things.com/base3/base4/base5/base6/user/1234/thing/abcd", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, &sync.Map{}) assert.NotNil(t, pathItem) assert.Equal(t, "addUser", pathItem.Post.OperationId) } @@ -287,7 +290,7 @@ paths: request, _ := http.NewRequest(http.MethodHead, "https://things.com/not/here", nil) - pathItem, errs, _ := FindPath(request, &m.Model) + pathItem, errs, _ := FindPath(request, &m.Model, nil) assert.Nil(t, pathItem) assert.NotNil(t, errs) assert.Equal(t, "HEAD Path '/not/here' not found", errs[0].Message) @@ -307,7 +310,7 @@ paths: request, _ := http.NewRequest(http.MethodPut, "https://things.com/burgers/12345", nil) - pathItem, errs, _ := FindPath(request, &m.Model) + pathItem, errs, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.NotNil(t, errs) assert.Equal(t, "PUT Path '/burgers/12345' not found", errs[0].Message) @@ -323,7 +326,7 @@ func TestNewValidator_GetLiteralMatch(t *testing.T) { m, _ := doc.BuildV3Model() - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 0) } @@ -337,7 +340,7 @@ func TestNewValidator_PostLiteralMatch(t *testing.T) { m, _ := doc.BuildV3Model() - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 0) } @@ -354,7 +357,7 @@ paths: request, _ := http.NewRequest(http.MethodPut, "https://things.com/pizza/burger", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, &sync.Map{}) assert.Len(t, errs, 0) } @@ -377,7 +380,7 @@ paths: request, _ := http.NewRequest(http.MethodPost, "https://things.com/pizza/1234", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, &sync.Map{}) assert.Len(t, errs, 1) assert.Equal(t, "POST Path '/pizza/1234' not found", errs[0].Message) @@ -401,7 +404,7 @@ paths: request, _ := http.NewRequest(http.MethodPost, "https://things.com/pizza/1234", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, &sync.Map{}) assert.Len(t, errs, 1) assert.Equal(t, "POST Path '/pizza/1234' not found", errs[0].Message) @@ -419,7 +422,7 @@ paths: request, _ := http.NewRequest(http.MethodPatch, "https://things.com/pizza/burger", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, &sync.Map{}) assert.Len(t, errs, 0) } @@ -442,7 +445,7 @@ paths: request, _ := http.NewRequest(http.MethodPost, "https://things.com/pizza/1234", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 1) assert.Equal(t, "POST Path '/pizza/1234' not found", errs[0].Message) @@ -460,7 +463,7 @@ paths: request, _ := http.NewRequest(http.MethodDelete, "https://things.com/pizza/burger", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 0) } @@ -477,7 +480,7 @@ paths: request, _ := http.NewRequest(http.MethodOptions, "https://things.com/pizza/burger", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, &sync.Map{}) assert.Len(t, errs, 0) } @@ -494,7 +497,7 @@ paths: request, _ := http.NewRequest(http.MethodHead, "https://things.com/pizza/burger", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 0) } @@ -511,7 +514,7 @@ paths: request, _ := http.NewRequest(http.MethodTrace, "https://things.com/pizza/burger", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, &sync.Map{}) assert.Len(t, errs, 0) } @@ -534,7 +537,7 @@ paths: request, _ := http.NewRequest(http.MethodPost, "https://things.com/pizza/1234", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 1) assert.Equal(t, "POST Path '/pizza/1234' not found", errs[0].Message) @@ -558,7 +561,7 @@ paths: request, _ := http.NewRequest(http.MethodPost, "https://things.com/pizza/1234", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 1) assert.Equal(t, "POST Path '/pizza/1234' not found", errs[0].Message) @@ -582,7 +585,7 @@ paths: request, _ := http.NewRequest(http.MethodPut, "https://things.com/pizza/1234", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, &sync.Map{}) assert.Len(t, errs, 1) assert.Equal(t, "PUT Path '/pizza/1234' not found", errs[0].Message) @@ -604,13 +607,13 @@ paths: request, _ := http.NewRequest(http.MethodPost, "https://things.com/hashy#one", nil) - pathItem, errs, _ := FindPath(request, &m.Model) + pathItem, errs, _ := FindPath(request, &m.Model, &sync.Map{}) assert.Len(t, errs, 0) assert.NotNil(t, pathItem) assert.Equal(t, "one", pathItem.Post.OperationId) request, _ = http.NewRequest(http.MethodPost, "https://things.com/hashy#two", nil) - pathItem, errs, _ = FindPath(request, &m.Model) + pathItem, errs, _ = FindPath(request, &m.Model, &sync.Map{}) assert.Len(t, errs, 0) assert.NotNil(t, pathItem) assert.Equal(t, "two", pathItem.Post.OperationId) @@ -634,7 +637,7 @@ paths: request, _ := http.NewRequest(http.MethodGet, "https://things.com/not_here", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 1) assert.Equal(t, "GET Path '/not_here' not found", errs[0].Message) } @@ -690,7 +693,7 @@ paths: request, _ := http.NewRequest(http.MethodPut, "https://things.com/something/pkg%3Agithub%2Frs%2Fzerolog%40v1.18.0", nil) - pathItem, errs, _ := FindPath(request, &m.Model) + pathItem, errs, _ := FindPath(request, &m.Model, nil) assert.Equal(t, 0, len(errs), "Errors found: %v", errs) assert.NotNil(t, pathItem) @@ -730,19 +733,72 @@ paths: request, _ := http.NewRequest(http.MethodGet, "https://things.com/entities('1')", nil) - pathItem, _, _ := FindPath(request, &m.Model) + pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "one", pathItem.Get.OperationId) request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='1234',ValidityEndDate=datetime'1492041600000')", nil) - pathItem, _, _ = FindPath(request, &m.Model) + pathItem, _, _ = FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "one", pathItem.Get.OperationId) request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='dummy',ValidityEndDate=datetime'1492041600000')", nil) - pathItem, _, _ = FindPath(request, &m.Model) + pathItem, _, _ = FindPath(request, &m.Model, nil) + assert.NotNil(t, pathItem) + assert.Equal(t, "one", pathItem.Get.OperationId) +} + +func TestNewValidator_ODataFormattedOpenAPISpecsWithRegexCache(t *testing.T) { + spec := `openapi: 3.0.0 +paths: + /entities('{Entity}'): + parameters: + - description: 'key: Entity' + in: path + name: Entity + required: true + schema: + type: integer + get: + operationId: one + /orders(RelationshipNumber='{RelationshipNumber}',ValidityEndDate=datetime'{ValidityEndDate}'): + parameters: + - name: RelationshipNumber + in: path + required: true + schema: + type: integer + - name: ValidityEndDate + in: path + required: true + schema: + type: string + get: + operationId: one +` + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + + request, _ := http.NewRequest(http.MethodGet, "https://things.com/entities('1')", nil) + + regexCache := &sync.Map{} + + pathItem, _, _ := FindPath(request, &m.Model, regexCache) + assert.NotNil(t, pathItem) + assert.Equal(t, "one", pathItem.Get.OperationId) + + request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='1234',ValidityEndDate=datetime'1492041600000')", nil) + + pathItem, _, _ = FindPath(request, &m.Model, regexCache) + assert.NotNil(t, pathItem) + assert.Equal(t, "one", pathItem.Get.OperationId) + + request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='dummy',ValidityEndDate=datetime'1492041600000')", nil) + + pathItem, _, _ = FindPath(request, &m.Model, regexCache) assert.NotNil(t, pathItem) assert.Equal(t, "one", pathItem.Get.OperationId) } @@ -766,6 +822,40 @@ paths: request, _ := http.NewRequest(http.MethodGet, "https://things.com/entities('1')", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model, &sync.Map{}) assert.NotEmpty(t, errs) } + +func TestNewValidator_FindPathWithRegexpCache(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /pizza/{sauce}/{fill}/hamburger/pizza: + head: + operationId: locateBurger` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() + + request, _ := http.NewRequest(http.MethodHead, "https://things.com/pizza/tomato/pepperoni/hamburger/pizza", nil) + + syncMap := sync.Map{} + + _, errs, _ := FindPath(request, &m.Model, &syncMap) + + keys := []string{} + addresses := make(map[string]bool) + + syncMap.Range(func(key, value any) bool { + keys = append(keys, key.(string)) + addresses[fmt.Sprintf("%p", value)] = true + return true + }) + + cached, found := syncMap.Load("pizza") + + assert.True(t, found) + assert.True(t, cached.(*regexp.Regexp).MatchString("pizza")) + assert.Len(t, errs, 0) + assert.Len(t, keys, 4) + assert.Len(t, addresses, 3) +} diff --git a/requests/validate_body.go b/requests/validate_body.go index a8c91b0..43cd553 100644 --- a/requests/validate_body.go +++ b/requests/validate_body.go @@ -20,7 +20,7 @@ import ( ) func (v *requestBodyValidator) ValidateRequestBody(request *http.Request) (bool, []*errors.ValidationError) { - pathItem, errs, foundPath := paths.FindPath(request, v.document) + pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options.RegexCache) if len(errs) > 0 { return false, errs } diff --git a/requests/validate_body_test.go b/requests/validate_body_test.go index b0414a8..6dca43a 100644 --- a/requests/validate_body_test.go +++ b/requests/validate_body_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "net/http" + "sync" "testing" "github.com/pb33f/libopenapi" @@ -355,7 +356,7 @@ paths: bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") - pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model) + pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model, nil) assert.Len(t, validationErrors, 0) request2, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/createBurger", @@ -451,7 +452,7 @@ paths: bytes.NewBuffer(bodyBytes)) request.Header.Set("content-type", "application/not-json") - pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model) + pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model, nil) assert.Len(t, validationErrors, 0) valid, errors := v.ValidateRequestBodyWithPathItem(request, pathItem, pathValue) @@ -494,7 +495,7 @@ paths: request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) - pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model) + pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model, &sync.Map{}) assert.Len(t, validationErrors, 0) valid, errors := v.ValidateRequestBodyWithPathItem(request, pathItem, pathValue) diff --git a/responses/validate_body.go b/responses/validate_body.go index 7d42bde..3d1d913 100644 --- a/responses/validate_body.go +++ b/responses/validate_body.go @@ -26,7 +26,7 @@ func (v *responseBodyValidator) ValidateResponseBody( request *http.Request, response *http.Response, ) (bool, []*errors.ValidationError) { - pathItem, errs, foundPath := paths.FindPath(request, v.document) + pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options.RegexCache) if len(errs) > 0 { return false, errs } diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go index ae3039c..17c3c6a 100644 --- a/responses/validate_body_test.go +++ b/responses/validate_body_test.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "sync" "testing" "github.com/pb33f/libopenapi" @@ -240,7 +241,7 @@ paths: request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // preset the path - path, _, pv := paths.FindPath(request, &m.Model) + path, _, pv := paths.FindPath(request, &m.Model, &sync.Map{}) // simulate a request/response res := httptest.NewRecorder() @@ -301,7 +302,7 @@ paths: request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // preset the path - path, _, pv := paths.FindPath(request, &m.Model) + path, _, pv := paths.FindPath(request, &m.Model, nil) // simulate a request/response res := httptest.NewRecorder() @@ -644,7 +645,7 @@ paths: response := res.Result() // preset the path - path, _, pv := paths.FindPath(request, &m.Model) + path, _, pv := paths.FindPath(request, &m.Model, &sync.Map{}) // validate! valid, errors := v.ValidateResponseBodyWithPathItem(request, response, path, pv) diff --git a/validator.go b/validator.go index 158a129..0ced185 100644 --- a/validator.go +++ b/validator.go @@ -138,7 +138,7 @@ func (v *validator) ValidateHttpResponse( var pathValue string var errs []*errors.ValidationError - pathItem, errs, pathValue = paths.FindPath(request, v.v3Model) + pathItem, errs, pathValue = paths.FindPath(request, v.v3Model, v.options.RegexCache) if pathItem == nil || errs != nil { return false, errs } @@ -162,7 +162,7 @@ func (v *validator) ValidateHttpRequestResponse( var pathValue string var errs []*errors.ValidationError - pathItem, errs, pathValue = paths.FindPath(request, v.v3Model) + pathItem, errs, pathValue = paths.FindPath(request, v.v3Model, v.options.RegexCache) if pathItem == nil || errs != nil { return false, errs } @@ -180,7 +180,7 @@ func (v *validator) ValidateHttpRequestResponse( } func (v *validator) ValidateHttpRequest(request *http.Request) (bool, []*errors.ValidationError) { - pathItem, errs, foundPath := paths.FindPath(request, v.v3Model) + pathItem, errs, foundPath := paths.FindPath(request, v.v3Model, v.options.RegexCache) if len(errs) > 0 { return false, errs } @@ -287,7 +287,7 @@ func (v *validator) ValidateHttpRequestWithPathItem(request *http.Request, pathI } func (v *validator) ValidateHttpRequestSync(request *http.Request) (bool, []*errors.ValidationError) { - pathItem, errs, foundPath := paths.FindPath(request, v.v3Model) + pathItem, errs, foundPath := paths.FindPath(request, v.v3Model, v.options.RegexCache) if len(errs) > 0 { return false, errs }