Skip to content

Commit 78bb273

Browse files
authored
openapi3filter: deepObject array of objects and array of arrays support (#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
1 parent 7f46bdf commit 78bb273

File tree

4 files changed

+862
-197
lines changed

4 files changed

+862
-197
lines changed

openapi3filter/req_resp_decoder.go

+165-96
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.Serialization
469469
if err != nil {
470470
return nil, ok, err
471471
}
472+
472473
val, err := makeObject(props, schema)
473474
return val, ok, err
474475
}
@@ -654,9 +655,7 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization
654655
case l == 0:
655656
// A query parameter's name does not match the required format, so skip it.
656657
continue
657-
case l == 1:
658-
props[matches[0][1]] = strings.Join(values, urlDecoderDelimiter)
659-
case l > 1:
658+
case l >= 1:
660659
kk := []string{}
661660
for _, m := range matches {
662661
kk = append(kk, m[1])
@@ -680,7 +679,6 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization
680679
if props == nil {
681680
return nil, false, nil
682681
}
683-
684682
val, err := makeObject(props, schema)
685683
if err != nil {
686684
return nil, false, err
@@ -896,127 +894,198 @@ func deepSet(m map[string]interface{}, keys []string, value interface{}) {
896894
func findNestedSchema(parentSchema *openapi3.SchemaRef, keys []string) (*openapi3.SchemaRef, error) {
897895
currentSchema := parentSchema
898896
for _, key := range keys {
899-
propertySchema, ok := currentSchema.Value.Properties[key]
900-
if !ok {
901-
if currentSchema.Value.AdditionalProperties.Schema == nil {
902-
return nil, fmt.Errorf("nested schema for key %q not found", key)
897+
if currentSchema.Value.Type.Includes(openapi3.TypeArray) {
898+
currentSchema = currentSchema.Value.Items
899+
} else {
900+
propertySchema, ok := currentSchema.Value.Properties[key]
901+
if !ok {
902+
if currentSchema.Value.AdditionalProperties.Schema == nil {
903+
return nil, fmt.Errorf("nested schema for key %q not found", key)
904+
}
905+
currentSchema = currentSchema.Value.AdditionalProperties.Schema
906+
continue
903907
}
904-
currentSchema = currentSchema.Value.AdditionalProperties.Schema
905-
continue
908+
currentSchema = propertySchema
906909
}
907-
currentSchema = propertySchema
908910
}
909911
return currentSchema, nil
910912
}
911913

912914
// makeObject returns an object that contains properties from props.
913-
// A value of every property is parsed as a primitive value.
914-
// The function returns an error when an error happened while parse object's properties.
915915
func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string]interface{}, error) {
916-
obj := make(map[string]interface{})
917-
918-
for propName, propSchema := range schema.Value.Properties {
919-
switch {
920-
case propSchema.Value.Type.Is("array"):
921-
vals := strings.Split(props[propName], urlDecoderDelimiter)
922-
for _, v := range vals {
923-
_, err := parsePrimitive(v, propSchema.Value.Items)
924-
if err != nil {
925-
return nil, handlePropParseError([]string{propName}, err)
926-
}
927-
}
916+
mobj := make(map[string]interface{})
928917

929-
ivals, err := convertArrayParameterToType(vals, propSchema.Value.Items.Value.Type)
930-
if err != nil {
931-
return nil, handlePropParseError([]string{propName}, err)
932-
}
933-
obj[propName] = ivals
934-
case propSchema.Value.Type.Is("object"):
935-
for prop := range props {
936-
if !strings.HasPrefix(prop, propName+urlDecoderDelimiter) {
937-
continue
938-
}
939-
mapKeys := strings.Split(prop, urlDecoderDelimiter)
940-
nestedSchema, err := findNestedSchema(schema, mapKeys)
941-
if err != nil {
942-
return nil, &ParseError{path: pathFromKeys(mapKeys), Reason: err.Error()}
943-
}
944-
if nestedSchema.Value.Type.Permits("array") {
945-
vals := strings.Split(props[prop], urlDecoderDelimiter)
946-
for _, v := range vals {
947-
_, err := parsePrimitive(v, nestedSchema.Value.Items)
948-
if err != nil {
949-
return nil, handlePropParseError(mapKeys, err)
950-
}
951-
}
952-
ivals, err := convertArrayParameterToType(vals, nestedSchema.Value.Items.Value.Type)
953-
if err != nil {
954-
return nil, handlePropParseError(mapKeys, err)
955-
}
956-
deepSet(obj, mapKeys, ivals)
957-
continue
958-
}
959-
value, err := parsePrimitive(props[prop], nestedSchema)
960-
if err != nil {
961-
return nil, handlePropParseError(mapKeys, err)
962-
}
963-
deepSet(obj, mapKeys, value)
964-
}
965-
default:
966-
value, err := parsePrimitive(props[propName], propSchema)
967-
if err != nil {
968-
return nil, handlePropParseError([]string{propName}, err)
969-
}
970-
obj[propName] = value
918+
for kk, value := range props {
919+
keys := strings.Split(kk, urlDecoderDelimiter)
920+
if strings.Contains(value, urlDecoderDelimiter) {
921+
// don't support implicit array indexes anymore
922+
p := pathFromKeys(keys)
923+
return nil, &ParseError{path: p, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}
971924
}
925+
deepSet(mobj, keys, value)
926+
}
927+
r, err := buildResObj(mobj, nil, "", schema)
928+
if err != nil {
929+
return nil, err
930+
}
931+
result, ok := r.(map[string]interface{})
932+
if !ok {
933+
return nil, &ParseError{Kind: KindOther, Reason: "invalid param object", Value: result}
972934
}
973935

974-
return obj, nil
936+
return result, nil
975937
}
976938

977-
func convertArrayParameterToType(strArray []string, typ *openapi3.Types) (interface{}, error) {
978-
var iarr []interface{}
939+
// example: map[0:map[key:true] 1:map[key:false]] -> [map[key:true] map[key:false]]
940+
func sliceMapToSlice(m map[string]interface{}) ([]interface{}, error) {
941+
var result []interface{}
942+
943+
keys := make([]int, 0, len(m))
944+
for k := range m {
945+
key, err := strconv.Atoi(k)
946+
if err != nil {
947+
return nil, fmt.Errorf("array indexes must be integers: %w", err)
948+
}
949+
keys = append(keys, key)
950+
}
951+
max := -1
952+
for _, k := range keys {
953+
if k > max {
954+
max = k
955+
}
956+
}
957+
for i := 0; i <= max; i++ {
958+
val, ok := m[strconv.Itoa(i)]
959+
if !ok {
960+
result = append(result, nil)
961+
continue
962+
}
963+
result = append(result, val)
964+
}
965+
return result, nil
966+
}
967+
968+
// buildResObj constructs an object based on a given schema and param values
969+
func buildResObj(params map[string]interface{}, parentKeys []string, key string, schema *openapi3.SchemaRef) (interface{}, error) {
970+
mapKeys := parentKeys
971+
if key != "" {
972+
mapKeys = append(mapKeys, key)
973+
}
974+
979975
switch {
980-
case typ.Permits(openapi3.TypeBoolean):
981-
for _, str := range strArray {
982-
if str == "" {
983-
continue
984-
}
985-
parsedBool, err := strconv.ParseBool(str)
976+
case schema.Value.Type.Is("array"):
977+
paramArr, ok := deepGet(params, mapKeys...)
978+
if !ok {
979+
return nil, nil
980+
}
981+
t, isMap := paramArr.(map[string]interface{})
982+
if !isMap {
983+
return nil, &ParseError{path: pathFromKeys(mapKeys), Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}
984+
}
985+
// intermediate arrays have to be instantiated
986+
arr, err := sliceMapToSlice(t)
987+
if err != nil {
988+
return nil, &ParseError{path: pathFromKeys(mapKeys), Kind: KindInvalidFormat, Reason: fmt.Sprintf("could not convert value map to array: %v", err)}
989+
}
990+
resultArr := make([]interface{}, len(arr))
991+
for i := range arr {
992+
r, err := buildResObj(params, mapKeys, strconv.Itoa(i), schema.Value.Items)
986993
if err != nil {
987994
return nil, err
988995
}
989-
iarr = append(iarr, parsedBool)
990-
}
991-
case typ.Permits(openapi3.TypeInteger):
992-
for _, str := range strArray {
993-
if str == "" {
994-
continue
996+
if r != nil {
997+
resultArr[i] = r
995998
}
996-
parsedInt, err := strconv.Atoi(str)
999+
}
1000+
return resultArr, nil
1001+
case schema.Value.Type.Is("object"):
1002+
resultMap := make(map[string]interface{})
1003+
additPropsSchema := schema.Value.AdditionalProperties.Schema
1004+
pp, _ := deepGet(params, mapKeys...)
1005+
objectParams, ok := pp.(map[string]interface{})
1006+
if !ok {
1007+
// not the expected type, but return it either way and leave validation up to ValidateParameter
1008+
return pp, nil
1009+
}
1010+
for k, propSchema := range schema.Value.Properties {
1011+
r, err := buildResObj(params, mapKeys, k, propSchema)
9971012
if err != nil {
9981013
return nil, err
9991014
}
1000-
iarr = append(iarr, parsedInt)
1015+
if r != nil {
1016+
resultMap[k] = r
1017+
}
1018+
}
1019+
if additPropsSchema != nil {
1020+
// dynamic creation of possibly nested objects
1021+
for k := range objectParams {
1022+
r, err := buildResObj(params, mapKeys, k, additPropsSchema)
1023+
if err != nil {
1024+
return nil, err
1025+
}
1026+
if r != nil {
1027+
resultMap[k] = r
1028+
}
1029+
}
1030+
}
1031+
1032+
return resultMap, nil
1033+
case len(schema.Value.AnyOf) > 0:
1034+
return buildFromSchemas(schema.Value.AnyOf, params, parentKeys, key)
1035+
case len(schema.Value.OneOf) > 0:
1036+
return buildFromSchemas(schema.Value.OneOf, params, parentKeys, key)
1037+
case len(schema.Value.AllOf) > 0:
1038+
return buildFromSchemas(schema.Value.AllOf, params, parentKeys, key)
1039+
default:
1040+
val, ok := deepGet(params, mapKeys...)
1041+
if !ok {
1042+
// leave validation up to ValidateParameter. here there really is not parameter set
1043+
return nil, nil
1044+
}
1045+
v, ok := val.(string)
1046+
if !ok {
1047+
return nil, &ParseError{path: pathFromKeys(mapKeys), Kind: KindInvalidFormat, Value: val, Reason: "path is not convertible to primitive"}
10011048
}
1002-
case typ.Permits(openapi3.TypeNumber):
1003-
for _, str := range strArray {
1004-
if str == "" {
1049+
prim, err := parsePrimitive(v, schema)
1050+
if err != nil {
1051+
return nil, handlePropParseError(mapKeys, err)
1052+
}
1053+
1054+
return prim, nil
1055+
}
1056+
}
1057+
1058+
// buildFromSchemas decodes params with anyOf, oneOf, allOf schemas.
1059+
func buildFromSchemas(schemas openapi3.SchemaRefs, params map[string]interface{}, mapKeys []string, key string) (interface{}, error) {
1060+
resultMap := make(map[string]interface{})
1061+
for _, s := range schemas {
1062+
val, err := buildResObj(params, mapKeys, key, s)
1063+
if err == nil && val != nil {
1064+
1065+
if m, ok := val.(map[string]interface{}); ok {
1066+
for k, v := range m {
1067+
resultMap[k] = v
1068+
}
10051069
continue
10061070
}
1007-
parsedFloat, err := strconv.ParseFloat(str, 64)
1008-
if err != nil {
1009-
return nil, err
1071+
1072+
if a, ok := val.([]interface{}); ok {
1073+
if len(a) > 0 {
1074+
return a, nil
1075+
}
1076+
continue
10101077
}
1011-
iarr = append(iarr, parsedFloat)
1078+
1079+
// if its a primitive and not nil just return that and let it be validated
1080+
return val, nil
10121081
}
1013-
case typ.Permits(openapi3.TypeString):
1014-
return strArray, nil
1015-
default:
1016-
return nil, fmt.Errorf("unsupported parameter array type: %s", typ)
10171082
}
10181083

1019-
return iarr, nil
1084+
if len(resultMap) > 0 {
1085+
return resultMap, nil
1086+
}
1087+
1088+
return nil, nil
10201089
}
10211090

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

0 commit comments

Comments
 (0)