diff --git a/dbs/dbs.go b/dbs/dbs.go index 003a771..5f89c1f 100644 --- a/dbs/dbs.go +++ b/dbs/dbs.go @@ -165,7 +165,7 @@ func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *Fil And: make(map[string][]Filter), Or: make(map[string][]Filter), } - paths := jsonToBsonPaths(reflect.TypeOf(target), "") + paths := jsonToBsonPaths(reflect.TypeOf(target), "", "") resolve := func(jsonKey string) string { if p, ok := paths[jsonKey]; ok { return p @@ -215,11 +215,22 @@ func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *Fil // // Anonymous embedded fields without any tag follow the BSON convention of this // codebase: they are stored as a nested sub-document whose key is the lowercased -// struct type name (e.g. utils.AbstractObject → "abstractobject"). -func jsonToBsonPaths(t reflect.Type, prefix string) map[string]string { +// struct type name (e.g. utils.AbstractObject → "abstractobject"). Their JSON +// fields are promoted (flat), so bsonPrefix advances but jsonPrefix does not. +// +// For fields inside slices or maps, both the leaf json name and the full dotted +// json path (e.g. "instances.access_protocol") are registered as keys so callers +// can use either form unambiguously. +func jsonToBsonPaths(t reflect.Type, bsonPrefix string, jsonPrefix string) map[string]string { for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice { t = t.Elem() } + if t.Kind() == reflect.Map { + t = t.Elem() + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + } result := make(map[string]string) if t.Kind() != reflect.Struct { return result @@ -233,20 +244,21 @@ func jsonToBsonPaths(t reflect.Type, prefix string) map[string]string { bsonName := strings.Split(bsonTag, ",")[0] // Anonymous embedded struct with no tags: use lowercase type name as BSON prefix. + // JSON fields are promoted so jsonPrefix stays the same. if field.Anonymous && jsonName == "" && bsonName == "" { ft := field.Type for ft.Kind() == reflect.Ptr { ft = ft.Elem() } if ft.Kind() == reflect.Struct { - embedPrefix := strings.ToLower(ft.Name()) + embedBsonPrefix := strings.ToLower(ft.Name()) re := regexp.MustCompile(`\[[^\]]*\]`) - embedPrefix = re.ReplaceAllString(embedPrefix, "") - embedPrefix = strings.ReplaceAll(embedPrefix, "*", "") - if prefix != "" { - embedPrefix = prefix + "." + embedPrefix + embedBsonPrefix = re.ReplaceAllString(embedBsonPrefix, "") + embedBsonPrefix = strings.ReplaceAll(embedBsonPrefix, "*", "") + if bsonPrefix != "" { + embedBsonPrefix = bsonPrefix + "." + embedBsonPrefix } - for k, v := range jsonToBsonPaths(ft, embedPrefix) { + for k, v := range jsonToBsonPaths(ft, embedBsonPrefix, jsonPrefix) { if _, exists := result[k]; !exists { result[k] = v } @@ -262,19 +274,36 @@ func jsonToBsonPaths(t reflect.Type, prefix string) map[string]string { bsonName = jsonName } - fullPath := bsonName - if prefix != "" { - fullPath = prefix + "." + bsonName + fullBsonPath := bsonName + if bsonPrefix != "" { + fullBsonPath = bsonPrefix + "." + bsonName + } + fullJsonPath := jsonName + if jsonPrefix != "" { + fullJsonPath = jsonPrefix + "." + jsonName } - result[jsonName] = fullPath + result[jsonName] = fullBsonPath + // Also register the full dotted JSON path so callers can use + // "instances.access_protocol" instead of just "access_protocol". + if fullJsonPath != jsonName { + if _, exists := result[fullJsonPath]; !exists { + result[fullJsonPath] = fullBsonPath + } + } ft := field.Type for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice { ft = ft.Elem() } + if ft.Kind() == reflect.Map { + ft = ft.Elem() + for ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + } if ft.Kind() == reflect.Struct { - for k, v := range jsonToBsonPaths(ft, fullPath) { + for k, v := range jsonToBsonPaths(ft, fullBsonPath, fullJsonPath) { if _, exists := result[k]; !exists { result[k] = v }