package magic import ( "fmt" "net/http" "reflect" "regexp" "strconv" "strings" ) var ( cachedStructPathValueFields = make(map[reflect.Type][]structPathValueFields) cachedUnsupportedPathValueFields = make(map[reflect.Type]reflect.StructField) ) type UnsupportedPathValueType struct { inner reflect.Type } func (err UnsupportedPathValueType) Error() string { return "unsupported type for path value: " + err.inner.String() } type structPathValueFields struct { PathKey string FieldKey string Type reflect.Type Optional bool Match *regexp.Regexp } type PathValue[T any] struct { Data T } type PathValueNotFound struct { Key string } func (err PathValueNotFound) Error() string { return "path value not found: " + err.Key } type InvalidPathValueType struct { Kind reflect.Kind Value string } func (err InvalidPathValueType) Error() string { return fmt.Sprintf("invalid value for kind %v: %v", err.Kind, err.Value) } type InvalidPathValue struct { Match *regexp.Regexp Value string } func (err InvalidPathValue) Error() string { return fmt.Sprintf("value not matched with regexp %v: %v", err.Match, err.Value) } func findReflectPathValueFields(v reflect.Value) ([]structPathValueFields, error) { if cached, ok := cachedStructPathValueFields[v.Type()]; ok { return cached, nil } if cached, ok := cachedUnsupportedPathValueFields[v.Type()]; ok { return nil, UnsupportedPathValueType{inner: cached.Type} } var fields []structPathValueFields if v.Kind() == reflect.Struct { t := v.Type() for idx := 0; idx < t.NumField(); idx++ { field := t.Field(idx) pathKey := field.Name var match *regexp.Regexp if tags, ok := field.Tag.Lookup("pathvalue"); ok { parts := strings.Split(strings.TrimSpace(tags), ",") if len(parts) == 0 { // do nothing } else { if parts[0] != "" { pathKey = parts[0] } for _, part := range parts[1:] { part = strings.TrimSpace(part) if strings.HasPrefix(part, "match=") { part := strings.TrimPrefix(part, "match=") if unquoted, err := strconv.Unquote(part); err != nil { return nil, err } else { part = unquoted } exp, err := regexp.Compile(part) if err != nil { return nil, err } match = exp } } } } fieldType := field.Type optional := false if fieldType.Kind() == reflect.Ptr { fieldType = fieldType.Elem() optional = true } if pathValueConvertTable[fieldType.Kind()] == nil { if _, ok := fieldType.MethodByName("FromString"); !ok { cachedUnsupportedPathValueFields[t] = field return nil, UnsupportedPathValueType{inner: field.Type} } } fields = append(fields, structPathValueFields{ PathKey: pathKey, FieldKey: field.Name, Type: fieldType, Optional: optional, Match: match, }) } cachedStructPathValueFields[t] = fields } else { return nil, UnsupportedPathValueType{inner: v.Type()} } return fields, nil } func (pathValue *PathValue[T]) FromRequest(r *http.Request) error { v := reflect.ValueOf(pathValue).Elem().FieldByName("Data") fields, err := findReflectPathValueFields(v) if err != nil { return err } for _, field := range fields { str := r.PathValue(field.PathKey) if str == "" { if !field.Optional { return PathValueNotFound{Key: field.PathKey} } continue } if field.Match != nil && !field.Match.MatchString(str) { return InvalidPathValue{Match: field.Match, Value: str} } convert := pathValueConvertTable[field.Type.Kind()] result, err := convert(str) if err != nil { return err } reflectValue := reflect.ValueOf(result) if field.Optional { newReflectedValue := reflect.New(reflectValue.Type()) newReflectedValue.Elem().Set(reflect.ValueOf(result)) reflectValue = newReflectedValue } v.FieldByName(field.FieldKey).Set(reflectValue) } return nil }