diff --git a/openapi3filter/issue625_test.go b/openapi3filter/issue625_test.go new file mode 100644 index 000000000..f00940b26 --- /dev/null +++ b/openapi3filter/issue625_test.go @@ -0,0 +1,123 @@ +package openapi3filter_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestIssue625(t *testing.T) { + + anyOfArraySpec := ` +openapi: 3.0.0 +info: + version: 1.0.0 + title: Sample API +paths: + /items: + get: + description: Returns a list of stuff + parameters: + - description: test object + explode: false + in: query + name: test + required: false + schema: + type: array + items: + anyOf: + - type: integer + - type: boolean + responses: + '200': + description: Successful response +`[1:] + + oneOfArraySpec := strings.ReplaceAll(anyOfArraySpec, "anyOf", "oneOf") + + allOfArraySpec := strings.ReplaceAll(strings.ReplaceAll(anyOfArraySpec, "anyOf", "allOf"), + "type: boolean", "type: number") + + tests := []struct { + name string + spec string + req string + errStr string + }{ + { + name: "success anyof object array", + spec: anyOfArraySpec, + req: "/items?test=3,7", + }, + { + name: "failed anyof object array", + spec: anyOfArraySpec, + req: "/items?test=s1,s2", + errStr: `parameter "test" in query has an error: path 0: value s1: an invalid boolean: invalid syntax`, + }, + + { + name: "success allof object array", + spec: allOfArraySpec, + req: `/items?test=1,3`, + }, + { + name: "failed allof object array", + spec: allOfArraySpec, + req: `/items?test=1.2,3.1`, + errStr: `parameter "test" in query has an error: Value must be an integer`, + }, + { + name: "success oneof object array", + spec: oneOfArraySpec, + req: `/items?test=true,3`, + }, + { + name: "faled oneof object array", + spec: oneOfArraySpec, + req: `/items?test="val1","val2"`, + errStr: `parameter "test" in query has an error: item 0: decoding oneOf failed: 0 schemas matched`, + }, + } + + for _, testcase := range tests { + t.Run(testcase.name, func(t *testing.T) { + loader := openapi3.NewLoader() + ctx := loader.Context + + doc, err := loader.LoadFromData([]byte(testcase.spec)) + require.NoError(t, err) + + err = doc.Validate(ctx) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + httpReq, err := http.NewRequest(http.MethodGet, testcase.req, nil) + require.NoError(t, err) + + route, pathParams, err := router.FindRoute(httpReq) + require.NoError(t, err) + + requestValidationInput := &openapi3filter.RequestValidationInput{ + Request: httpReq, + PathParams: pathParams, + Route: route, + } + err = openapi3filter.ValidateRequest(ctx, requestValidationInput) + if testcase.errStr == "" { + require.NoError(t, err) + } else { + require.Contains(t, err.Error(), testcase.errStr) + } + }, + ) + } +} diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index ca8194432..300b2705f 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -526,10 +526,92 @@ func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationM } values = strings.Split(values[0], delim) } - val, err := parseArray(values, schema) + val, err := d.parseArray(values, sm, schema) return val, ok, err } +// parseArray returns an array that contains items from a raw array. +// Every item is parsed as a primitive value. +// The function returns an error when an error happened while parse array's items. +func (d *urlValuesDecoder) parseArray(raw []string, sm *openapi3.SerializationMethod, schemaRef *openapi3.SchemaRef) ([]interface{}, error) { + var value []interface{} + + for i, v := range raw { + item, err := d.parseValue(v, schemaRef.Value.Items) + if err != nil { + if v, ok := err.(*ParseError); ok { + return nil, &ParseError{path: []interface{}{i}, Cause: v} + } + return nil, fmt.Errorf("item %d: %w", i, err) + } + + // If the items are nil, then the array is nil. There shouldn't be case where some values are actual primitive + // values and some are nil values. + if item == nil { + return nil, nil + } + value = append(value, item) + } + return value, nil +} + +func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (interface{}, error) { + if len(schema.Value.AllOf) > 0 { + var value interface{} + var err error + for _, sr := range schema.Value.AllOf { + value, err = d.parseValue(v, sr) + if value == nil || err != nil { + break + } + } + return value, err + } + + if len(schema.Value.AnyOf) > 0 { + var value interface{} + var err error + for _, sr := range schema.Value.AnyOf { + value, err = d.parseValue(v, sr) + if err == nil { + return value, nil + } + } + + return nil, err + } + + if len(schema.Value.OneOf) > 0 { + isMatched := 0 + var value interface{} + var err error + for _, sr := range schema.Value.OneOf { + result, err := d.parseValue(v, sr) + if err == nil { + value = result + isMatched++ + } + } + if isMatched == 1 { + return value, nil + } else if isMatched > 1 { + return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched) + } else if isMatched == 0 { + return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched) + } + + return nil, err + } + + if schema.Value.Not != nil { + // TODO(decode not): handle decoding "not" JSON Schema + return nil, errors.New("not implemented: decoding 'not'") + } + + return parsePrimitive(v, schema) + +} + func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { var propsFn func(url.Values) (map[string]string, error) switch sm.Style {