Skip to content

Commit

Permalink
openapi3filter: deepObject array of objects and array of arrays suppo…
Browse files Browse the repository at this point in the history
…rt (#923)

* fix array of primitives query parameter types

* update tests

* fix parameter checks

* add test cases

* update

* update

* deepget

* work on deep set

* working on array of object deep set

* update

* fix current deepset

* attempt array of arrays

* fix deepset - TODO support deeply nested keys via deepget and recursion

* notes for simpler deepset and deepget

* deepset and deepget tests update

* bring back previous deepset and get

* remove duplicate tests with old deepset

* nested array of object test

* deepset construct obj

* intermediate array building

* broken

* update

* update

* fix array of object parsing

* FIXME error messages in tests

* update errors

* update - need error updates

* update some tests

* build array of primitives properly

* dont support implicit array index based on param position

* additional props

* test update

* notes for early return when params not set

* update

* update tests

* FIXME - ignore unset nullable params

* remove empty params from obj

* update tests

* fix tests

* array of arrays check

* dont error on unset keys if addit properties

* additional properties with object properties

* test additional properties

* notes for out of scope validation in decoder

* start moving test

* remove some param validation from decoder

* allow empty map elements

* allow array of arrays

* clean and use primitive return val

* let decoder return wrong values and fix tests

* should bring back array index missing error

* update tests

* validate array indexes are set at decoder level and full coverage

* bring back test

* address issues

* use exp slices

* remove old fn

* remove duplicate parse test

* dont test ParseError in request validation

* oneof anyof allof draft

* primitives anyof oneof allof decode

* pending objects

* notes

* update decode for keywords

* drop exp dependency
  • Loading branch information
danicc097 committed Mar 22, 2024
1 parent 7f46bdf commit 78bb273
Show file tree
Hide file tree
Showing 4 changed files with 862 additions and 197 deletions.
261 changes: 165 additions & 96 deletions openapi3filter/req_resp_decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.Serialization
if err != nil {
return nil, ok, err
}

val, err := makeObject(props, schema)
return val, ok, err
}
Expand Down Expand Up @@ -654,9 +655,7 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization
case l == 0:
// A query parameter's name does not match the required format, so skip it.
continue
case l == 1:
props[matches[0][1]] = strings.Join(values, urlDecoderDelimiter)
case l > 1:
case l >= 1:
kk := []string{}
for _, m := range matches {
kk = append(kk, m[1])
Expand All @@ -680,7 +679,6 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization
if props == nil {
return nil, false, nil
}

val, err := makeObject(props, schema)
if err != nil {
return nil, false, err
Expand Down Expand Up @@ -896,127 +894,198 @@ func deepSet(m map[string]interface{}, keys []string, value interface{}) {
func findNestedSchema(parentSchema *openapi3.SchemaRef, keys []string) (*openapi3.SchemaRef, error) {
currentSchema := parentSchema
for _, key := range keys {
propertySchema, ok := currentSchema.Value.Properties[key]
if !ok {
if currentSchema.Value.AdditionalProperties.Schema == nil {
return nil, fmt.Errorf("nested schema for key %q not found", key)
if currentSchema.Value.Type.Includes(openapi3.TypeArray) {
currentSchema = currentSchema.Value.Items
} else {
propertySchema, ok := currentSchema.Value.Properties[key]
if !ok {
if currentSchema.Value.AdditionalProperties.Schema == nil {
return nil, fmt.Errorf("nested schema for key %q not found", key)
}
currentSchema = currentSchema.Value.AdditionalProperties.Schema
continue
}
currentSchema = currentSchema.Value.AdditionalProperties.Schema
continue
currentSchema = propertySchema
}
currentSchema = propertySchema
}
return currentSchema, nil
}

// makeObject returns an object that contains properties from props.
// A value of every property is parsed as a primitive value.
// The function returns an error when an error happened while parse object's properties.
func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string]interface{}, error) {
obj := make(map[string]interface{})

for propName, propSchema := range schema.Value.Properties {
switch {
case propSchema.Value.Type.Is("array"):
vals := strings.Split(props[propName], urlDecoderDelimiter)
for _, v := range vals {
_, err := parsePrimitive(v, propSchema.Value.Items)
if err != nil {
return nil, handlePropParseError([]string{propName}, err)
}
}
mobj := make(map[string]interface{})

ivals, err := convertArrayParameterToType(vals, propSchema.Value.Items.Value.Type)
if err != nil {
return nil, handlePropParseError([]string{propName}, err)
}
obj[propName] = ivals
case propSchema.Value.Type.Is("object"):
for prop := range props {
if !strings.HasPrefix(prop, propName+urlDecoderDelimiter) {
continue
}
mapKeys := strings.Split(prop, urlDecoderDelimiter)
nestedSchema, err := findNestedSchema(schema, mapKeys)
if err != nil {
return nil, &ParseError{path: pathFromKeys(mapKeys), Reason: err.Error()}
}
if nestedSchema.Value.Type.Permits("array") {
vals := strings.Split(props[prop], urlDecoderDelimiter)
for _, v := range vals {
_, err := parsePrimitive(v, nestedSchema.Value.Items)
if err != nil {
return nil, handlePropParseError(mapKeys, err)
}
}
ivals, err := convertArrayParameterToType(vals, nestedSchema.Value.Items.Value.Type)
if err != nil {
return nil, handlePropParseError(mapKeys, err)
}
deepSet(obj, mapKeys, ivals)
continue
}
value, err := parsePrimitive(props[prop], nestedSchema)
if err != nil {
return nil, handlePropParseError(mapKeys, err)
}
deepSet(obj, mapKeys, value)
}
default:
value, err := parsePrimitive(props[propName], propSchema)
if err != nil {
return nil, handlePropParseError([]string{propName}, err)
}
obj[propName] = value
for kk, value := range props {
keys := strings.Split(kk, urlDecoderDelimiter)
if strings.Contains(value, urlDecoderDelimiter) {
// don't support implicit array indexes anymore
p := pathFromKeys(keys)
return nil, &ParseError{path: p, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}
}
deepSet(mobj, keys, value)
}
r, err := buildResObj(mobj, nil, "", schema)
if err != nil {
return nil, err
}
result, ok := r.(map[string]interface{})
if !ok {
return nil, &ParseError{Kind: KindOther, Reason: "invalid param object", Value: result}
}

return obj, nil
return result, nil
}

func convertArrayParameterToType(strArray []string, typ *openapi3.Types) (interface{}, error) {
var iarr []interface{}
// 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{}

keys := make([]int, 0, len(m))
for k := range m {
key, err := strconv.Atoi(k)
if err != nil {
return nil, fmt.Errorf("array indexes must be integers: %w", err)
}
keys = append(keys, key)
}
max := -1
for _, k := range keys {
if k > max {
max = k
}
}
for i := 0; i <= max; i++ {
val, ok := m[strconv.Itoa(i)]
if !ok {
result = append(result, nil)
continue
}
result = append(result, val)
}
return result, nil
}

// 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) {
mapKeys := parentKeys
if key != "" {
mapKeys = append(mapKeys, key)
}

switch {
case typ.Permits(openapi3.TypeBoolean):
for _, str := range strArray {
if str == "" {
continue
}
parsedBool, err := strconv.ParseBool(str)
case schema.Value.Type.Is("array"):
paramArr, ok := deepGet(params, mapKeys...)
if !ok {
return nil, nil
}
t, isMap := paramArr.(map[string]interface{})
if !isMap {
return nil, &ParseError{path: pathFromKeys(mapKeys), Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}
}
// intermediate arrays have to be instantiated
arr, err := sliceMapToSlice(t)
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{}, len(arr))
for i := range arr {
r, err := buildResObj(params, mapKeys, strconv.Itoa(i), schema.Value.Items)
if err != nil {
return nil, err
}
iarr = append(iarr, parsedBool)
}
case typ.Permits(openapi3.TypeInteger):
for _, str := range strArray {
if str == "" {
continue
if r != nil {
resultArr[i] = r
}
parsedInt, err := strconv.Atoi(str)
}
return resultArr, nil
case schema.Value.Type.Is("object"):
resultMap := make(map[string]interface{})
additPropsSchema := schema.Value.AdditionalProperties.Schema
pp, _ := deepGet(params, mapKeys...)
objectParams, ok := pp.(map[string]interface{})
if !ok {
// not the expected type, but return it either way and leave validation up to ValidateParameter
return pp, nil
}
for k, propSchema := range schema.Value.Properties {
r, err := buildResObj(params, mapKeys, k, propSchema)
if err != nil {
return nil, err
}
iarr = append(iarr, parsedInt)
if r != nil {
resultMap[k] = r
}
}
if additPropsSchema != nil {
// dynamic creation of possibly nested objects
for k := range objectParams {
r, err := buildResObj(params, mapKeys, k, additPropsSchema)
if err != nil {
return nil, err
}
if r != nil {
resultMap[k] = r
}
}
}

return resultMap, nil
case len(schema.Value.AnyOf) > 0:
return buildFromSchemas(schema.Value.AnyOf, params, parentKeys, key)
case len(schema.Value.OneOf) > 0:
return buildFromSchemas(schema.Value.OneOf, params, parentKeys, key)
case len(schema.Value.AllOf) > 0:
return buildFromSchemas(schema.Value.AllOf, params, parentKeys, key)
default:
val, ok := deepGet(params, mapKeys...)
if !ok {
// leave validation up to ValidateParameter. here there really is not parameter set
return nil, nil
}
v, ok := val.(string)
if !ok {
return nil, &ParseError{path: pathFromKeys(mapKeys), Kind: KindInvalidFormat, Value: val, Reason: "path is not convertible to primitive"}
}
case typ.Permits(openapi3.TypeNumber):
for _, str := range strArray {
if str == "" {
prim, err := parsePrimitive(v, schema)
if err != nil {
return nil, handlePropParseError(mapKeys, err)
}

return prim, nil
}
}

// 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{})
for _, s := range schemas {
val, err := buildResObj(params, mapKeys, key, s)
if err == nil && val != nil {

if m, ok := val.(map[string]interface{}); ok {
for k, v := range m {
resultMap[k] = v
}
continue
}
parsedFloat, err := strconv.ParseFloat(str, 64)
if err != nil {
return nil, err

if a, ok := val.([]interface{}); ok {
if len(a) > 0 {
return a, nil
}
continue
}
iarr = append(iarr, parsedFloat)

// if its a primitive and not nil just return that and let it be validated
return val, nil
}
case typ.Permits(openapi3.TypeString):
return strArray, nil
default:
return nil, fmt.Errorf("unsupported parameter array type: %s", typ)
}

return iarr, nil
if len(resultMap) > 0 {
return resultMap, nil
}

return nil, nil
}

func handlePropParseError(path []string, err error) error {
Expand Down

0 comments on commit 78bb273

Please sign in to comment.