Skip to content

Commit e7cbb6e

Browse files
author
Michael Bonifacio
committed
merge origin/main and resolve conflicts
Made-with: Cursor
2 parents 442023b + b8be188 commit e7cbb6e

36 files changed

Lines changed: 3838 additions & 245 deletions

config/config.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,18 @@ type RegexCache interface {
2121
//
2222
// Generally fluent With... style functions are used to establish the desired behavior.
2323
type ValidationOptions struct {
24-
RegexEngine jsonschema.RegexpEngine
25-
RegexCache RegexCache // Enable compiled regex caching
26-
FormatAssertions bool
27-
ContentAssertions bool
28-
SecurityValidation bool
29-
OpenAPIMode bool // Enable OpenAPI-specific vocabulary validation
30-
AllowScalarCoercion bool // Enable string->boolean/number coercion
31-
Formats map[string]func(v any) error
32-
SchemaCache cache.SchemaCache // Optional cache for compiled schemas
33-
Logger *slog.Logger // Logger for debug/error output (nil = silent)
24+
RegexEngine jsonschema.RegexpEngine
25+
RegexCache RegexCache // Enable compiled regex caching
26+
FormatAssertions bool
27+
ContentAssertions bool
28+
SecurityValidation bool
29+
OpenAPIMode bool // Enable OpenAPI-specific vocabulary validation
30+
AllowScalarCoercion bool // Enable string->boolean/number coercion
31+
Formats map[string]func(v any) error
32+
SchemaCache cache.SchemaCache // Optional cache for compiled schemas
33+
Logger *slog.Logger // Logger for debug/error output (nil = silent)
34+
AllowXMLBodyValidation bool // Allows to convert XML to JSON for validating a request/response body.
35+
AllowURLEncodedBodyValidation bool // Allows to convert URL Encoded to JSON for validating a request/response body.
3436

3537
// strict mode options - detect undeclared properties even when additionalProperties: true
3638
StrictMode bool // Enable strict property validation
@@ -75,6 +77,8 @@ func WithExistingOpts(options *ValidationOptions) Option {
7577
o.Formats = options.Formats
7678
o.SchemaCache = options.SchemaCache
7779
o.Logger = options.Logger
80+
o.AllowXMLBodyValidation = options.AllowXMLBodyValidation
81+
o.AllowURLEncodedBodyValidation = options.AllowURLEncodedBodyValidation
7882
o.StrictMode = options.StrictMode
7983
o.StrictIgnorePaths = options.StrictIgnorePaths
8084
o.StrictIgnoredHeaders = options.StrictIgnoredHeaders
@@ -161,6 +165,22 @@ func WithScalarCoercion() Option {
161165
}
162166
}
163167

168+
// WithXmlBodyValidation enables converting an XML body to a JSON when validating the schema from a request and response body
169+
// The default option is set to false
170+
func WithXmlBodyValidation() Option {
171+
return func(o *ValidationOptions) {
172+
o.AllowXMLBodyValidation = true
173+
}
174+
}
175+
176+
// WithURLEncodedBodyValidation enables converting an URL Encoded body to a JSON when validating the schema from a request and response body
177+
// The default option is set to false
178+
func WithURLEncodedBodyValidation() Option {
179+
return func(o *ValidationOptions) {
180+
o.AllowURLEncodedBodyValidation = true
181+
}
182+
}
183+
164184
// WithSchemaCache sets a custom cache implementation or disables caching if nil.
165185
// Pass nil to disable schema caching and skip cache warming during validator initialization.
166186
// The default cache is a thread-safe sync.Map wrapper.

config/config_test.go

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ func TestNewValidationOptions_Defaults(t *testing.T) {
1919
assert.False(t, opts.FormatAssertions)
2020
assert.False(t, opts.ContentAssertions)
2121
assert.True(t, opts.SecurityValidation)
22-
assert.True(t, opts.OpenAPIMode) // Default is true
23-
assert.False(t, opts.AllowScalarCoercion) // Default is false
22+
assert.True(t, opts.OpenAPIMode) // Default is true
23+
assert.False(t, opts.AllowScalarCoercion) // Default is false
24+
assert.False(t, opts.AllowXMLBodyValidation) // Default is false
25+
assert.False(t, opts.AllowURLEncodedBodyValidation) // Default is false
2426
assert.Nil(t, opts.RegexEngine)
2527
assert.Nil(t, opts.RegexCache)
2628
}
@@ -32,8 +34,9 @@ func TestNewValidationOptions_WithNilOption(t *testing.T) {
3234
assert.False(t, opts.FormatAssertions)
3335
assert.False(t, opts.ContentAssertions)
3436
assert.True(t, opts.SecurityValidation)
35-
assert.True(t, opts.OpenAPIMode) // Default is true
36-
assert.False(t, opts.AllowScalarCoercion) // Default is false
37+
assert.True(t, opts.OpenAPIMode) // Default is true
38+
assert.False(t, opts.AllowScalarCoercion) // Default is false
39+
assert.False(t, opts.AllowXMLBodyValidation) // Default is false
3740
assert.Nil(t, opts.RegexEngine)
3841
assert.Nil(t, opts.RegexCache)
3942
}
@@ -44,8 +47,9 @@ func TestWithFormatAssertions(t *testing.T) {
4447
assert.True(t, opts.FormatAssertions)
4548
assert.False(t, opts.ContentAssertions)
4649
assert.True(t, opts.SecurityValidation)
47-
assert.True(t, opts.OpenAPIMode) // Default is true
48-
assert.False(t, opts.AllowScalarCoercion) // Default is false
50+
assert.True(t, opts.OpenAPIMode) // Default is true
51+
assert.False(t, opts.AllowScalarCoercion) // Default is false
52+
assert.False(t, opts.AllowXMLBodyValidation) // Default is false
4953
assert.Nil(t, opts.RegexEngine)
5054
assert.Nil(t, opts.RegexCache)
5155
}
@@ -56,8 +60,9 @@ func TestWithContentAssertions(t *testing.T) {
5660
assert.False(t, opts.FormatAssertions)
5761
assert.True(t, opts.ContentAssertions)
5862
assert.True(t, opts.SecurityValidation)
59-
assert.True(t, opts.OpenAPIMode) // Default is true
60-
assert.False(t, opts.AllowScalarCoercion) // Default is false
63+
assert.True(t, opts.OpenAPIMode) // Default is true
64+
assert.False(t, opts.AllowScalarCoercion) // Default is false
65+
assert.False(t, opts.AllowXMLBodyValidation) // Default is false
6166
assert.Nil(t, opts.RegexEngine)
6267
assert.Nil(t, opts.RegexCache)
6368
}
@@ -93,18 +98,22 @@ func TestWithExistingOpts(t *testing.T) {
9398
// Create original options with all settings enabled
9499
var testEngine jsonschema.RegexpEngine = nil
95100
original := &ValidationOptions{
96-
RegexEngine: testEngine,
97-
RegexCache: &sync.Map{},
98-
FormatAssertions: true,
99-
ContentAssertions: true,
100-
SecurityValidation: false,
101+
RegexEngine: testEngine,
102+
RegexCache: &sync.Map{},
103+
FormatAssertions: true,
104+
AllowXMLBodyValidation: true,
105+
AllowURLEncodedBodyValidation: true,
106+
ContentAssertions: true,
107+
SecurityValidation: false,
101108
}
102109

103110
// Create new options using existing options
104111
opts := NewValidationOptions(WithExistingOpts(original))
105112

106113
assert.Nil(t, opts.RegexEngine) // Both should be nil
107114
assert.NotNil(t, opts.RegexCache)
115+
assert.Equal(t, original.AllowXMLBodyValidation, opts.AllowXMLBodyValidation)
116+
assert.Equal(t, original.AllowURLEncodedBodyValidation, opts.AllowURLEncodedBodyValidation)
108117
assert.Equal(t, original.FormatAssertions, opts.FormatAssertions)
109118
assert.Equal(t, original.ContentAssertions, opts.ContentAssertions)
110119
assert.Equal(t, original.SecurityValidation, opts.SecurityValidation)
@@ -119,8 +128,9 @@ func TestWithExistingOpts_NilSource(t *testing.T) {
119128
assert.False(t, opts.FormatAssertions)
120129
assert.False(t, opts.ContentAssertions)
121130
assert.True(t, opts.SecurityValidation)
122-
assert.True(t, opts.OpenAPIMode) // Default is true
123-
assert.False(t, opts.AllowScalarCoercion) // Default is false
131+
assert.True(t, opts.OpenAPIMode) // Default is true
132+
assert.False(t, opts.AllowScalarCoercion) // Default is false
133+
assert.False(t, opts.AllowXMLBodyValidation) // Default is false
124134
assert.Nil(t, opts.RegexEngine)
125135
assert.Nil(t, opts.RegexCache)
126136
}
@@ -129,11 +139,13 @@ func TestMultipleOptions(t *testing.T) {
129139
opts := NewValidationOptions(
130140
WithFormatAssertions(),
131141
WithContentAssertions(),
142+
WithXmlBodyValidation(),
132143
)
133144

134145
assert.True(t, opts.FormatAssertions)
135146
assert.True(t, opts.ContentAssertions)
136147
assert.True(t, opts.SecurityValidation)
148+
assert.True(t, opts.AllowXMLBodyValidation)
137149
assert.True(t, opts.OpenAPIMode) // Default is true
138150
assert.False(t, opts.AllowScalarCoercion) // Default is false
139151
assert.Nil(t, opts.RegexEngine)
@@ -180,6 +192,14 @@ func TestWithExistingOpts_PartialOverride(t *testing.T) {
180192
assert.False(t, opts.SecurityValidation) // From original
181193
}
182194

195+
func TestWithUrlEncodedBodyValidation(t *testing.T) {
196+
opts := NewValidationOptions(
197+
WithURLEncodedBodyValidation(),
198+
)
199+
200+
assert.True(t, opts.AllowURLEncodedBodyValidation)
201+
}
202+
183203
func TestComplexScenario(t *testing.T) {
184204
// Test a complex real-world scenario
185205
var mockEngine jsonschema.RegexpEngine = nil

errors/parameters_howtofix.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,30 @@ const (
1212
HowToFixParamInvalidBoolean string = "Convert the value '%s' into a true/false value"
1313
HowToFixParamInvalidEnum string = "Instead of '%s', use one of the allowed values: '%s'"
1414
HowToFixParamInvalidFormEncode string = "Use a form style encoding for parameter values, for example: '%s'"
15+
HowToFixInvalidXml string = "Ensure xml is well-formed and matches schema structure"
16+
HowToFixXmlPrefix string = "Make sure to prepend the correct prefix '%s' to the declared fields"
17+
HowToFixXmlNamespace string = "Make sure to declare the 'xmlns:%s' with the correct namespace URI"
18+
HowToFixFormDataReservedCharacters string = "Make sure to correcly encode specials characters to percent encoding, or set allowReserved to true"
1519
HowToFixInvalidSchema string = "Ensure that the object being submitted, matches the schema correctly"
20+
HowToFixInvalidTypeEncoding string = "Ensure that the object being submitted matches the property encoding Content-Type"
1621
HowToFixParamInvalidSpaceDelimitedObjectExplode string = "When using 'explode' with space delimited parameters, " +
1722
"they should be separated by spaces. For example: '%s'"
1823
HowToFixParamInvalidPipeDelimitedObjectExplode string = "When using 'explode' with pipe delimited parameters, " +
1924
"they should be separated by pipes '|'. For example: '%s'"
2025
HowToFixParamInvalidDeepObjectMultipleValues string = "There can only be a single value per property name, " +
2126
"deepObject parameters should contain the property key in square brackets next to the parameter name. For example: '%s'"
22-
HowToFixInvalidJSON string = "The JSON submitted is invalid, please check the syntax"
23-
HowToFixDecodingError = "The object can't be decoded, so make sure it's being encoded correctly according to the spec."
24-
HowToFixInvalidContentType = "The content type is invalid, Use one of the %d supported types for this operation: %s"
25-
HowToFixInvalidResponseCode = "The service is responding with a code that is not defined in the spec, fix the service or add the code to the specification"
26-
HowToFixInvalidEncoding = "Ensure the correct encoding has been used on the object"
27-
HowToFixMissingValue = "Ensure the value has been set"
28-
HowToFixPath = "Check the path is correct, and check that the correct HTTP method has been used (e.g. GET, POST, PUT, DELETE)"
29-
HowToFixPathMethod = "Add the missing operation to the contract for the path"
30-
HowToFixInvalidMaxItems = "Reduce the number of items in the array to %d or less"
31-
HowToFixInvalidMinItems = "Increase the number of items in the array to %d or more"
32-
HowToFixMissingHeader = "Make sure the service responding sets the required headers with this response code"
27+
HowToFixInvalidJSON string = "The JSON submitted is invalid, please check the syntax"
28+
HowToFixInvalidUrlEncoded string = "Ensure URL Encoded submitted is well-formed and matches schema structure"
29+
HowToFixDecodingError string = "The object can't be decoded, so make sure it's being encoded correctly according to the spec."
30+
HowToFixInvalidContentType string = "The content type is invalid, Use one of the %d supported types for this operation: %s"
31+
HowToFixInvalidResponseCode string = "The service is responding with a code that is not defined in the spec, fix the service or add the code to the specification"
32+
HowToFixInvalidEncoding string = "Ensure the correct encoding has been used on the object"
33+
HowToFixMissingValue string = "Ensure the value has been set"
34+
HowToFixPath string = "Check the path is correct, and check that the correct HTTP method has been used (e.g. GET, POST, PUT, DELETE)"
35+
HowToFixPathMethod string = "Add the missing operation to the contract for the path"
36+
HowToFixInvalidMaxItems string = "Reduce the number of items in the array to %d or less"
37+
HowToFixInvalidMinItems string = "Increase the number of items in the array to %d or more"
38+
HowToFixMissingHeader string = "Make sure the service responding sets the required headers with this response code"
39+
HowToFixInvalidRenderedSchema string = "Check the request schema for circular references or invalid structures"
40+
HowToFixInvalidJsonSchema string = "Check the request schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs"
3341
)

errors/urlencoded_errors.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package errors
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/pb33f/libopenapi-validator/helpers"
7+
"github.com/pb33f/libopenapi/datamodel/high/base"
8+
)
9+
10+
func InvalidURLEncodedParsing(reason, referenceObject string) *ValidationError {
11+
return &ValidationError{
12+
ValidationType: helpers.URLEncodedValidation,
13+
ValidationSubType: helpers.Schema,
14+
Message: "Unable to parse form-urlencoded body",
15+
Reason: fmt.Sprintf("failed to parse form-urlencoded: %s", reason),
16+
SchemaValidationErrors: []*SchemaValidationFailure{{
17+
Reason: reason,
18+
ReferenceSchema: "",
19+
ReferenceObject: referenceObject,
20+
}},
21+
HowToFix: HowToFixInvalidUrlEncoded,
22+
}
23+
}
24+
25+
func InvalidTypeEncoding(schema *base.Schema, name, contentType string) *ValidationError {
26+
line := 1
27+
col := 0
28+
if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil {
29+
line = low.Type.KeyNode.Line
30+
col = low.Type.KeyNode.Column
31+
}
32+
33+
return &ValidationError{
34+
ValidationType: helpers.URLEncodedValidation,
35+
ValidationSubType: helpers.InvalidTypeEncoding,
36+
Message: fmt.Sprintf("The value '%s' could not be parsed to the defined encoding", name),
37+
Reason: fmt.Sprintf("The value '%s' is encoded as '%s' in the schema, however the value could not be parsed", name, contentType),
38+
SpecLine: line,
39+
SpecCol: col,
40+
Context: schema,
41+
HowToFix: HowToFixInvalidTypeEncoding,
42+
}
43+
}
44+
45+
func ReservedURLEncodedValue(schema *base.Schema, name, value string) *ValidationError {
46+
line := 1
47+
col := 0
48+
if schema != nil {
49+
if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil {
50+
line = low.Type.KeyNode.Line
51+
col = low.Type.KeyNode.Column
52+
}
53+
}
54+
55+
return &ValidationError{
56+
ValidationType: helpers.URLEncodedValidation,
57+
ValidationSubType: helpers.ReservedValues,
58+
Message: fmt.Sprintf("Form value '%s' contains reserved characters", name),
59+
Reason: fmt.Sprintf("The form value '%s' contains reserved characters but allowReserved is false. Value: '%s'", name, value),
60+
SpecLine: line,
61+
SpecCol: col,
62+
Context: schema,
63+
HowToFix: HowToFixFormDataReservedCharacters,
64+
}
65+
}

errors/urlencoded_errors_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package errors
2+
3+
import (
4+
"testing"
5+
6+
"github.com/pb33f/libopenapi"
7+
"github.com/pb33f/libopenapi-validator/helpers"
8+
"github.com/pb33f/libopenapi/datamodel/high/base"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func getURLEncodingTestSchema() *base.Schema {
13+
spec := `openapi: 3.0.0
14+
paths:
15+
/pet:
16+
get:
17+
responses:
18+
'200':
19+
content:
20+
application/x-www-form-urlencoded:
21+
encoding:
22+
animal:
23+
contentType: application/json
24+
schema:
25+
type: object
26+
properties:
27+
animal:
28+
type: object`
29+
30+
doc, _ := libopenapi.NewDocument([]byte(spec))
31+
v3Doc, _ := doc.BuildV3Model()
32+
33+
return v3Doc.Model.Paths.PathItems.GetOrZero("/pet").Get.Responses.Codes.GetOrZero("200").
34+
Content.GetOrZero("application/x-www-form-urlencoded").Schema.Schema()
35+
}
36+
37+
func TestInvalidURLEncodedParsing(t *testing.T) {
38+
err := InvalidURLEncodedParsing("no data sent", "invalid-formdata")
39+
40+
assert.NotNil(t, (*err))
41+
assert.Equal(t, (*err).SchemaValidationErrors[0].Reason, "no data sent")
42+
assert.Equal(t, (*err).SchemaValidationErrors[0].ReferenceObject, "invalid-formdata")
43+
assert.Equal(t, helpers.Schema, (*err).ValidationSubType)
44+
}
45+
46+
func TestInvalidTypeEncoding(t *testing.T) {
47+
err := InvalidTypeEncoding(getURLEncodingTestSchema(), "animal", helpers.JSONContentType)
48+
49+
assert.NotNil(t, (*err))
50+
assert.Equal(t, helpers.InvalidTypeEncoding, (*err).ValidationSubType)
51+
}
52+
53+
func TestReservedURLEncodedValue(t *testing.T) {
54+
err := ReservedURLEncodedValue(getURLEncodingTestSchema(), "animal", "!")
55+
56+
assert.NotNil(t, (*err))
57+
assert.Equal(t, helpers.ReservedValues, (*err).ValidationSubType)
58+
}

0 commit comments

Comments
 (0)