package path
import "time"
func Equal(obj any, path string, matchStr string) (bool, error) {
return op(obj, path, matchStr, equal)
}
func equal(obj, match any, _ string) bool {
switch objt := obj.(type) {
case time.Time:
tm := match.(*timeVal)
// TODO: Replace Truncate() with a timezone-aware version
return tm.time.Equal(objt.Truncate(tm.precision))
default:
return obj == match
}
}
package path
import (
"time"
"cloud.google.com/go/civil"
)
func Greater(obj any, path string, matchStr string) (bool, error) {
return op(obj, path, matchStr, greater)
}
func greater(obj, match any, _ string) bool {
switch objt := obj.(type) {
case int:
return objt > match.(int)
case int64:
return objt > match.(int64)
case uint:
return objt > match.(uint)
case uint64:
return objt > match.(uint64)
case float32:
return objt > match.(float32)
case float64:
return objt > match.(float64)
case string:
return objt > match.(string)
case bool:
return objt && !match.(bool)
case time.Time:
tm := match.(*timeVal)
return objt.Truncate(tm.precision).After(tm.time)
case civil.Date:
return objt.After(match.(civil.Date))
default:
panic(obj)
}
}
package path
import (
"time"
"cloud.google.com/go/civil"
)
func GreaterEqual(obj any, path string, matchStr string) (bool, error) {
return op(obj, path, matchStr, greaterEqual)
}
func greaterEqual(obj, match any, _ string) bool {
switch objt := obj.(type) {
case int:
return objt >= match.(int)
case int64:
return objt >= match.(int64)
case uint:
return objt >= match.(uint)
case uint64:
return objt >= match.(uint64)
case float32:
return objt >= match.(float32)
case float64:
return objt >= match.(float64)
case string:
return objt >= match.(string)
case bool:
return objt || objt == match.(bool)
case time.Time:
tm := match.(*timeVal)
trunc := objt.Truncate(tm.precision)
return trunc.Equal(tm.time) || trunc.After(tm.time)
case civil.Date:
return objt == match.(civil.Date) || objt.After(match.(civil.Date))
default:
panic(obj)
}
}
package path
import (
"strconv"
"strings"
"time"
"cloud.google.com/go/civil"
)
func HasPrefix(obj any, path string, matchStr string) (bool, error) {
return op(obj, path, matchStr, hasPrefix)
}
func hasPrefix(obj, match any, matchStr string) bool {
var objStr string
switch objt := obj.(type) {
case int:
objStr = strconv.FormatInt(int64(objt), 10)
case int64:
objStr = strconv.FormatInt(objt, 10)
case uint:
objStr = strconv.FormatUint(uint64(objt), 10)
case uint64:
objStr = strconv.FormatUint(objt, 10)
case float32:
objStr = strconv.FormatFloat(float64(objt), 'f', -1, 32)
case float64:
objStr = strconv.FormatFloat(objt, 'f', -1, 64)
case string:
objStr = objt
case bool:
objStr = strconv.FormatBool(objt)
case time.Time:
objStr = objt.String()
case civil.Date:
objStr = objt.String()
default:
panic(obj)
}
return strings.HasPrefix(objStr, matchStr)
}
package path
func In(obj any, path string, matchStr string) (bool, error) {
return opList(obj, path, matchStr, equal)
}
package path
import (
"time"
"cloud.google.com/go/civil"
)
func Less(obj any, path string, matchStr string) (bool, error) {
return op(obj, path, matchStr, less)
}
func less(obj, match any, _ string) bool {
switch objt := obj.(type) {
case int:
return objt < match.(int)
case int64:
return objt < match.(int64)
case uint:
return objt < match.(uint)
case uint64:
return objt < match.(uint64)
case float32:
return objt < match.(float32)
case float64:
return objt < match.(float64)
case string:
return objt < match.(string)
case bool:
return !objt && match.(bool)
case time.Time:
tm := match.(*timeVal)
return objt.Truncate(tm.precision).Before(tm.time)
case civil.Date:
return objt.Before(match.(civil.Date))
default:
panic(obj)
}
}
package path
import (
"time"
"cloud.google.com/go/civil"
)
func LessEqual(obj any, path string, matchStr string) (bool, error) {
return op(obj, path, matchStr, lessEqual)
}
func lessEqual(obj, match any, _ string) bool {
switch objt := obj.(type) {
case int:
return objt <= match.(int)
case int64:
return objt <= match.(int64)
case uint:
return objt <= match.(uint)
case uint64:
return objt <= match.(uint64)
case float32:
return objt <= match.(float32)
case float64:
return objt <= match.(float64)
case string:
return objt <= match.(string)
case bool:
return !objt || objt == match.(bool)
case time.Time:
tm := match.(*timeVal)
trunc := objt.Truncate(tm.precision)
return trunc.Equal(tm.time) || trunc.Before(tm.time)
case civil.Date:
return objt == match.(civil.Date) || objt.Before(match.(civil.Date))
default:
panic(obj)
}
}
package path
import (
"encoding/json"
"reflect"
"github.com/gopatchy/jsrest"
)
func Merge(to, from any) {
MergeValue(reflect.ValueOf(to), reflect.ValueOf(from))
}
func MergeValue(to, from reflect.Value) {
to = reflect.Indirect(to)
from = reflect.Indirect(from)
for i := 0; i < to.NumField(); i++ {
toField := to.Field(i)
fromField := from.Field(i)
if fromField.IsZero() {
continue
}
if reflect.Indirect(fromField).Kind() == reflect.Struct {
MergeValue(toField, fromField)
continue
}
toField.Set(fromField)
}
}
func MergeMap(to any, from map[string]any) error {
m, err := ToMap(to)
if err != nil {
return jsrest.Errorf(jsrest.ErrInternalServerError, "converting to map failed (%w)", err)
}
MergeMaps(m, from)
return FromMap(to, m)
}
func MergeMaps(to map[string]any, from map[string]any) {
for k, v := range from {
if vMap, isMap := v.(map[string]any); isMap {
if _, ok := to[k].(map[string]any); !ok {
// Either key doesn't exist or it's a different type
// If different type, error will happen during json decode
to[k] = map[string]any{}
}
MergeMaps(to[k].(map[string]any), vMap)
} else {
to[k] = v
}
}
}
func ToMap(from any) (map[string]any, error) {
js, err := json.Marshal(from)
if err != nil {
return nil, jsrest.Errorf(jsrest.ErrInternalServerError, "json marshal failed (%w)", err)
}
ret := map[string]any{}
err = json.Unmarshal(js, &ret)
if err != nil {
return nil, jsrest.Errorf(jsrest.ErrInternalServerError, "json unmarshal failed (%w)", err)
}
return ret, nil
}
func FromMap(to any, from map[string]any) error {
js, err := json.Marshal(from)
if err != nil {
return jsrest.Errorf(jsrest.ErrInternalServerError, "json marshal failed (%w)", err)
}
err = json.Unmarshal(js, to)
if err != nil {
return jsrest.Errorf(jsrest.ErrInternalServerError, "json unmarshal failed (%w)", err)
}
return nil
}
package path
import (
"strings"
)
func op(obj any, path string, matchStr string, cb func(any, any, string) bool) (bool, error) {
objVal, err := Get(obj, path)
if err != nil {
return false, err
}
matchVal, err := parse(matchStr, objVal)
if err != nil {
return false, err
}
if isSlice(objVal) {
return anyTrue(objVal, func(x any, _ int) bool { return cb(x, matchVal, matchStr) }), nil
}
return cb(objVal, matchVal, matchStr), nil
}
func opList(obj any, path string, matchStr string, cb func(any, any, string) bool) (bool, error) {
objVal, err := Get(obj, path)
if err != nil {
return false, err
}
if objVal == nil {
return false, nil
}
matchVal := []any{}
matchParts := strings.Split(matchStr, ",")
for _, matchPart := range matchParts {
matchTmp, err := parse(matchPart, objVal)
if err != nil {
return false, err
}
matchVal = append(matchVal, matchTmp)
}
return anyTrue(matchVal, func(y any, i int) bool {
str := matchParts[i]
if isSlice(objVal) {
return anyTrue(objVal, func(x any, _ int) bool { return cb(x, y, str) })
}
return cb(objVal, y, str)
}), nil
}
package path
import (
"errors"
"reflect"
"strconv"
"strings"
"time"
"cloud.google.com/go/civil"
"github.com/gopatchy/jsrest"
)
type timeVal struct {
time time.Time
precision time.Duration
}
var (
ErrUnsupportedType = errors.New("unsupported type")
ErrUnknownTimeFormat = errors.New("unknown time format")
)
func parse(str string, t any) (any, error) {
typ := reflect.TypeOf(t)
if typ.Kind() == reflect.Slice {
typ = typ.Elem()
}
if typ.Kind() == reflect.Pointer {
typ = typ.Elem()
}
// TODO: Consider attempting to convert to string in default case
switch typ.Kind() { //nolint:exhaustive
case reflect.Int:
return parseInt(str)
case reflect.Int64:
return strconv.ParseInt(str, 10, 64)
case reflect.Uint:
return parseUint(str)
case reflect.Uint64:
return strconv.ParseUint(str, 10, 64)
case reflect.Float32:
return parseFloat32(str)
case reflect.Float64:
return strconv.ParseFloat(str, 64)
case reflect.String:
return str, nil
case reflect.Bool:
return strconv.ParseBool(str)
case reflect.Struct:
switch typ {
case reflect.TypeOf(time.Time{}):
return parseTime(str)
case reflect.TypeOf(civil.Date{}):
return civil.ParseDate(str)
}
}
return nil, jsrest.Errorf(jsrest.ErrBadRequest, "%T (%w)", t, ErrUnsupportedType)
}
func parseInt(str string) (int, error) {
val, err := strconv.ParseInt(str, 10, strconv.IntSize)
return int(val), err
}
func parseUint(str string) (uint, error) {
val, err := strconv.ParseUint(str, 10, strconv.IntSize)
return uint(val), err
}
func parseFloat32(str string) (float32, error) {
val, err := strconv.ParseFloat(str, 32)
return float32(val), err
}
type timeFormat struct {
format string
precision time.Duration
}
var timeFormats = []timeFormat{
{
format: "2006-01-02-07:00",
precision: 24 * time.Hour,
// TODO: Support field annotation to change start vs end of day
// TODO: Support timezone context passed down to allow naked date
},
{
format: "2006-01-02T15:04:05Z",
precision: 1 * time.Second,
},
{
format: "2006-01-02T15:04:05-07:00",
precision: 1 * time.Second,
},
}
func parseTime(str string) (*timeVal, error) {
if strings.ToLower(str) == "now" {
return &timeVal{
time: time.Now(),
precision: 1 * time.Nanosecond,
}, nil
}
for _, format := range timeFormats {
tm, err := time.Parse(format.format, str)
if err != nil {
continue
}
return &timeVal{
time: tm,
precision: format.precision,
}, nil
}
i, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return nil, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", str, ErrUnknownTimeFormat)
}
// UNIX Seconds: 2969-05-03
// UNIX Millis: 1971-01-01
// Intended to give us a wide range of useful values in both schemes
if i > 31536000000 {
return &timeVal{
time: time.UnixMilli(i),
precision: 1 * time.Millisecond,
}, nil
}
return &timeVal{
time: time.Unix(i, 0),
precision: 1 * time.Second,
}, nil
}
package path
import (
"errors"
"fmt"
"reflect"
"sort"
"strings"
"time"
"cloud.google.com/go/civil"
"github.com/gopatchy/jsrest"
"golang.org/x/exp/slices"
)
type WalkCallback func(string, []string, reflect.StructField)
var (
TimeTimeType = reflect.TypeOf(time.Time{})
CivilDateType = reflect.TypeOf(civil.Date{})
ErrNotAStruct = errors.New("not a struct")
ErrUnknownFieldName = errors.New("unknown field name")
)
func Get(obj any, path string) (any, error) {
v, err := GetValue(reflect.ValueOf(obj), path)
if err != nil {
return nil, err
}
return v.Interface(), nil
}
func GetValue(v reflect.Value, path string) (reflect.Value, error) {
parts := strings.Split(path, ".")
return getRecursive(v, parts, []string{})
}
func getRecursive(v reflect.Value, parts []string, prev []string) (reflect.Value, error) {
if v.Kind() == reflect.Pointer {
if v.IsNil() {
v = reflect.Zero(v.Type().Elem())
} else {
v = reflect.Indirect(v)
}
}
if len(parts) == 0 {
return v, nil
}
if v.Kind() != reflect.Struct {
return reflect.Value{}, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", strings.Join(prev, "."), ErrNotAStruct)
}
part := parts[0]
sub, found := getField(v, part)
if !found {
return reflect.Value{}, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", errorPath(prev, part), ErrUnknownFieldName)
}
newPrev := []string{}
newPrev = append(newPrev, prev...)
newPrev = append(newPrev, part)
return getRecursive(sub, parts[1:], newPrev)
}
func getField(v reflect.Value, name string) (reflect.Value, bool) {
field, found := getStructField(v.Type(), name)
if !found {
return reflect.Value{}, false
}
return v.FieldByName(field.Name), true
}
func Set(obj any, path string, val string) error {
return SetValue(reflect.ValueOf(obj), path, val)
}
func SetValue(v reflect.Value, path string, val string) error {
parts := strings.Split(path, ".")
return setRecursive(v, parts, []string{}, val)
}
func setRecursive(v reflect.Value, parts []string, prev []string, val string) error {
if v.Kind() == reflect.Pointer {
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
v = reflect.Indirect(v)
}
if len(parts) == 0 {
n, err := parse(val, v.Interface())
if err != nil {
return err
}
if _, ok := n.(*timeVal); ok {
n = n.(*timeVal).time
}
v.Set(reflect.ValueOf(n))
return nil
}
if v.Kind() != reflect.Struct {
return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", strings.Join(prev, "."), ErrNotAStruct)
}
part := parts[0]
sub, found := getField(v, part)
if !found {
return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", errorPath(prev, part), ErrUnknownFieldName)
}
newPrev := []string{}
newPrev = append(newPrev, prev...)
newPrev = append(newPrev, part)
return setRecursive(sub, parts[1:], newPrev, val)
}
func List(obj any) []string {
return ListType(reflect.TypeOf(obj))
}
func ListType(t reflect.Type) []string {
list := []string{}
WalkType(t, func(path string, _ []string, field reflect.StructField) {
t := MaybeIndirectType(field.Type)
if t.Kind() == reflect.Struct && t != TimeTimeType && t != CivilDateType {
return
}
list = append(list, path)
})
sort.Strings(list)
return list
}
func GetFieldType(t reflect.Type, path string) reflect.Type {
parts := strings.Split(path, ".")
for _, part := range parts {
field, found := getStructField(MaybeIndirectType(t), part)
if !found {
return nil
}
t = field.Type
}
return t
}
func FindTagValueType(t reflect.Type, key, value string) (string, bool) {
ret := ""
WalkType(t, func(path string, _ []string, field reflect.StructField) {
tag, found := field.Tag.Lookup(key)
if !found {
return
}
parts := strings.Split(tag, ",")
if slices.Contains(parts, value) {
ret = path
}
})
return ret, ret != ""
}
func Walk(obj any, cb WalkCallback) {
WalkType(reflect.TypeOf(obj), cb)
}
func WalkType(t reflect.Type, cb WalkCallback) {
walkRecursive(MaybeIndirectType(t), cb, []string{})
}
func walkRecursive(t reflect.Type, cb WalkCallback, prev []string) {
for i := 0; i < t.NumField(); i++ {
sub := t.Field(i)
newPrev := []string{}
newPrev = append(newPrev, prev...)
if !sub.Anonymous {
newPrev = append(newPrev, FieldName(sub))
}
t := MaybeIndirectType(sub.Type)
if len(newPrev) > 0 {
cb(strings.Join(newPrev, "."), newPrev, sub)
}
if t.Kind() == reflect.Struct && t != TimeTimeType && t != CivilDateType {
walkRecursive(t, cb, newPrev)
}
}
}
func getStructField(t reflect.Type, name string) (reflect.StructField, bool) {
name = strings.ToLower(name)
return t.FieldByNameFunc(func(iterName string) bool {
iterField, iterOK := t.FieldByName(iterName)
if !iterOK {
panic(iterName)
}
return strings.ToLower(FieldName(iterField)) == name
})
}
func errorPath(prev []string, part string) string {
if len(prev) == 0 {
return part
}
return fmt.Sprintf("%s.%s", strings.Join(prev, "."), part)
}
func FieldName(field reflect.StructField) string {
tag := field.Tag.Get("json")
if tag != "" {
if tag == "-" {
return ""
}
parts := strings.SplitN(tag, ",", 2)
return parts[0]
}
return field.Name
}
func MaybeIndirectType(t reflect.Type) reflect.Type {
if t.Kind() == reflect.Pointer {
return t.Elem()
}
return t
}
package path
import "reflect"
func isSlice(v any) bool {
return reflect.TypeOf(v).Kind() == reflect.Slice
}
func anyTrue(v any, cb func(any, int) bool) bool {
val := reflect.ValueOf(v)
for i := 0; i < val.Len(); i++ {
sub := val.Index(i)
if sub.Kind() == reflect.Pointer && sub.IsNil() {
continue
}
sub = reflect.Indirect(sub)
if cb(sub.Interface(), i) {
return true
}
}
return false
}
package path
import (
"errors"
"reflect"
"sort"
"time"
"cloud.google.com/go/civil"
"github.com/gopatchy/jsrest"
)
func Sort(objs any, path string) error {
as := newAnySlice(objs, path)
sort.Stable(as)
return as.err
}
func SortReverse(objs any, path string) error {
as := newAnySlice(objs, path)
sort.Stable(sort.Reverse(as))
return as.err
}
type anySlice struct {
path string
slice reflect.Value
swapper func(i, j int)
err error
}
var ErrUnsupportedSortType = errors.New("unsupported _sort type")
func newAnySlice(objs any, path string) *anySlice {
return &anySlice{
path: path,
slice: reflect.ValueOf(objs),
swapper: reflect.Swapper(objs),
}
}
func (as *anySlice) Len() int {
return as.slice.Len()
}
func (as *anySlice) Less(i, j int) bool {
v1, err := Get(as.slice.Index(i).Interface(), as.path)
if err != nil {
as.err = err
// We have to obey the Less() contract even in error cases
return i < j
}
v2, err := Get(as.slice.Index(j).Interface(), as.path)
if err != nil {
as.err = err
return i < j
}
switch {
case v1 == nil && v2 == nil:
return false
case v1 == nil:
return true
case v2 == nil:
return false
}
switch t1 := v1.(type) {
case int:
return t1 < v2.(int)
case int64:
return t1 < v2.(int64)
case uint:
return t1 < v2.(uint)
case uint64:
return t1 < v2.(uint64)
case float32:
return t1 < v2.(float32)
case float64:
return t1 < v2.(float64)
case string:
return t1 < v2.(string)
case bool:
return !t1 && v2.(bool)
case time.Time:
return t1.Before(v2.(time.Time))
case civil.Date:
return t1.Before(v2.(civil.Date))
default:
as.err = jsrest.Errorf(jsrest.ErrBadRequest, "%s: %T (%w)", as.path, t1, ErrUnsupportedSortType)
return i < j
}
}
func (as *anySlice) Swap(i, j int) {
as.swapper(i, j)
}