Skip to content

Commit 13f18e4

Browse files
emilien-pugetdaveshanley
authored andcommitted
query param validation
1 parent 657229e commit 13f18e4

File tree

3 files changed

+167
-65
lines changed

3 files changed

+167
-65
lines changed

parameters/query_parameters.go

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -108,43 +108,15 @@ doneLooking:
108108
switch ty {
109109

110110
case helpers.String:
111-
112-
// check if the param is within an enum
113-
if sch.Enum != nil {
114-
matchFound := false
115-
for _, enumVal := range sch.Enum {
116-
if strings.TrimSpace(ef) == fmt.Sprint(enumVal.Value) {
117-
matchFound = true
118-
break
119-
}
120-
}
121-
if !matchFound {
122-
validationErrors = append(validationErrors,
123-
errors.IncorrectQueryParamEnum(params[p], ef, sch))
124-
}
125-
}
126-
111+
validationErrors = v.validateSimpleParam(sch, ef, ef, params[p])
127112
case helpers.Integer, helpers.Number:
128-
if _, err := strconv.ParseFloat(ef, 64); err != nil {
113+
efF, err := strconv.ParseFloat(ef, 64)
114+
if err != nil {
129115
validationErrors = append(validationErrors,
130116
errors.InvalidQueryParamNumber(params[p], ef, sch))
131117
break
132118
}
133-
// check if the param is within an enum
134-
if sch.Enum != nil {
135-
matchFound := false
136-
for _, enumVal := range sch.Enum {
137-
if strings.TrimSpace(ef) == fmt.Sprint(enumVal.Value) {
138-
matchFound = true
139-
break
140-
}
141-
}
142-
if !matchFound {
143-
validationErrors = append(validationErrors,
144-
errors.IncorrectQueryParamEnum(params[p], ef, sch))
145-
}
146-
}
147-
119+
validationErrors = v.validateSimpleParam(sch, ef, efF, params[p])
148120
case helpers.Boolean:
149121
if _, err := strconv.ParseBool(ef); err != nil {
150122
validationErrors = append(validationErrors,
@@ -245,3 +217,29 @@ doneLooking:
245217
}
246218
return true, nil
247219
}
220+
221+
func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string, parsedParam any, parameter *v3.Parameter) (validationErrors []*errors.ValidationError) {
222+
// check if the param is within an enum
223+
if sch.Enum != nil {
224+
matchFound := false
225+
for _, enumVal := range sch.Enum {
226+
if strings.TrimSpace(rawParam) == fmt.Sprint(enumVal.Value) {
227+
matchFound = true
228+
break
229+
}
230+
}
231+
if !matchFound {
232+
return []*errors.ValidationError{errors.IncorrectQueryParamEnum(parameter, rawParam, sch)}
233+
}
234+
}
235+
236+
return ValidateSingleParameterSchema(
237+
sch,
238+
parsedParam,
239+
"Query parameter",
240+
"The query parameter",
241+
parameter.Name,
242+
helpers.ParameterValidation,
243+
helpers.ParameterValidationQuery,
244+
)
245+
}

parameters/query_parameters_test.go

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/pb33f/libopenapi"
1111
"github.com/pb33f/libopenapi-validator/paths"
1212
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
1314
)
1415

1516
func TestNewValidator_QueryParamMissing(t *testing.T) {
@@ -68,6 +69,36 @@ paths:
6869
assert.Nil(t, errors)
6970
}
7071

72+
func TestNewValidator_QueryParamMinimum(t *testing.T) {
73+
spec := `openapi: 3.1.0
74+
paths:
75+
/a/fishy/on/a/dishy:
76+
get:
77+
parameters:
78+
- name: fishy
79+
in: query
80+
required: true
81+
schema:
82+
type: string
83+
minLength: 4
84+
operationId: locateFishy
85+
`
86+
87+
doc, err := libopenapi.NewDocument([]byte(spec))
88+
require.NoError(t, err)
89+
m, errs := doc.BuildV3Model()
90+
require.Len(t, errs, 0)
91+
92+
v := NewParameterValidator(&m.Model)
93+
94+
request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil)
95+
96+
valid, errors := v.ValidateQueryParams(request)
97+
assert.False(t, valid)
98+
assert.Equal(t, 1, len(errors))
99+
assert.Equal(t, "Query parameter 'fishy' failed to validate", errors[0].Message)
100+
}
101+
71102
func TestNewValidator_QueryParamPost(t *testing.T) {
72103
spec := `openapi: 3.1.0
73104
paths:
@@ -348,6 +379,36 @@ paths:
348379
assert.Nil(t, errors)
349380
}
350381

382+
func TestNewValidator_QueryParamMinimumNumber(t *testing.T) {
383+
spec := `openapi: 3.1.0
384+
paths:
385+
/a/fishy/on/a/dishy:
386+
get:
387+
parameters:
388+
- name: fishy
389+
in: query
390+
required: true
391+
schema:
392+
type: number
393+
minimum: 200
394+
operationId: locateFishy
395+
`
396+
397+
doc, err := libopenapi.NewDocument([]byte(spec))
398+
require.NoError(t, err)
399+
m, errs := doc.BuildV3Model()
400+
require.Len(t, errs, 0)
401+
402+
v := NewParameterValidator(&m.Model)
403+
404+
request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=123", nil)
405+
406+
valid, errors := v.ValidateQueryParams(request)
407+
assert.False(t, valid)
408+
assert.Equal(t, 1, len(errors))
409+
assert.Equal(t, "Query parameter 'fishy' failed to validate", errors[0].Message)
410+
}
411+
351412
func TestNewValidator_QueryParamValidTypeFloat(t *testing.T) {
352413
spec := `openapi: 3.1.0
353414
paths:
@@ -2533,7 +2594,7 @@ components:
25332594
doc, _ := libopenapi.NewDocument([]byte(spec))
25342595

25352596
m, err := doc.BuildV3Model()
2536-
assert.Len(t, err, 0) //no patch build here
2597+
assert.Len(t, err, 0) // no patch build here
25372598

25382599
v := NewParameterValidator(&m.Model)
25392600

parameters/validate_parameter.go

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,52 @@ package parameters
55

66
import (
77
"encoding/json"
8+
stdError "errors"
89
"fmt"
10+
"net/url"
11+
"reflect"
12+
"strings"
13+
914
"github.com/pb33f/libopenapi-validator/errors"
1015
"github.com/pb33f/libopenapi/datamodel/high/base"
1116
"github.com/pb33f/libopenapi/utils"
1217
"github.com/santhosh-tekuri/jsonschema/v5"
13-
"net/url"
14-
"reflect"
15-
"strings"
1618
)
1719

20+
func ValidateSingleParameterSchema(
21+
schema *base.Schema,
22+
rawObject any,
23+
entity string,
24+
reasonEntity string,
25+
name string,
26+
validationType string,
27+
subValType string,
28+
) (validationErrors []*errors.ValidationError) {
29+
jsch := compileSchema(name, buildJsonRender(schema))
30+
31+
scErrs := jsch.Validate(rawObject)
32+
var werras *jsonschema.ValidationError
33+
if stdError.As(scErrs, &werras) {
34+
validationErrors = formatJsonSchemaValidationError(schema, werras, entity, reasonEntity, name, validationType, subValType)
35+
}
36+
return validationErrors
37+
}
38+
39+
// compileSchema create a new json schema compiler and add the schema to it.
40+
func compileSchema(name string, jsonSchema []byte) *jsonschema.Schema {
41+
compiler := jsonschema.NewCompiler()
42+
_ = compiler.AddResource(fmt.Sprintf("%s.json", name), strings.NewReader(string(jsonSchema)))
43+
jsch, _ := compiler.Compile(fmt.Sprintf("%s.json", name))
44+
return jsch
45+
}
46+
47+
// buildJsonRender build a JSON render of the schema.
48+
func buildJsonRender(schema *base.Schema) []byte {
49+
renderedSchema, _ := schema.Render()
50+
jsonSchema, _ := utils.ConvertYAMLtoJSON(renderedSchema)
51+
return jsonSchema
52+
}
53+
1854
// ValidateParameterSchema will validate a parameter against a raw object, or a blob of json/yaml.
1955
// It will return a list of validation errors, if any.
2056
//
@@ -108,35 +144,9 @@ func ValidateParameterSchema(
108144
}
109145
}
110146
}
111-
if scErrs != nil {
112-
jk := scErrs.(*jsonschema.ValidationError)
113-
114-
// flatten the validationErrors
115-
schFlatErrs := jk.BasicOutput().Errors
116-
var schemaValidationErrors []*errors.SchemaValidationFailure
117-
for q := range schFlatErrs {
118-
er := schFlatErrs[q]
119-
if er.KeywordLocation == "" || strings.HasPrefix(er.Error, "doesn't validate with") {
120-
continue // ignore this error, it's not useful
121-
}
122-
schemaValidationErrors = append(schemaValidationErrors, &errors.SchemaValidationFailure{
123-
Reason: er.Error,
124-
Location: er.KeywordLocation,
125-
OriginalError: jk,
126-
})
127-
}
128-
// add the error to the list
129-
validationErrors = append(validationErrors, &errors.ValidationError{
130-
ValidationType: validationType,
131-
ValidationSubType: subValType,
132-
Message: fmt.Sprintf("%s '%s' failed to validate", entity, name),
133-
Reason: fmt.Sprintf("%s '%s' is defined as an object, "+
134-
"however it failed to pass a schema validation", reasonEntity, name),
135-
SpecLine: schema.GoLow().Type.KeyNode.Line,
136-
SpecCol: schema.GoLow().Type.KeyNode.Column,
137-
SchemaValidationErrors: schemaValidationErrors,
138-
HowToFix: errors.HowToFixInvalidSchema,
139-
})
147+
var werras *jsonschema.ValidationError
148+
if stdError.As(scErrs, &werras) {
149+
validationErrors = formatJsonSchemaValidationError(schema, werras, entity, reasonEntity, name, validationType, subValType)
140150
}
141151

142152
// if there are no validationErrors, check that the supplied value is even JSON
@@ -159,3 +169,36 @@ func ValidateParameterSchema(
159169
}
160170
return validationErrors
161171
}
172+
173+
func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.ValidationError, entity string, reasonEntity string, name string, validationType string, subValType string) (validationErrors []*errors.ValidationError) {
174+
// flatten the validationErrors
175+
schFlatErrs := scErrs.BasicOutput().Errors
176+
var schemaValidationErrors []*errors.SchemaValidationFailure
177+
for q := range schFlatErrs {
178+
er := schFlatErrs[q]
179+
if er.KeywordLocation == "" || strings.HasPrefix(er.Error, "doesn't validate with") {
180+
continue // ignore this error, it's not useful
181+
}
182+
schemaValidationErrors = append(schemaValidationErrors, &errors.SchemaValidationFailure{
183+
Reason: er.Error,
184+
Location: er.KeywordLocation,
185+
OriginalError: scErrs,
186+
})
187+
}
188+
schemaType := "undefined"
189+
if len(schema.Type) > 0 {
190+
schemaType = schema.Type[0]
191+
}
192+
validationErrors = append(validationErrors, &errors.ValidationError{
193+
ValidationType: validationType,
194+
ValidationSubType: subValType,
195+
Message: fmt.Sprintf("%s '%s' failed to validate", entity, name),
196+
Reason: fmt.Sprintf("%s '%s' is defined as an %s, "+
197+
"however it failed to pass a schema validation", reasonEntity, name, schemaType),
198+
SpecLine: schema.GoLow().Type.KeyNode.Line,
199+
SpecCol: schema.GoLow().Type.KeyNode.Column,
200+
SchemaValidationErrors: schemaValidationErrors,
201+
HowToFix: errors.HowToFixInvalidSchema,
202+
})
203+
return validationErrors
204+
}

0 commit comments

Comments
 (0)