2025-02-22 23:06:16 +08:00

179 lines
3.9 KiB
Go

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
}