mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-07-31 17:59:49 -04:00
feat(search): derive a case-insensitive field-name index from the resource struct
mapping.FieldNameIndex walks the struct and maps a lowercased field path to the real field name, including nested facet sub-fields. Backend-neutral.
This commit is contained in:
36
services/search/pkg/mapping/fieldindex.go
Normal file
36
services/search/pkg/mapping/fieldindex.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package mapping
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FieldNameIndex maps a lowercased field path to the real field name for every
|
||||
// field of t, recursing into nested facets (photo.cameraMake, ...). Names come
|
||||
// from json tags, so it is backend-neutral; the query layer resolves KQL keys
|
||||
// case-insensitively against it.
|
||||
func FieldNameIndex(t reflect.Type, overrides map[string]FieldOpts) map[string]string {
|
||||
out := map[string]string{}
|
||||
var walk func(t reflect.Type, prefix string)
|
||||
walk = func(t reflect.Type, prefix string) {
|
||||
_ = walkFields(t, func(fi fieldInfo) error {
|
||||
name := fi.Name
|
||||
if prefix != "" {
|
||||
name = prefix + "." + fi.Name
|
||||
}
|
||||
out[strings.ToLower(name)] = name
|
||||
|
||||
fieldType := overrides[name].Type
|
||||
if fieldType == "" {
|
||||
fieldType = inferType(fi.GoField.Type)
|
||||
}
|
||||
// recurse into nested facets; time.Time is a struct too but a leaf.
|
||||
if sub := structType(fi.GoField.Type); sub != nil && fieldType != TypeDatetime {
|
||||
walk(sub, name)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
walk(t, "")
|
||||
return out
|
||||
}
|
||||
79
services/search/pkg/mapping/fieldindex_test.go
Normal file
79
services/search/pkg/mapping/fieldindex_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package mapping_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/services/search/pkg/mapping"
|
||||
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
|
||||
)
|
||||
|
||||
func resourceFieldIndex(testing.TB) map[string]string {
|
||||
return mapping.FieldNameIndex(
|
||||
reflect.TypeFor[search.Resource](),
|
||||
search.Resource{}.SearchFieldOverrides(),
|
||||
)
|
||||
}
|
||||
|
||||
// resolve looks up the lowercased key in the index, falling back to the key.
|
||||
func resolve(idx map[string]string, key string) string {
|
||||
if v, ok := idx[strings.ToLower(key)]; ok {
|
||||
return v
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func TestFieldNameIndex_TopLevelCaseInsensitive(t *testing.T) {
|
||||
idx := resourceFieldIndex(t)
|
||||
for in, want := range map[string]string{
|
||||
"rootid": "RootID", "ROOTID": "RootID", "RootID": "RootID",
|
||||
"name": "Name", "NAME": "Name",
|
||||
"mimetype": "MimeType", "MimeType": "MimeType",
|
||||
"tags": "Tags", "favorites": "Favorites",
|
||||
"mtime": "Mtime", "parentid": "ParentID", "id": "ID",
|
||||
} {
|
||||
require.Equalf(t, want, resolve(idx, in), "resolve(%q)", in)
|
||||
}
|
||||
}
|
||||
|
||||
// Facet sub-fields are lowerCamelCase in the index (from libregraph json tags);
|
||||
// the derived index resolves them case-insensitively.
|
||||
func TestFieldNameIndex_FacetsCaseInsensitive(t *testing.T) {
|
||||
idx := resourceFieldIndex(t)
|
||||
for in, want := range map[string]string{
|
||||
// case-insensitive: same field, different casings
|
||||
"photo.cameramake": "photo.cameraMake",
|
||||
"photo.CAMERAMAKE": "photo.cameraMake",
|
||||
// a representative sub-field across each facet
|
||||
"photo.takendatetime": "photo.takenDateTime",
|
||||
"audio.artist": "audio.artist",
|
||||
"audio.albumartist": "audio.albumArtist",
|
||||
"image.width": "image.width",
|
||||
"location.latitude": "location.latitude",
|
||||
} {
|
||||
require.Equalf(t, want, resolve(idx, in), "resolve(%q)", in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldNameIndex_UnknownPassesThrough(t *testing.T) {
|
||||
idx := resourceFieldIndex(t)
|
||||
require.Equal(t, "nope.field", resolve(idx, "nope.field"))
|
||||
require.Equal(t, "custom", resolve(idx, "custom"))
|
||||
}
|
||||
|
||||
// All top-level fields are covered from one derived source, so both backends
|
||||
// resolve them the same way.
|
||||
func TestFieldNameIndex_CoversAllTopLevelFields(t *testing.T) {
|
||||
idx := resourceFieldIndex(t)
|
||||
for in, want := range map[string]string{
|
||||
"rootid": "RootID", "path": "Path", "id": "ID", "name": "Name",
|
||||
"size": "Size", "mtime": "Mtime", "type": "Type",
|
||||
"content": "Content", "hidden": "Hidden", "tags": "Tags",
|
||||
"favorites": "Favorites",
|
||||
} {
|
||||
require.Equalf(t, want, resolve(idx, in), "derived should cover %q", in)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user