photoprism/internal/form/serialize.go

167 lines
4.3 KiB
Go

package form
import (
"fmt"
"reflect"
"strconv"
"strings"
"time"
"unicode"
"github.com/photoprism/photoprism/pkg/sanitize"
"github.com/araddon/dateparse"
"github.com/photoprism/photoprism/pkg/txt"
)
// Serialize returns a string containing all non-empty fields and values of a struct.
func Serialize(f interface{}, all bool) string {
v := reflect.ValueOf(f)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return ""
}
q := make([]string, 0, v.NumField())
// Iterate through all form fields.
for i := 0; i < v.NumField(); i++ {
fieldValue := v.Field(i)
fieldName := v.Type().Field(i).Tag.Get("form")
fieldInfo := v.Type().Field(i).Tag.Get("serialize")
// Serialize field values as string.
if fieldName != "" && (fieldInfo != "-" || all) {
switch t := fieldValue.Interface().(type) {
case time.Time:
if val := fieldValue.Interface().(time.Time); !val.IsZero() {
if val.Hour() == 0 && val.Minute() == 0 {
q = append(q, fmt.Sprintf("%s:%s", fieldName, val.Format("2006-01-02")))
} else {
q = append(q, fmt.Sprintf("%s:\"%s\"", fieldName, val.String()))
}
}
case int, int8, int16, int32, int64:
if val := fieldValue.Int(); val != 0 {
q = append(q, fmt.Sprintf("%s:%d", fieldName, val))
}
case uint, uint8, uint16, uint32, uint64:
if val := fieldValue.Uint(); val != 0 {
q = append(q, fmt.Sprintf("%s:%d", fieldName, val))
}
case float32, float64:
if val := fieldValue.Float(); val != 0 {
q = append(q, fmt.Sprintf("%s:%f", fieldName, val))
}
case string:
if val := strings.ReplaceAll(fieldValue.String(), "\"", ""); val != "" {
if strings.ContainsAny(val, " :'()[]-+`") {
q = append(q, fmt.Sprintf("%s:\"%s\"", fieldName, val))
} else {
q = append(q, fmt.Sprintf("%s:%s", fieldName, val))
}
}
case bool:
if val := fieldValue.Bool(); val {
q = append(q, fmt.Sprintf("%s:%t", fieldName, fieldValue.Bool()))
}
default:
log.Warnf("form: failed serializing %T %s", t, sanitize.Token(fieldName))
}
}
}
return strings.Join(q, " ")
}
func Unserialize(f SearchForm, q string) (result error) {
var key, value []rune
var escaped, isKeyValue bool
f.SetQuery("")
formValues := reflect.ValueOf(f).Elem()
q = strings.TrimSpace(q) + "\n"
var queryStrings []string
for _, char := range q {
if unicode.IsSpace(char) && !escaped {
if isKeyValue {
fieldName := strings.Title(string(key))
field := formValues.FieldByNameFunc(func(name string) bool {
return strings.EqualFold(name, fieldName)
})
stringValue := string(value)
if field.CanSet() {
switch field.Interface().(type) {
case time.Time:
if timeValue, err := dateparse.ParseAny(stringValue); err != nil {
result = err
} else {
field.Set(reflect.ValueOf(timeValue))
}
case float32, float64:
if floatValue, err := strconv.ParseFloat(stringValue, 64); err != nil {
result = err
} else {
field.SetFloat(floatValue)
}
case int, int8, int16, int32, int64:
if intValue, err := strconv.Atoi(stringValue); err != nil {
result = err
} else {
field.SetInt(int64(intValue))
}
case uint, uint8, uint16, uint32, uint64:
if intValue, err := strconv.Atoi(stringValue); err != nil {
result = err
} else {
field.SetUint(uint64(intValue))
}
case string:
field.SetString(sanitize.SearchString(stringValue))
case bool:
field.SetBool(txt.Bool(stringValue))
default:
result = fmt.Errorf("unsupported type: %s", fieldName)
}
} else {
result = fmt.Errorf("unknown filter: %s", fieldName)
}
} else if len(strings.TrimSpace(string(key))) > 0 {
queryStrings = append(queryStrings, strings.TrimSpace(string(key)))
}
escaped = false
isKeyValue = false
key = key[:0]
value = value[:0]
} else if char == ':' && !escaped {
isKeyValue = true
} else if char == '"' {
escaped = !escaped
} else if isKeyValue {
value = append(value, char)
} else {
key = append(key, unicode.ToLower(char))
}
}
if len(queryStrings) > 0 {
f.SetQuery(sanitize.SearchQuery(strings.Join(queryStrings, " ")))
}
if result != nil {
log.Warnf("form: failed parsing search query")
}
return result
}