diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index 550b18ce..b4dbdb22 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -51,7 +51,7 @@ jobs: needs: - draft-release env: - X_GO_DISTRIBUTION: "https://go.dev/dl/go1.23.6.linux-amd64.tar.gz" + X_GO_DISTRIBUTION: "https://go.dev/dl/go1.23.7.linux-amd64.tar.gz" APIFIREWALL_NAMESPACE: "github.com/wallarm/api-firewall" strategy: matrix: @@ -162,7 +162,7 @@ jobs: needs: - draft-release env: - X_GO_VERSION: "1.23.6" + X_GO_VERSION: "1.23.7" APIFIREWALL_NAMESPACE: "github.com/wallarm/api-firewall" strategy: matrix: @@ -272,19 +272,19 @@ jobs: include: - arch: armv6 distro: bullseye - go_distribution: https://go.dev/dl/go1.23.6.linux-armv6l.tar.gz + go_distribution: https://go.dev/dl/go1.23.7.linux-armv6l.tar.gz artifact: armv6-libc - arch: aarch64 distro: bullseye - go_distribution: https://go.dev/dl/go1.23.6.linux-arm64.tar.gz + go_distribution: https://go.dev/dl/go1.23.7.linux-arm64.tar.gz artifact: arm64-libc - arch: armv6 distro: alpine_latest - go_distribution: https://go.dev/dl/go1.23.6.linux-armv6l.tar.gz + go_distribution: https://go.dev/dl/go1.23.7.linux-armv6l.tar.gz artifact: armv6-musl - arch: aarch64 distro: alpine_latest - go_distribution: https://go.dev/dl/go1.23.6.linux-arm64.tar.gz + go_distribution: https://go.dev/dl/go1.23.7.linux-arm64.tar.gz artifact: arm64-musl steps: - uses: actions/checkout@v2.1.0 diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index a0d4ae5b..a1e06efd 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -23,22 +23,20 @@ jobs: runs-on: "ubuntu-20.04" steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build an image from Dockerfile run: | docker build -t wallarm/api-firewall:${{ github.sha }} . - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@7b7aa264d83dc58691451798b4d117d53d21edfe + uses: aquasecurity/trivy-action@0.28.0 with: image-ref: 'wallarm/api-firewall:${{ github.sha }}' - format: 'template' - template: '@/contrib/sarif.tpl' + format: 'sarif' output: 'trivy-results.sarif' - severity: 'CRITICAL,HIGH' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' diff --git a/Makefile b/Makefile index 5e8587da..01bc5632 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION := 0.8.8 +VERSION := 0.8.9 NAMESPACE := github.com/wallarm/api-firewall .DEFAULT_GOAL := build @@ -39,10 +39,10 @@ vulncheck: govulncheck ./... stop_k6_tests: - @docker-compose -f resources/test/docker-compose-api-mode.yml down + @docker compose -f resources/test/docker-compose-api-mode.yml down run_k6_tests: stop_k6_tests - @docker-compose -f resources/test/docker-compose-api-mode.yml up --build --detach --force-recreate + @docker compose -f resources/test/docker-compose-api-mode.yml up --build --detach --force-recreate docker run --rm -i --network host grafana/k6 run -v - 0 { @@ -119,7 +119,7 @@ func invalidSerializationMethodErr(sm *openapi3.SerializationMethod) error { // Decodes a parameter defined via the content property as an object. It uses // the user specified decoder, or our build-in decoder for application/json func decodeContentParameter(param *openapi3.Parameter, input *openapi3filter.RequestValidationInput) ( - value interface{}, schema *openapi3.Schema, found bool, err error) { + value any, schema *openapi3.Schema, found bool, err error) { var paramValues []string switch param.In { @@ -167,7 +167,7 @@ func decodeContentParameter(param *openapi3.Parameter, input *openapi3filter.Req } func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) ( - outValue interface{}, outSchema *openapi3.Schema, err error) { + outValue any, outSchema *openapi3.Schema, err error) { // Only query parameters can have multiple values. if len(values) > 1 && param.In != openapi3.ParameterInQuery { err = fmt.Errorf("%s parameter %q cannot have multiple values", param.In, param.Name) @@ -193,7 +193,7 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) } outSchema = mt.Schema.Value - unmarshal := func(encoded string, paramSchema *openapi3.SchemaRef) (decoded interface{}, err error) { + unmarshal := func(encoded string, paramSchema *openapi3.SchemaRef) (decoded any, err error) { if err = json.Unmarshal([]byte(encoded), &decoded); err != nil { if paramSchema != nil && !paramSchema.Value.Type.Is("object") { decoded, err = encoded, nil @@ -208,9 +208,9 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) return } } else { - outArray := make([]interface{}, 0, len(values)) + outArray := make([]any, 0, len(values)) for _, v := range values { - var item interface{} + var item any if item, err = unmarshal(v, outSchema.Items); err != nil { err = fmt.Errorf("error unmarshaling parameter %q", param.Name) return @@ -223,15 +223,15 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) } type valueDecoder interface { - DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) - DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) - DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) + DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) + DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]any, bool, error) + DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]any, bool, error) } // decodeStyledParameter returns a value of an operation's parameter from HTTP request for // parameters defined using the style format, and whether the parameter is supplied in the input. // The function returns ParseError when HTTP request contains an invalid value of a parameter. -func decodeStyledParameter(param *openapi3.Parameter, input *openapi3filter.RequestValidationInput) (interface{}, bool, error) { +func decodeStyledParameter(param *openapi3.Parameter, input *openapi3filter.RequestValidationInput) (any, bool, error) { sm, err := param.SerializationMethod() if err != nil { return nil, false, err @@ -261,11 +261,11 @@ func decodeStyledParameter(param *openapi3.Parameter, input *openapi3filter.Requ return decodeValue(dec, param.Name, sm, param.Schema, param.Required) } -func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef, required bool) (interface{}, bool, error) { +func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef, required bool) (any, bool, error) { var found bool if len(schema.Value.AllOf) > 0 { - var value interface{} + var value any var err error for _, sr := range schema.Value.AllOf { var f bool @@ -294,7 +294,7 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho if len(schema.Value.OneOf) > 0 { isMatched := 0 - var value interface{} + var value any for _, sr := range schema.Value.OneOf { v, f, _ := decodeValue(dec, param, sm, sr, required) found = found || f @@ -318,14 +318,18 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho } if schema.Value.Type != nil { - var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) + var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) switch { case schema.Value.Type.Is("array"): - decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { - return dec.DecodeArray(param, sm, schema) + decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { + res, b, e := dec.DecodeArray(param, sm, schema) + if len(res) == 0 { + return nil, b, e + } + return res, b, e } case schema.Value.Type.Is("object"): - decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { + decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { return dec.DecodeObject(param, sm, schema) } default: @@ -357,7 +361,7 @@ type pathParamDecoder struct { pathParams map[string]string } -func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { +func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { var prefix string switch sm.Style { case "simple": @@ -387,7 +391,7 @@ func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.Serializat return val, ok, err } -func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) { +func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]any, bool, error) { var prefix, delim string switch { case sm.Style == "simple": @@ -425,7 +429,7 @@ func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationM return val, ok, err } -func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { +func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]any, bool, error) { var prefix, propsDelim, valueDelim string switch { case sm.Style == "simple" && !sm.Explode: @@ -496,7 +500,7 @@ type urlValuesDecoder struct { values url.Values } -func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { +func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { if sm.Style != "form" { return nil, false, invalidSerializationMethodErr(sm) } @@ -514,7 +518,7 @@ func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.Serializat return val, ok, err } -func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) { +func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]any, bool, error) { if sm.Style == "deepObject" { return nil, false, invalidSerializationMethodErr(sm) } @@ -543,14 +547,14 @@ func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationM // 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{} +func (d *urlValuesDecoder) parseArray(raw []string, sm *openapi3.SerializationMethod, schemaRef *openapi3.SchemaRef) ([]any, error) { + var value []any 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, &ParseError{path: []any{i}, Cause: v} } return nil, fmt.Errorf("item %d: %w", i, err) } @@ -565,9 +569,9 @@ func (d *urlValuesDecoder) parseArray(raw []string, sm *openapi3.SerializationMe return value, nil } -func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (interface{}, error) { +func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (any, error) { if len(schema.Value.AllOf) > 0 { - var value interface{} + var value any var err error for _, sr := range schema.Value.AllOf { value, err = d.parseValue(v, sr) @@ -579,7 +583,7 @@ func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (int } if len(schema.Value.AnyOf) > 0 { - var value interface{} + var value any var err error for _, sr := range schema.Value.AnyOf { if value, err = d.parseValue(v, sr); err == nil { @@ -592,7 +596,7 @@ func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (int if len(schema.Value.OneOf) > 0 { isMatched := 0 - var value interface{} + var value any var err error for _, sr := range schema.Value.OneOf { result, err := d.parseValue(v, sr) @@ -625,7 +629,7 @@ const ( urlDecoderDelimiter = "\x1F" // should not conflict with URL characters ) -func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { +func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]any, bool, error) { var propsFn func(url.Values) (map[string]string, error) switch sm.Style { case "form": @@ -652,6 +656,10 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization propsFn = func(params url.Values) (map[string]string, error) { props := make(map[string]string) for key, values := range params { + if !regexp.MustCompile(fmt.Sprintf(`^%s\[`, regexp.QuoteMeta(param))).MatchString(key) { + continue + } + matches := regexp.MustCompile(`\[(.*?)\]`).FindAllStringSubmatch(key, -1) switch l := len(matches); { case l == 0: @@ -715,7 +723,7 @@ type headerParamDecoder struct { header http.Header } -func (d *headerParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { +func (d *headerParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { if sm.Style != "simple" { return nil, false, invalidSerializationMethodErr(sm) } @@ -730,7 +738,7 @@ func (d *headerParamDecoder) DecodePrimitive(param string, sm *openapi3.Serializ return val, ok, err } -func (d *headerParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) { +func (d *headerParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]any, bool, error) { if sm.Style != "simple" { return nil, false, invalidSerializationMethodErr(sm) } @@ -745,7 +753,7 @@ func (d *headerParamDecoder) DecodeArray(param string, sm *openapi3.Serializatio return val, ok, err } -func (d *headerParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { +func (d *headerParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]any, bool, error) { if sm.Style != "simple" { return nil, false, invalidSerializationMethodErr(sm) } @@ -772,7 +780,7 @@ type cookieParamDecoder struct { req *http.Request } -func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { +func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { if sm.Style != "form" { return nil, false, invalidSerializationMethodErr(sm) } @@ -791,7 +799,7 @@ func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.Serializ return val, found, err } -func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) { +func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]any, bool, error) { if sm.Style != "form" || sm.Explode { return nil, false, invalidSerializationMethodErr(sm) } @@ -809,7 +817,7 @@ func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.Serializatio return val, found, err } -func (d *cookieParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { +func (d *cookieParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]any, bool, error) { if sm.Style != "form" || sm.Explode { return nil, false, invalidSerializationMethodErr(sm) } @@ -872,33 +880,33 @@ func propsFromString(src, propDelim, valueDelim string) (map[string]string, erro return props, nil } -func deepGet(m map[string]interface{}, keys ...string) (interface{}, bool) { +func deepGet(m map[string]any, keys ...string) (any, bool) { for _, key := range keys { val, ok := m[key] if !ok { return nil, false } - if m, ok = val.(map[string]interface{}); !ok { + if m, ok = val.(map[string]any); !ok { return val, true } } return m, true } -func deepSet(m map[string]interface{}, keys []string, value interface{}) { +func deepSet(m map[string]any, keys []string, value any) { for i := 0; i < len(keys)-1; i++ { key := keys[i] if _, ok := m[key]; !ok { - m[key] = make(map[string]interface{}) + m[key] = make(map[string]any) } - m = m[key].(map[string]interface{}) + m = m[key].(map[string]any) } m[keys[len(keys)-1]] = value } // makeObject returns an object that contains properties from props. -func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string]interface{}, error) { - mobj := make(map[string]interface{}) +func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string]any, error) { + mobj := make(map[string]any) for kk, value := range props { keys := strings.Split(kk, urlDecoderDelimiter) @@ -913,7 +921,7 @@ func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string if err != nil { return nil, err } - result, ok := r.(map[string]interface{}) + result, ok := r.(map[string]any) if !ok { return nil, &ParseError{Kind: KindOther, Reason: "invalid param object", Value: result} } @@ -922,8 +930,8 @@ func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string } // example: map[0:map[key:true] 1:map[key:false]] -> [map[key:true] map[key:false]] -func sliceMapToSlice(m map[string]interface{}) ([]interface{}, error) { - var result []interface{} +func sliceMapToSlice(m map[string]any) ([]any, error) { + var result []any keys := make([]int, 0, len(m)) for k := range m { @@ -951,7 +959,7 @@ func sliceMapToSlice(m map[string]interface{}) ([]interface{}, error) { } // buildResObj constructs an object based on a given schema and param values -func buildResObj(params map[string]interface{}, parentKeys []string, key string, schema *openapi3.SchemaRef) (interface{}, error) { +func buildResObj(params map[string]any, parentKeys []string, key string, schema *openapi3.SchemaRef) (any, error) { mapKeys := parentKeys if key != "" { mapKeys = append(mapKeys, key) @@ -963,7 +971,7 @@ func buildResObj(params map[string]interface{}, parentKeys []string, key string, if !ok { return nil, nil } - t, isMap := paramArr.(map[string]interface{}) + t, isMap := paramArr.(map[string]any) if !isMap { return nil, &ParseError{path: pathFromKeys(mapKeys), Kind: KindInvalidFormat, Reason: "array items must be set with indexes"} } @@ -972,7 +980,7 @@ func buildResObj(params map[string]interface{}, parentKeys []string, key string, if err != nil { return nil, &ParseError{path: pathFromKeys(mapKeys), Kind: KindInvalidFormat, Reason: fmt.Sprintf("could not convert value map to array: %v", err)} } - resultArr := make([]interface{} /*not 0,*/, len(arr)) + resultArr := make([]any /*not 0,*/, len(arr)) for i := range arr { r, err := buildResObj(params, mapKeys, strconv.Itoa(i), schema.Value.Items) if err != nil { @@ -984,10 +992,10 @@ func buildResObj(params map[string]interface{}, parentKeys []string, key string, } return resultArr, nil case schema.Value.Type.Is("object"): - resultMap := make(map[string]interface{}) + resultMap := make(map[string]any) additPropsSchema := schema.Value.AdditionalProperties.Schema pp, _ := deepGet(params, mapKeys...) - objectParams, ok := pp.(map[string]interface{}) + objectParams, ok := pp.(map[string]any) if !ok { // not the expected type, but return it either way and leave validation up to ValidateParameter return pp, nil @@ -1041,20 +1049,20 @@ func buildResObj(params map[string]interface{}, parentKeys []string, key string, } // buildFromSchemas decodes params with anyOf, oneOf, allOf schemas. -func buildFromSchemas(schemas openapi3.SchemaRefs, params map[string]interface{}, mapKeys []string, key string) (interface{}, error) { - resultMap := make(map[string]interface{}) +func buildFromSchemas(schemas openapi3.SchemaRefs, params map[string]any, mapKeys []string, key string) (any, error) { + resultMap := make(map[string]any) for _, s := range schemas { val, err := buildResObj(params, mapKeys, key, s) if err == nil && val != nil { - if m, ok := val.(map[string]interface{}); ok { + if m, ok := val.(map[string]any); ok { for k, v := range m { resultMap[k] = v } continue } - if a, ok := val.([]interface{}); ok { + if a, ok := val.([]any); ok { if len(a) > 0 { return a, nil } @@ -1080,8 +1088,8 @@ func handlePropParseError(path []string, err error) error { return fmt.Errorf("property %q: %w", strings.Join(path, "."), err) } -func pathFromKeys(kk []string) []interface{} { - path := make([]interface{}, 0, len(kk)) +func pathFromKeys(kk []string) []any { + path := make([]any, 0, len(kk)) for _, v := range kk { path = append(path, v) } @@ -1091,13 +1099,13 @@ func pathFromKeys(kk []string) []interface{} { // 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 parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, error) { - var value []interface{} +func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]any, error) { + var value []any for i, v := range raw { item, err := parsePrimitive(v, schemaRef.Value.Items) if err != nil { if v, ok := err.(*ParseError); ok { - return nil, &ParseError{path: []interface{}{i}, Cause: v} + return nil, &ParseError{path: []any{i}, Cause: v} } return nil, fmt.Errorf("item %d: %w", i, err) } @@ -1115,7 +1123,7 @@ func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, err // parsePrimitive returns a value that is created by parsing a source string to a primitive type // that is specified by a schema. The function returns nil when the source string is empty. // The function panics when a schema has a non-primitive type. -func parsePrimitive(raw string, schema *openapi3.SchemaRef) (v interface{}, err error) { +func parsePrimitive(raw string, schema *openapi3.SchemaRef) (v any, err error) { if raw == "" { return nil, nil } @@ -1127,7 +1135,7 @@ func parsePrimitive(raw string, schema *openapi3.SchemaRef) (v interface{}, err return } -func parsePrimitiveCase(raw string, schema *openapi3.SchemaRef, typ string) (interface{}, error) { +func parsePrimitiveCase(raw string, schema *openapi3.SchemaRef, typ string) (any, error) { switch typ { case "integer": if schema.Value.Format == "int32" { @@ -1165,8 +1173,8 @@ func parsePrimitiveCase(raw string, schema *openapi3.SchemaRef, typ string) (int type EncodingFn func(partName string) *openapi3.Encoding // BodyDecoder is an interface to decode a body of a request or response. -// An implementation must return a value that is a primitive, []interface{}, or map[string]interface{}. -type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn, *fastjson.Parser) (interface{}, error) +// An implementation must return a value that is a primitive, []any, or map[string]any. +type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn, *fastjson.Parser) (any, error) // bodyDecoders contains decoders for supported content types of a body. // By default, there is content type "application/json" is supported only. @@ -1249,7 +1257,7 @@ func getBodyDecoder(mediaType, suffix string) (BodyDecoder, error) { // The function returns ParseError when a body is invalid. func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) ( string, - interface{}, + any, error, ) { contentType := header.Get(headerCT) @@ -1299,7 +1307,7 @@ func init() { RegisterBodyDecoder("text/plain", plainBodyDecoder) } -func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (interface{}, error) { +func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (any, error) { data, err := io.ReadAll(body) if err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Cause: err} @@ -1307,7 +1315,7 @@ func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schem return string(data), nil } -func multipartPartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (interface{}, error) { +func multipartPartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (any, error) { data, err := io.ReadAll(body) if err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Cause: err} @@ -1333,7 +1341,7 @@ func multipartPartBodyDecoder(body io.Reader, header http.Header, schema *openap return dataStr, nil } -func jsonBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (interface{}, error) { +func jsonBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (any, error) { var data []byte var err error @@ -1350,7 +1358,7 @@ func jsonBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schema return convertToMap(parsedDoc), nil } -func xmlBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (interface{}, error) { +func xmlBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (any, error) { var data []byte var err error @@ -1367,15 +1375,15 @@ func xmlBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaR return mv.Old(), nil } -func yamlBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (interface{}, error) { - var value interface{} +func yamlBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (any, error) { + var value any if err := yaml.NewDecoder(body).Decode(&value); err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Cause: err} } return value, nil } -func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (interface{}, error) { +func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (any, error) { // Validate schema of request body. // By the OpenAPI 3 specification request body's schema must have type "object". @@ -1407,7 +1415,7 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3. } // Make an object value from form values. - obj := make(map[string]interface{}) + obj := make(map[string]any) dec := &urlValuesDecoder{values: values} // Decode schema constructs (allOf, anyOf, oneOf) if err := decodeSchemaConstructs(dec, schema.Value.AllOf, obj, encFn); err != nil { @@ -1430,8 +1438,20 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3. // decodeSchemaConstructs tries to decode properties based on provided schemas. // This function is for decoding purposes only and not for validation. -func decodeSchemaConstructs(dec *urlValuesDecoder, schemas []*openapi3.SchemaRef, obj map[string]interface{}, encFn EncodingFn) error { +func decodeSchemaConstructs(dec *urlValuesDecoder, schemas []*openapi3.SchemaRef, obj map[string]any, encFn EncodingFn) error { for _, schemaRef := range schemas { + + // Decode schema constructs (allOf, anyOf, oneOf) + if err := decodeSchemaConstructs(dec, schemaRef.Value.AllOf, obj, encFn); err != nil { + return err + } + if err := decodeSchemaConstructs(dec, schemaRef.Value.AnyOf, obj, encFn); err != nil { + return err + } + if err := decodeSchemaConstructs(dec, schemaRef.Value.OneOf, obj, encFn); err != nil { + return err + } + for name, prop := range schemaRef.Value.Properties { value, _, err := decodeProperty(dec, name, prop, encFn) if err != nil { @@ -1447,11 +1467,11 @@ func decodeSchemaConstructs(dec *urlValuesDecoder, schemas []*openapi3.SchemaRef return nil } -func isEqual(value1, value2 interface{}) bool { +func isEqual(value1, value2 any) bool { return reflect.DeepEqual(value1, value2) } -func decodeProperty(dec valueDecoder, name string, prop *openapi3.SchemaRef, encFn EncodingFn) (interface{}, bool, error) { +func decodeProperty(dec valueDecoder, name string, prop *openapi3.SchemaRef, encFn EncodingFn) (any, bool, error) { var enc *openapi3.Encoding if encFn != nil { enc = encFn(name) @@ -1460,13 +1480,13 @@ func decodeProperty(dec valueDecoder, name string, prop *openapi3.SchemaRef, enc return decodeValue(dec, name, sm, prop, false) } -func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (interface{}, error) { +func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (any, error) { if !schema.Value.Type.Is("object") { return nil, errors.New("unsupported schema of request body") } // Parse form. - values := make(map[string][]interface{}) + values := make(map[string][]any) contentType := header.Get(headerCT) _, params, err := mime.ParseMediaType(contentType) if err != nil { @@ -1529,10 +1549,10 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S } } - var value interface{} + var value any if _, value, err = decodeBody(part, http.Header(part.Header), valueSchema, subEncFn, jsonParser); err != nil { if v, ok := err.(*ParseError); ok { - return nil, &ParseError{path: []interface{}{name}, Cause: v} + return nil, &ParseError{path: []any{name}, Cause: v} } return nil, fmt.Errorf("part %s: %w", name, err) } @@ -1563,7 +1583,7 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S } // Make an object value from form values. - obj := make(map[string]interface{}) + obj := make(map[string]any) for name, prop := range allTheProperties { vv := values[name] if len(vv) == 0 { @@ -1580,7 +1600,7 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S } // FileBodyDecoder is a body decoder that decodes a file body to a string. -func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (interface{}, error) { +func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (any, error) { data, err := io.ReadAll(body) if err != nil { return nil, err @@ -1589,7 +1609,7 @@ func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schema } // zipFileBodyDecoder is a body decoder that decodes a zip file body to a string. -func zipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (interface{}, error) { +func zipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (any, error) { buff := bytes.NewBuffer([]byte{}) size, err := io.Copy(buff, body) if err != nil { @@ -1640,10 +1660,10 @@ func zipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Sch } // csvBodyDecoder is a body decoder that decodes a csv body to a string. -func csvBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (interface{}, error) { +func csvBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (any, error) { r := csv.NewReader(body) - var content string + var sb strings.Builder for { record, err := r.Read() if err == io.EOF { @@ -1653,8 +1673,9 @@ func csvBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaR return nil, err } - content += strings.Join(record, ",") + "\n" + sb.WriteString(strings.Join(record, ",")) + sb.WriteString("\n") } - return content, nil + return sb.String(), nil } diff --git a/internal/platform/validator/req_resp_decoder_test.go b/internal/platform/validator/req_resp_decoder_test.go index 829ebbd2..afd5391f 100644 --- a/internal/platform/validator/req_resp_decoder_test.go +++ b/internal/platform/validator/req_resp_decoder_test.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/stretchr/testify/assert" "io" "mime/multipart" "net/http" @@ -18,6 +17,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/valyala/fastjson" ) @@ -28,7 +28,7 @@ var ( arrayOf = func(items *openapi3.SchemaRef) *openapi3.SchemaRef { return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"array"}, Items: items}} } - objectOf = func(args ...interface{}) *openapi3.SchemaRef { + objectOf = func(args ...any) *openapi3.SchemaRef { s := &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}, Properties: make(map[string]*openapi3.SchemaRef)}} if len(args)%2 != 0 { panic("invalid arguments. must be an even number of arguments") @@ -119,25 +119,25 @@ var ( ) func TestDeepGet(t *testing.T) { - iarray := map[string]interface{}{ - "0": map[string]interface{}{ + iarray := map[string]any{ + "0": map[string]any{ "foo": 111, }, - "1": map[string]interface{}{ + "1": map[string]any{ "bar": 222, }, } tests := []struct { name string - m map[string]interface{} + m map[string]any keys []string - expected interface{} + expected any shouldFind bool }{ { name: "Simple map - key exists", - m: map[string]interface{}{ + m: map[string]any{ "foo": "bar", }, keys: []string{"foo"}, @@ -146,8 +146,8 @@ func TestDeepGet(t *testing.T) { }, { name: "Nested map - key exists", - m: map[string]interface{}{ - "foo": map[string]interface{}{ + m: map[string]any{ + "foo": map[string]any{ "bar": "baz", }, }, @@ -157,8 +157,8 @@ func TestDeepGet(t *testing.T) { }, { name: "Nested map - key does not exist", - m: map[string]interface{}{ - "foo": map[string]interface{}{ + m: map[string]any{ + "foo": map[string]any{ "bar": "baz", }, }, @@ -168,8 +168,8 @@ func TestDeepGet(t *testing.T) { }, { name: "Array - element exists", - m: map[string]interface{}{ - "array": map[string]interface{}{"0": "a", "1": "b", "2": "c"}, + m: map[string]any{ + "array": map[string]any{"0": "a", "1": "b", "2": "c"}, }, keys: []string{"array", "1"}, expected: "b", @@ -177,8 +177,8 @@ func TestDeepGet(t *testing.T) { }, { name: "Array - element does not exist - invalid index", - m: map[string]interface{}{ - "array": map[string]interface{}{"0": "a", "1": "b", "2": "c"}, + m: map[string]any{ + "array": map[string]any{"0": "a", "1": "b", "2": "c"}, }, keys: []string{"array", "3"}, expected: nil, @@ -186,8 +186,8 @@ func TestDeepGet(t *testing.T) { }, { name: "Array - element does not exist - invalid keys", - m: map[string]interface{}{ - "array": map[string]interface{}{"0": "a", "1": "b", "2": "c"}, + m: map[string]any{ + "array": map[string]any{"0": "a", "1": "b", "2": "c"}, }, keys: []string{"array", "a", "999"}, expected: nil, @@ -195,7 +195,7 @@ func TestDeepGet(t *testing.T) { }, { name: "Array of objects - element exists 1", - m: map[string]interface{}{ + m: map[string]any{ "array": iarray, }, keys: []string{"array", "1", "bar"}, @@ -204,18 +204,18 @@ func TestDeepGet(t *testing.T) { }, { name: "Array of objects - element exists 2", - m: map[string]interface{}{ + m: map[string]any{ "array": iarray, }, keys: []string{"array", "0"}, - expected: map[string]interface{}{ + expected: map[string]any{ "foo": 111, }, shouldFind: true, }, { name: "Array of objects - element exists 3", - m: map[string]interface{}{ + m: map[string]any{ "array": iarray, }, keys: []string{"array"}, @@ -238,26 +238,26 @@ func TestDeepGet(t *testing.T) { func TestDeepSet(t *testing.T) { tests := []struct { name string - inputMap map[string]interface{} + inputMap map[string]any keys []string - value interface{} - expected map[string]interface{} + value any + expected map[string]any }{ { name: "simple set", - inputMap: map[string]interface{}{}, + inputMap: map[string]any{}, keys: []string{"key"}, value: "value", - expected: map[string]interface{}{"key": "value"}, + expected: map[string]any{"key": "value"}, }, { name: "intermediate array of objects", - inputMap: map[string]interface{}{}, + inputMap: map[string]any{}, keys: []string{"nested", "0", "key"}, value: true, - expected: map[string]interface{}{ - "nested": map[string]interface{}{ - "0": map[string]interface{}{ + expected: map[string]any{ + "nested": map[string]any{ + "0": map[string]any{ "key": true, }, }, @@ -265,12 +265,12 @@ func TestDeepSet(t *testing.T) { }, { name: "existing nested array of objects", - inputMap: map[string]interface{}{"nested": map[string]interface{}{"0": map[string]interface{}{"existingKey": "existingValue"}}}, + inputMap: map[string]any{"nested": map[string]any{"0": map[string]any{"existingKey": "existingValue"}}}, keys: []string{"nested", "0", "newKey"}, value: "newValue", - expected: map[string]interface{}{ - "nested": map[string]interface{}{ - "0": map[string]interface{}{ + expected: map[string]any{ + "nested": map[string]any{ + "0": map[string]any{ "existingKey": "existingValue", "newKey": "newValue", }, @@ -295,7 +295,7 @@ func TestDecodeParameter(t *testing.T) { query string header string cookie string - want interface{} + want any found bool err error } @@ -442,21 +442,21 @@ func TestDecodeParameter(t *testing.T) { name: "simple", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: stringArraySchema}, path: "/foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: stringArraySchema}, path: "/foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "label", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: stringArraySchema}, path: "/.foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -470,7 +470,7 @@ func TestDecodeParameter(t *testing.T) { name: "label explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: stringArraySchema}, path: "/.foo.bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -484,7 +484,7 @@ func TestDecodeParameter(t *testing.T) { name: "matrix", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: stringArraySchema}, path: "/;param=foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -498,7 +498,7 @@ func TestDecodeParameter(t *testing.T) { name: "matrix explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: stringArraySchema}, path: "/;param=foo;param=bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -512,7 +512,7 @@ func TestDecodeParameter(t *testing.T) { name: "default", param: &openapi3.Parameter{Name: "param", In: "path", Schema: stringArraySchema}, path: "/foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -520,21 +520,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(integerSchema)}, path: "/1,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(numberSchema)}, path: "/1.1,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(booleanSchema)}, path: "/true,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, }, @@ -545,21 +545,21 @@ func TestDecodeParameter(t *testing.T) { name: "simple", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: objectSchema}, path: "/id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: objectSchema}, path: "/id=foo,name=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { name: "label", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: objectSchema}, path: "/.id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -573,7 +573,7 @@ func TestDecodeParameter(t *testing.T) { name: "label explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: objectSchema}, path: "/.id=foo.name=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -587,7 +587,7 @@ func TestDecodeParameter(t *testing.T) { name: "matrix", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: objectSchema}, path: "/;param=id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -601,7 +601,7 @@ func TestDecodeParameter(t *testing.T) { name: "matrix explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: objectSchema}, path: "/;id=foo;name=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -615,7 +615,7 @@ func TestDecodeParameter(t *testing.T) { name: "default", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectSchema}, path: "/id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -623,21 +623,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", integerSchema)}, path: "/foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", numberSchema)}, path: "/foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", booleanSchema)}, path: "/foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, }, @@ -787,49 +787,49 @@ func TestDecodeParameter(t *testing.T) { name: "form", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: stringArraySchema}, query: "param=foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "form explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: stringArraySchema}, query: "param=foo¶m=bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "spaceDelimited", param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: noExplode, Schema: stringArraySchema}, query: "param=foo bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "spaceDelimited explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: explode, Schema: stringArraySchema}, query: "param=foo¶m=bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "pipeDelimited", param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: noExplode, Schema: stringArraySchema}, query: "param=foo|bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "pipeDelimited explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: explode, Schema: stringArraySchema}, query: "param=foo¶m=bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "default", param: &openapi3.Parameter{Name: "param", In: "query", Schema: stringArraySchema}, query: "param=foo¶m=bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -837,21 +837,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(integerSchema)}, query: "param=1¶m=foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(numberSchema)}, query: "param=1.1¶m=foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(booleanSchema)}, query: "param=true¶m=foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, }, @@ -862,23 +862,39 @@ func TestDecodeParameter(t *testing.T) { name: "form", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: objectSchema}, query: "param=id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { name: "form explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: objectSchema}, query: "id=foo&name=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { name: "deepObject explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "deepObject", Explode: explode, Schema: objectSchema}, query: "param[id]=foo¶m[name]=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, + { + name: "no param, for arrays", + param: &openapi3.Parameter{Name: "something", In: "query", Schema: stringArraySchema}, + query: "", + want: nil, + found: false, + err: nil, + }, + { + name: "missing param, for arrays", + param: &openapi3.Parameter{Name: "something", In: "query", Schema: stringArraySchema}, + query: "foo=bar", + want: nil, + found: false, + err: nil, + }, { name: "deepObject explode additionalProperties with object properties - missing index on nested array", param: &openapi3.Parameter{ @@ -889,20 +905,20 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][prop2][item2]=def", - err: &ParseError{path: []interface{}{"obj", "prop2", "item2"}, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}, + err: &ParseError{path: []any{"obj", "prop2", "item2"}, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}, }, { name: "deepObject explode array - missing indexes", param: &openapi3.Parameter{Name: "param", In: "query", Style: "deepObject", Explode: explode, Schema: objectOf("items", stringArraySchema)}, query: "param[items]=f%26oo¶m[items]=bar", found: true, - err: &ParseError{path: []interface{}{"items"}, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}, + err: &ParseError{path: []any{"items"}, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}, }, { name: "deepObject explode array", param: &openapi3.Parameter{Name: "param", In: "query", Style: "deepObject", Explode: explode, Schema: objectOf("items", integerArraySchema)}, query: "param[items][1]=456¶m[items][0]=123", - want: map[string]interface{}{"items": []interface{}{int64(123), int64(456)}}, + want: map[string]any{"items": []any{int64(123), int64(456)}}, found: true, }, { @@ -916,8 +932,8 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][prop1]=bar¶m[obj][prop2]=foo¶m[objTwo]=string", - want: map[string]interface{}{ - "obj": map[string]interface{}{"prop1": "bar", "prop2": "foo"}, + want: map[string]any{ + "obj": map[string]any{"prop1": "bar", "prop2": "foo"}, "objTwo": "string", }, found: true, @@ -932,8 +948,8 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][prop1][item1]=1¶m[obj][prop1][item2]=abc", - want: map[string]interface{}{ - "obj": map[string]interface{}{"prop1": map[string]interface{}{ + want: map[string]any{ + "obj": map[string]any{"prop1": map[string]any{ "item1": int64(1), "item2": "abc", }}, @@ -951,7 +967,7 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][prop1]=notbool¶m[objTwo]=string", - err: &ParseError{path: []interface{}{"obj", "prop1"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "notbool"}}, + err: &ParseError{path: []any{"obj", "prop1"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "notbool"}}, }, { name: "deepObject explode nested object additionalProperties - bad index inside additionalProperties", @@ -965,10 +981,10 @@ func TestDecodeParameter(t *testing.T) { }, query: "param[obj][prop1]=bar¶m[obj][prop2][badindex]=bad¶m[objTwo]=string", err: &ParseError{ - path: []interface{}{"obj", "prop2"}, + path: []any{"obj", "prop2"}, Reason: `path is not convertible to primitive`, Kind: KindInvalidFormat, - Value: map[string]interface{}{"badindex": "bad"}, + Value: map[string]any{"badindex": "bad"}, }, }, { @@ -982,8 +998,8 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne]=bar¶m[obj][nestedObjTwo]=foo¶m[objTwo]=string", - want: map[string]interface{}{ - "obj": map[string]interface{}{"nestedObjOne": "bar", "nestedObjTwo": "foo"}, + want: map[string]any{ + "obj": map[string]any{"nestedObjOne": "bar", "nestedObjTwo": "foo"}, "objTwo": "string", }, found: true, @@ -997,7 +1013,7 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "anotherparam=bar", - want: map[string]interface{}(nil), + want: map[string]any(nil), }, { name: "deepObject explode nested object - bad array item type", @@ -1008,7 +1024,18 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[objTwo][0]=badint", - err: &ParseError{path: []interface{}{"objTwo", "0"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}}, + err: &ParseError{path: []any{"objTwo", "0"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}}, + }, + { + name: "deepObject explode nested object - extraneous deep object param ignored", + param: &openapi3.Parameter{ + Name: "param", In: "query", Style: "deepObject", Explode: explode, + Schema: objectOf( + "obj", objectOf("nestedObjOne", stringSchema, "nestedObjTwo", stringSchema), + ), + }, + query: "anotherparam[obj][nestedObjOne]=one&anotherparam[obj][nestedObjTwo]=two", + want: map[string]any(nil), }, { name: "deepObject explode deeply nested object - bad array item type", @@ -1019,7 +1046,7 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne][0]=badint", - err: &ParseError{path: []interface{}{"obj", "nestedObjOne", "0"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}}, + err: &ParseError{path: []any{"obj", "nestedObjOne", "0"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}}, }, { name: "deepObject explode deeply nested object - array index not an int", @@ -1030,7 +1057,7 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne][badindex]=badint", - err: &ParseError{path: []interface{}{"obj", "nestedObjOne"}, Kind: KindInvalidFormat, Reason: "could not convert value map to array: array indexes must be integers: strconv.Atoi: parsing \"badindex\": invalid syntax"}, + err: &ParseError{path: []any{"obj", "nestedObjOne"}, Kind: KindInvalidFormat, Reason: "could not convert value map to array: array indexes must be integers: strconv.Atoi: parsing \"badindex\": invalid syntax"}, }, { name: "deepObject explode nested object with array", @@ -1043,9 +1070,9 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne]=bar¶m[obj][nestedObjTwo]=foo¶m[objTwo][0]=f%26oo¶m[objTwo][1]=bar", - want: map[string]interface{}{ - "obj": map[string]interface{}{"nestedObjOne": "bar", "nestedObjTwo": "foo"}, - "objTwo": []interface{}{"f%26oo", "bar"}, + want: map[string]any{ + "obj": map[string]any{"nestedObjOne": "bar", "nestedObjTwo": "foo"}, + "objTwo": []any{"f%26oo", "bar"}, }, found: true, }, @@ -1060,7 +1087,7 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne]=bar¶m[obj][nestedObjTwo]=bad¶m[objTwo][0]=f%26oo¶m[objTwo][1]=bar", - err: &ParseError{path: []interface{}{"obj", "nestedObjTwo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bad"}}, + err: &ParseError{path: []any{"obj", "nestedObjTwo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bad"}}, }, { name: "deepObject explode nested object with nested array", @@ -1073,9 +1100,9 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne]=bar¶m[obj][nestedObjTwo]=foo¶m[objTwo][items][0]=f%26oo¶m[objTwo][items][1]=bar", - want: map[string]interface{}{ - "obj": map[string]interface{}{"nestedObjOne": "bar", "nestedObjTwo": "foo"}, - "objTwo": map[string]interface{}{"items": []interface{}{"f%26oo", "bar"}}, + want: map[string]any{ + "obj": map[string]any{"nestedObjOne": "bar", "nestedObjTwo": "foo"}, + "objTwo": map[string]any{"items": []any{"f%26oo", "bar"}}, }, found: true, }, @@ -1089,9 +1116,9 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne][items][0]=baz¶m[objTwo][items][0]=foo¶m[objTwo][items][1]=bar", - want: map[string]interface{}{ - "obj": map[string]interface{}{"nestedObjOne": map[string]interface{}{"items": []interface{}{"baz"}}}, - "objTwo": map[string]interface{}{"items": []interface{}{"foo", "bar"}}, + want: map[string]any{ + "obj": map[string]any{"nestedObjOne": map[string]any{"items": []any{"baz"}}}, + "objTwo": map[string]any{"items": []any{"foo", "bar"}}, }, found: true, }, @@ -1104,10 +1131,10 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[arr][1][1]=123¶m[arr][1][2]=456", - want: map[string]interface{}{ - "arr": []interface{}{ + want: map[string]any{ + "arr": []any{ nil, - []interface{}{nil, int64(123), int64(456)}, + []any{nil, int64(123), int64(456)}, }, }, found: true, @@ -1121,12 +1148,12 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[arr][3][key]=true¶m[arr][0][key]=false", - want: map[string]interface{}{ - "arr": []interface{}{ - map[string]interface{}{"key": false}, + want: map[string]any{ + "arr": []any{ + map[string]any{"key": false}, nil, nil, - map[string]interface{}{"key": true}, + map[string]any{"key": true}, }, }, found: true, @@ -1141,10 +1168,10 @@ func TestDecodeParameter(t *testing.T) { }, query: "param[arr][0][key]=true¶m[arr][1][key]=false", found: true, - want: map[string]interface{}{ - "arr": []interface{}{ - map[string]interface{}{"key": true}, - map[string]interface{}{"key": false}, + want: map[string]any{ + "arr": []any{ + map[string]any{"key": true}, + map[string]any{"key": false}, }, }, }, @@ -1152,7 +1179,7 @@ func TestDecodeParameter(t *testing.T) { name: "default", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectSchema}, query: "id=foo&name=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -1160,21 +1187,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", integerSchema)}, query: "foo=bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", numberSchema)}, query: "foo=bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", booleanSchema)}, query: "foo=bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, }, @@ -1260,21 +1287,21 @@ func TestDecodeParameter(t *testing.T) { name: "simple", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: stringArraySchema}, header: "X-Param:foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: stringArraySchema}, header: "X-Param:foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "default", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: stringArraySchema}, header: "X-Param:foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -1282,21 +1309,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(integerSchema)}, header: "X-Param:1,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(numberSchema)}, header: "X-Param:1.1,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(booleanSchema)}, header: "X-Param:true,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, }, @@ -1307,21 +1334,21 @@ func TestDecodeParameter(t *testing.T) { name: "simple", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: objectSchema}, header: "X-Param:id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: objectSchema}, header: "X-Param:id=foo,name=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { name: "default", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectSchema}, header: "X-Param:id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -1336,21 +1363,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", integerSchema)}, header: "X-Param:foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", numberSchema)}, header: "X-Param:foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", booleanSchema)}, header: "X-Param:foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, }, @@ -1436,7 +1463,7 @@ func TestDecodeParameter(t *testing.T) { name: "form", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: stringArraySchema}, cookie: "X-Param:foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -1444,21 +1471,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(integerSchema)}, cookie: "X-Param:1,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(numberSchema)}, cookie: "X-Param:1.1,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(booleanSchema)}, cookie: "X-Param:true,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, }, @@ -1469,7 +1496,7 @@ func TestDecodeParameter(t *testing.T) { name: "form", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectSchema}, cookie: "X-Param:id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -1477,21 +1504,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", integerSchema)}, cookie: "X-Param:foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", numberSchema)}, cookie: "X-Param:foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", booleanSchema)}, cookie: "X-Param:foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, }, @@ -1585,7 +1612,7 @@ func TestDecodeBody(t *testing.T) { urlencodedPipeDelim.Set("b", "10") urlencodedPipeDelim.Add("c", "c1|c2") - d, err := json.Marshal(map[string]interface{}{"d1": "d1"}) + d, err := json.Marshal(map[string]any{"d1": "d1"}) require.NoError(t, err) multipartForm, multipartFormMime, err := newTestMultipartForm([]*testFormPart{ {name: "a", contentType: "text/plain", data: strings.NewReader("a1")}, @@ -1629,7 +1656,7 @@ func TestDecodeBody(t *testing.T) { body io.Reader schema *openapi3.Schema encoding map[string]*openapi3.Encoding - want interface{} + want any wantErr error }{ { @@ -1687,7 +1714,7 @@ func TestDecodeBody(t *testing.T) { WithProperty("a", openapi3.NewStringSchema()). WithProperty("b", openapi3.NewIntegerSchema()). WithProperty("c", openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema())), - want: map[string]interface{}{"a": "a1", "b": int64(10), "c": []interface{}{"c1", "c2"}}, + want: map[string]any{"a": "a1", "b": int64(10), "c": []any{"c1", "c2"}}, }, { name: "urlencoded space delimited", @@ -1700,7 +1727,7 @@ func TestDecodeBody(t *testing.T) { encoding: map[string]*openapi3.Encoding{ "c": {Style: openapi3.SerializationSpaceDelimited, Explode: openapi3.BoolPtr(false)}, }, - want: map[string]interface{}{"a": "a1", "b": int64(10), "c": []interface{}{"c1", "c2"}}, + want: map[string]any{"a": "a1", "b": int64(10), "c": []any{"c1", "c2"}}, }, { name: "urlencoded pipe delimited", @@ -1713,7 +1740,7 @@ func TestDecodeBody(t *testing.T) { encoding: map[string]*openapi3.Encoding{ "c": {Style: openapi3.SerializationPipeDelimited, Explode: openapi3.BoolPtr(false)}, }, - want: map[string]interface{}{"a": "a1", "b": int64(10), "c": []interface{}{"c1", "c2"}}, + want: map[string]any{"a": "a1", "b": int64(10), "c": []any{"c1", "c2"}}, }, { name: "multipart", @@ -1726,7 +1753,7 @@ func TestDecodeBody(t *testing.T) { WithProperty("d", openapi3.NewObjectSchema().WithProperty("d1", openapi3.NewStringSchema())). WithProperty("f", openapi3.NewStringSchema().WithFormat("binary")). WithProperty("g", openapi3.NewStringSchema()), - want: map[string]interface{}{"a": "a1", "b": json.Number("10"), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo", "g": "g1"}, + want: map[string]any{"a": "a1", "b": json.Number("10"), "c": []any{"c1", "c2"}, "d": map[string]any{"d1": "d1"}, "f": "foo", "g": "g1"}, }, { name: "multipartExtraPart", @@ -1734,7 +1761,7 @@ func TestDecodeBody(t *testing.T) { body: multipartFormExtraPart, schema: openapi3.NewObjectSchema(). WithProperty("a", openapi3.NewStringSchema()), - want: map[string]interface{}{"a": "a1"}, + want: map[string]any{"a": "a1"}, wantErr: &ParseError{Kind: KindOther}, }, { @@ -1744,7 +1771,7 @@ func TestDecodeBody(t *testing.T) { schema: openapi3.NewObjectSchema(). WithAnyAdditionalProperties(). WithProperty("a", openapi3.NewStringSchema()), - want: map[string]interface{}{"a": "a1"}, + want: map[string]any{"a": "a1"}, }, { name: "multipartWithAdditionalProperties", @@ -1754,7 +1781,7 @@ func TestDecodeBody(t *testing.T) { WithAdditionalProperties(openapi3.NewObjectSchema(). WithProperty("x", openapi3.NewStringSchema())). WithProperty("a", openapi3.NewStringSchema()), - want: map[string]interface{}{"a": "a1", "x": "x1"}, + want: map[string]any{"a": "a1", "x": "x1"}, }, { name: "multipartWithAdditionalPropertiesError", @@ -1764,7 +1791,7 @@ func TestDecodeBody(t *testing.T) { WithAdditionalProperties(openapi3.NewObjectSchema(). WithProperty("x", openapi3.NewStringSchema())). WithProperty("a", openapi3.NewStringSchema()), - want: map[string]interface{}{"a": "a1", "x": "x1"}, + want: map[string]any{"a": "a1", "x": "x1"}, wantErr: &ParseError{Kind: KindOther}, }, { @@ -1838,7 +1865,7 @@ func newTestMultipartForm(parts []*testFormPart) (io.Reader, string, error) { func TestRegisterAndUnregisterBodyDecoder(t *testing.T) { var decoder BodyDecoder - decoder = func(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (decoded interface{}, err error) { + decoder = func(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn EncodingFn, jsonParser *fastjson.Parser) (decoded any, err error) { var data []byte if data, err = io.ReadAll(body); err != nil { return diff --git a/internal/platform/validator/req_resp_encoder.go b/internal/platform/validator/req_resp_encoder.go index 870f06e3..3b5f8b1d 100644 --- a/internal/platform/validator/req_resp_encoder.go +++ b/internal/platform/validator/req_resp_encoder.go @@ -5,7 +5,7 @@ import ( "fmt" ) -func encodeBody(body interface{}, mediaType string) ([]byte, error) { +func encodeBody(body any, mediaType string) ([]byte, error) { encoder, ok := bodyEncoders[mediaType] if !ok { return nil, &ParseError{ @@ -16,7 +16,7 @@ func encodeBody(body interface{}, mediaType string) ([]byte, error) { return encoder(body) } -type BodyEncoder func(body interface{}) ([]byte, error) +type BodyEncoder func(body any) ([]byte, error) var bodyEncoders = map[string]BodyEncoder{ "application/json": json.Marshal, diff --git a/internal/platform/validator/req_resp_encoder_test.go b/internal/platform/validator/req_resp_encoder_test.go index 8f746f06..51f7e4db 100644 --- a/internal/platform/validator/req_resp_encoder_test.go +++ b/internal/platform/validator/req_resp_encoder_test.go @@ -11,7 +11,7 @@ import ( func TestRegisterAndUnregisterBodyEncoder(t *testing.T) { var encoder BodyEncoder - encoder = func(body interface{}) (data []byte, err error) { + encoder = func(body any) (data []byte, err error) { return []byte(strings.Join(body.([]string), ",")), nil } contentType := "text/csv" diff --git a/internal/platform/validator/unknown_parameters_request.go b/internal/platform/validator/unknown_parameters_request.go index d8310d92..8dfabafd 100644 --- a/internal/platform/validator/unknown_parameters_request.go +++ b/internal/platform/validator/unknown_parameters_request.go @@ -178,7 +178,7 @@ func ValidateUnknownRequestParameters(ctx *fasthttp.RequestCtx, route *routers.R } case mType == "application/x-www-form-urlencoded": // required params in paramList - paramList, ok := value.(map[string]interface{}) + paramList, ok := value.(map[string]any) if !ok { return foundUnknownParams, ErrDecodingFailed } @@ -193,7 +193,7 @@ func ValidateUnknownRequestParameters(ctx *fasthttp.RequestCtx, route *routers.R } }) case mType == "application/json" || mType == "multipart/form-data" || suffix == "+json": - paramList, ok := value.(map[string]interface{}) + paramList, ok := value.(map[string]any) if !ok { return foundUnknownParams, ErrDecodingFailed } @@ -213,7 +213,7 @@ func ValidateUnknownRequestParameters(ctx *fasthttp.RequestCtx, route *routers.R propKeys = append(propKeys, strings.ToLower(key)) } - paramList, ok := value.(map[string]interface{}) + paramList, ok := value.(map[string]any) if !ok { return foundUnknownParams, ErrDecodingFailed } @@ -221,7 +221,7 @@ func ValidateUnknownRequestParameters(ctx *fasthttp.RequestCtx, route *routers.R switch len(paramList) { case 1: for _, paramValue := range paramList { - params, ok := paramValue.(map[string]interface{}) + params, ok := paramValue.(map[string]any) if !ok { continue } diff --git a/internal/platform/validator/validate_request.go b/internal/platform/validator/validate_request.go index 62f6ac2a..9d5a845e 100644 --- a/internal/platform/validator/validate_request.go +++ b/internal/platform/validator/validate_request.go @@ -6,7 +6,9 @@ import ( "fmt" "io" "net/http" + "net/url" "sort" + "strings" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" @@ -30,7 +32,7 @@ var ErrInvalidEmptyValue = errors.New("empty value is not allowed") // // Note: One can tune the behavior of uniqueItems: true verification // by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker -func ValidateRequest(ctx context.Context, input *openapi3filter.RequestValidationInput, jsonParser *fastjson.Parser) (err error) { +func ValidateRequest(ctx context.Context, input *openapi3filter.RequestValidationInput, jsonParser *fastjson.Parser) error { var me openapi3.MultiError options := input.Options @@ -50,11 +52,10 @@ func ValidateRequest(ctx context.Context, input *openapi3filter.RequestValidatio security = &route.Spec.Security } if security != nil { - if err = ValidateSecurityRequirements(ctx, input, *security); err != nil && !options.MultiError { - return - } - - if err != nil { + if err := ValidateSecurityRequirements(ctx, input, *security); err != nil { + if !options.MultiError { + return err + } me = append(me, err) } } @@ -68,11 +69,10 @@ func ValidateRequest(ctx context.Context, input *openapi3filter.RequestValidatio } } - if err = ValidateParameter(ctx, input, parameter); err != nil && !options.MultiError { - return - } - - if err != nil { + if err := ValidateParameter(ctx, input, parameter); err != nil { + if !options.MultiError { + return err + } me = append(me, err) } } @@ -82,11 +82,11 @@ func ValidateRequest(ctx context.Context, input *openapi3filter.RequestValidatio if options.ExcludeRequestQueryParams && parameter.Value.In == openapi3.ParameterInQuery { continue } - if err = ValidateParameter(ctx, input, parameter.Value); err != nil && !options.MultiError { - return - } - if err != nil { + if err := ValidateParameter(ctx, input, parameter.Value); err != nil { + if !options.MultiError { + return err + } me = append(me, err) } } @@ -94,11 +94,10 @@ func ValidateRequest(ctx context.Context, input *openapi3filter.RequestValidatio // RequestBody requestBody := operation.RequestBody if requestBody != nil && !options.ExcludeRequestBody { - if err = ValidateRequestBody(ctx, input, requestBody.Value, jsonParser); err != nil && !options.MultiError { - return - } - - if err != nil { + if err := ValidateRequestBody(ctx, input, requestBody.Value, jsonParser); err != nil { + if !options.MultiError { + return err + } me = append(me, err) } } @@ -106,7 +105,37 @@ func ValidateRequest(ctx context.Context, input *openapi3filter.RequestValidatio if len(me) > 0 { return me } - return + + return nil +} + +// appendToQueryValues adds to query parameters each value in the provided slice +func appendToQueryValues[T any](q url.Values, parameterName string, v []T) { + for _, i := range v { + q.Add(parameterName, fmt.Sprintf("%v", i)) + } +} + +func joinValues(values []any, sep string) string { + strValues := make([]string, len(values)) + for i, v := range values { + strValues[i] = fmt.Sprintf("%v", v) + } + return strings.Join(strValues, sep) +} + +// populateDefaultQueryParameters populates default values inside query parameters, while ensuring types are respected +func populateDefaultQueryParameters(q url.Values, parameterName string, value any, explode bool) { + switch t := value.(type) { + case []any: + if explode { + appendToQueryValues(q, parameterName, t) + } else { + q.Add(parameterName, joinValues(t, ",")) + } + default: + q.Add(parameterName, fmt.Sprintf("%v", value)) + } } // ValidateParameter validates a parameter's value by JSON schema. @@ -127,7 +156,7 @@ func ValidateParameter(ctx context.Context, input *openapi3filter.RequestValidat options = &openapi3filter.Options{} } - var value interface{} + var value any var err error var found bool var schema *openapi3.Schema @@ -162,7 +191,8 @@ func ValidateParameter(ctx context.Context, input *openapi3filter.RequestValidat // Next check `parameter.Required && !found` will catch this. case openapi3.ParameterInQuery: q := req.URL.Query() - q.Add(parameter.Name, fmt.Sprintf("%v", value)) + explode := parameter.Explode != nil && *parameter.Explode + populateDefaultQueryParameters(q, parameter.Name, value, explode) req.URL.RawQuery = q.Encode() case openapi3.ParameterInHeader: req.Header.Add(parameter.Name, fmt.Sprintf("%v", value)) diff --git a/internal/platform/validator/validate_request_test.go b/internal/platform/validator/validate_request_test.go index c42c04be..31d6df74 100644 --- a/internal/platform/validator/validate_request_test.go +++ b/internal/platform/validator/validate_request_test.go @@ -259,7 +259,7 @@ func TestValidateQueryParams(t *testing.T) { name string param *openapi3.Parameter query string - want map[string]interface{} + want map[string]any err *openapi3.SchemaError // test ParseError in decoder tests } @@ -298,9 +298,9 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj][prop1][inexistent]=1", - want: map[string]interface{}{ - "obj": map[string]interface{}{ - "prop1": map[string]interface{}{}, + want: map[string]any{ + "obj": map[string]any{ + "prop1": map[string]any{}, }, }, }, @@ -317,9 +317,9 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj][prop1][item1]=1.123", - want: map[string]interface{}{ - "obj": map[string]interface{}{ - "prop1": map[string]interface{}{ + want: map[string]any{ + "obj": map[string]any{ + "prop1": map[string]any{ "item1": float64(1.123), }, }, @@ -347,7 +347,7 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "anotherparam=bar", - want: map[string]interface{}(nil), + want: map[string]any(nil), }, { name: "deepObject explode additionalProperties with object properties - multiple properties", @@ -359,15 +359,15 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj][prop1][item1]=1¶m[obj][prop1][item2][0]=abc¶m[obj][prop2][item1]=2¶m[obj][prop2][item2][0]=def", - want: map[string]interface{}{ - "obj": map[string]interface{}{ - "prop1": map[string]interface{}{ + want: map[string]any{ + "obj": map[string]any{ + "prop1": map[string]any{ "item1": int64(1), - "item2": []interface{}{"abc"}, + "item2": []any{"abc"}, }, - "prop2": map[string]interface{}{ + "prop2": map[string]any{ "item1": int64(2), - "item2": []interface{}{"def"}, + "item2": []any{"def"}, }, }, }, @@ -384,7 +384,7 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj]=1", - want: map[string]interface{}{ + want: map[string]any{ "obj": int64(1), }, }, @@ -397,7 +397,7 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj]=1", - want: map[string]interface{}{ + want: map[string]any{ "obj": int64(1), }, }, @@ -410,7 +410,7 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj]=true", - want: map[string]interface{}{ + want: map[string]any{ "obj": true, }, }, @@ -423,8 +423,8 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj][id2]=1¶m[obj][name2]=abc", - want: map[string]interface{}{ - "obj": map[string]interface{}{ + want: map[string]any{ + "obj": map[string]any{ "id2": "1", "name2": "abc", }, @@ -441,7 +441,7 @@ func TestValidateQueryParams(t *testing.T) { query: "param[obj][id]=1¶m[obj][id2]=2", err: &openapi3.SchemaError{ SchemaField: "oneOf", - Value: map[string]interface{}{"id": "1", "id2": "2"}, + Value: map[string]any{"id": "1", "id2": "2"}, Reason: "value matches more than one schema from \"oneOf\" (matches schemas at indices [0 1])", Schema: oneofSchemaObject.Value, }, @@ -455,8 +455,8 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj][0]=a¶m[obj][1]=b", - want: map[string]interface{}{ - "obj": []interface{}{ + want: map[string]any{ + "obj": []any{ "a", "b", }, diff --git a/internal/platform/validator/validate_response.go b/internal/platform/validator/validate_response.go index 50920715..d964b268 100644 --- a/internal/platform/validator/validate_response.go +++ b/internal/platform/validator/validate_response.go @@ -79,7 +79,7 @@ func ValidateResponse(ctx context.Context, input *openapi3filter.ResponseValidat } content := response.Content - if len(content) == 0 || options.ExcludeResponseBody { + if len(content) == 0 { // An operation does not contains a validation schema for responses with this status code. return nil } @@ -146,7 +146,7 @@ func ValidateResponse(ctx context.Context, input *openapi3filter.ResponseValidat func validateResponseHeader(headerName string, headerRef *openapi3.HeaderRef, input *openapi3filter.ResponseValidationInput, opts []openapi3.SchemaValidationOption) error { var err error - var decodedValue interface{} + var decodedValue any var found bool var sm *openapi3.SerializationMethod dec := &headerParamDecoder{header: input.Header} diff --git a/internal/platform/web/response.go b/internal/platform/web/response.go index 150bfd1c..d841c6ab 100644 --- a/internal/platform/web/response.go +++ b/internal/platform/web/response.go @@ -76,7 +76,7 @@ func GetDecompressedRequestBody(req *fasthttp.Request, contentEncoding string) ( } // Respond converts a Go value to JSON and sends it to the client. -func Respond(ctx *fasthttp.RequestCtx, data interface{}, statusCode int) error { +func Respond(ctx *fasthttp.RequestCtx, data any, statusCode int) error { // If there is nothing to marshal then set status code and return. if statusCode == http.StatusNoContent { ctx.SetStatusCode(statusCode)