1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-03-19 14:17:48 +02:00
2024-09-29 21:09:46 +03:00

185 lines
4.0 KiB
Go

package picker
import (
"encoding/json"
"strings"
"github.com/pocketbase/pocketbase/tools/search"
"github.com/pocketbase/pocketbase/tools/tokenizer"
)
// Pick converts data into a []any, map[string]any, etc. (using json marshal->unmarshal)
// containing only the fields from the parsed rawFields expression.
//
// rawFields is a comma separated string of the fields to include.
// Nested fields should be listed with dot-notation.
// Fields value modifiers are also supported using the `:modifier(args)` format (see Modifiers).
//
// Example:
//
// data := map[string]any{"a": 1, "b": 2, "c": map[string]any{"c1": 11, "c2": 22}}
// Pick(data, "a,c.c1") // map[string]any{"a": 1, "c": map[string]any{"c1": 11}}
func Pick(data any, rawFields string) (any, error) {
parsedFields, err := parseFields(rawFields)
if err != nil {
return nil, err
}
// marshalize the provided data to ensure that the related json.Marshaler
// implementations are invoked, and then convert it back to a plain
// json value that we can further operate on.
//
// @todo research other approaches to avoid the double serialization
// ---
encoded, err := json.Marshal(data)
if err != nil {
return nil, err
}
var decoded any
if err := json.Unmarshal(encoded, &decoded); err != nil {
return nil, err
}
// ---
// special cases to preserve the same fields format when used with single item or search results data.
var isSearchResult bool
switch data.(type) {
case search.Result, *search.Result:
isSearchResult = true
}
if isSearchResult {
if decodedMap, ok := decoded.(map[string]any); ok {
pickParsedFields(decodedMap["items"], parsedFields)
}
} else {
pickParsedFields(decoded, parsedFields)
}
return decoded, nil
}
func parseFields(rawFields string) (map[string]Modifier, error) {
t := tokenizer.NewFromString(rawFields)
fields, err := t.ScanAll()
if err != nil {
return nil, err
}
result := make(map[string]Modifier, len(fields))
for _, f := range fields {
parts := strings.SplitN(strings.TrimSpace(f), ":", 2)
if len(parts) > 1 {
m, err := initModifer(parts[1])
if err != nil {
return nil, err
}
result[parts[0]] = m
} else {
result[parts[0]] = nil
}
}
return result, nil
}
func pickParsedFields(data any, fields map[string]Modifier) error {
switch v := data.(type) {
case map[string]any:
pickMapFields(v, fields)
case []map[string]any:
for _, item := range v {
if err := pickMapFields(item, fields); err != nil {
return err
}
}
case []any:
if len(v) == 0 {
return nil // nothing to pick
}
if _, ok := v[0].(map[string]any); !ok {
return nil // for now ignore non-map values
}
for _, item := range v {
if err := pickMapFields(item.(map[string]any), fields); err != nil {
return nil
}
}
}
return nil
}
func pickMapFields(data map[string]any, fields map[string]Modifier) error {
if len(fields) == 0 {
return nil // nothing to pick
}
if m, ok := fields["*"]; ok {
// append all missing root level data keys
for k := range data {
var exists bool
for f := range fields {
if strings.HasPrefix(f+".", k+".") {
exists = true
break
}
}
if !exists {
fields[k] = m
}
}
}
DataLoop:
for k := range data {
matchingFields := make(map[string]Modifier, len(fields))
for f, m := range fields {
if strings.HasPrefix(f+".", k+".") {
matchingFields[f] = m
continue
}
}
if len(matchingFields) == 0 {
delete(data, k)
continue DataLoop
}
// remove the current key from the matching fields path
for f, m := range matchingFields {
remains := strings.TrimSuffix(strings.TrimPrefix(f+".", k+"."), ".")
// final key
if remains == "" {
if m != nil {
var err error
data[k], err = m.Modify(data[k])
if err != nil {
return err
}
}
continue DataLoop
}
// cleanup the old field key and continue with the rest of the field path
delete(matchingFields, f)
matchingFields[remains] = m
}
if err := pickParsedFields(data[k], matchingFields); err != nil {
return err
}
}
return nil
}