package cache import ( "errors" "github.com/iancoleman/strcase" "meow.tf/go/cacheinterface/v2/encoder" "net/url" "reflect" "strconv" "strings" "time" ) // decodeQuery is a helper that behaves like gorilla/schema, // encoding/json, etc. It takes in a url.Values and decodes it into the // dest struct - assigning defaults if nothing is populated. func decodeQuery(query url.Values, dest any) error { destType := reflect.TypeOf(dest) if destType.Kind() == reflect.Ptr { destType = destType.Elem() } destVal := reflect.ValueOf(dest) if destVal.Kind() == reflect.Ptr { destVal = destVal.Elem() } for i := 0; i < destType.NumField(); i++ { fieldType := destType.Field(i) field := destVal.Field(i) queryKeys := []string{ fieldType.Tag.Get("query"), strcase.ToLowerCamel(fieldType.Name), } for _, key := range queryKeys { if key == "" || !query.Has(key) { continue } // Map query variable to default field name val, err := decodeType(fieldType.Type, query[key], false) if err != nil { return err } field.Set(reflect.ValueOf(val)) break } // TODO: ignoring bool might not be the best choice. // Maybe use bools as either pointers, or setup a "NullBool" like sql.Null*? canHaveDefault := field.IsZero() && field.Kind() != reflect.Bool if def := fieldType.Tag.Get("default"); canHaveDefault && def != "" { val, err := decodeType(fieldType.Type, []string{def}, true) if err != nil { return err } field.Set(reflect.ValueOf(val)) } } return nil } var ( encoderType = reflect.TypeOf((*encoder.Encoder)(nil)).Elem() durationType = reflect.TypeOf(time.Duration(0)) ) func decodeType(t reflect.Type, val []string, isDefault bool) (any, error) { switch t { case encoderType: return encoder.From(val[0]), nil case durationType: return time.ParseDuration(val[0]) } switch t.Kind() { case reflect.Bool: v, err := strconv.ParseBool(val[0]) if err != nil { return nil, err } return v, nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: v, err := strconv.ParseInt(val[0], 10, 64) if err != nil { return nil, err } switch t.Kind() { case reflect.Int: return int(v), nil case reflect.Int8: return int8(v), nil case reflect.Int16: return int16(v), nil case reflect.Int32: return int32(v), nil case reflect.Int64: return v, nil } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: v, err := strconv.ParseUint(val[0], 10, 64) if err != nil { return nil, err } switch t.Kind() { case reflect.Uint: return uint(v), nil case reflect.Uint8: return uint8(v), nil case reflect.Uint16: return uint16(v), nil case reflect.Uint32: return uint32(v), nil case reflect.Uint64: return v, nil } case reflect.Float32, reflect.Float64: v, err := strconv.ParseFloat(val[0], 64) if err != nil { return nil, err } switch t.Kind() { case reflect.Float32: return float32(v), nil case reflect.Float64: return v, nil } case reflect.String: return val[0], nil case reflect.Slice: elemType := t.Elem() if isDefault { val = strings.Split(val[0], ",") } out := reflect.MakeSlice(t, 0, len(val)) for _, v := range val { decodedVal, err := decodeType(elemType, []string{v}, isDefault) if err != nil { return nil, err } out = reflect.Append(out, reflect.ValueOf(decodedVal)) } return out.Interface(), nil } return nil, errors.New("unknown type " + t.String()) }