package magic import ( "fmt" "net/http" "reflect" "regexp" "strconv" "strings" ) var ( cachedStructQueryFields = make(map[reflect.Type][]structQueryFields) cachedUnsupportedQueryFields = make(map[reflect.Type]structQueryFields) ) type Query[T any] struct { Data T } type structQueryFields struct { PathKey string FieldKey string Type reflect.Type MinQuery, MaxQuery int Match *regexp.Regexp } type UnsupportedQueryType struct { inner reflect.Type } func (err UnsupportedQueryType) Error() string { return "unsupported type for query: " + err.inner.String() } type QueryValueNotFitIn struct { Count int field structQueryFields } func (err QueryValueNotFitIn) Error() string { return fmt.Sprintf("query %v is expected to has %v~%v values, got %v", err.field.PathKey, err.field.MinQuery, err.field.MaxQuery, err.Count) } type InvalidQueryValue struct { Match *regexp.Regexp Value string } func (err InvalidQueryValue) Error() string { return fmt.Sprintf("value not matched with regexp %v: %v", err.Match, err.Value) } func findReflectQueryFields(v reflect.Value) ([]structQueryFields, error) { if cached, ok := cachedStructQueryFields[v.Type()]; ok { return cached, nil } if cached, ok := cachedUnsupportedQueryFields[v.Type()]; ok { return nil, UnsupportedPathValueType{inner: cached.Type} } var fields []structQueryFields 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("query"); 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 maxQuery, minQuery := 1, 1 if fieldType.Kind() == reflect.Ptr { fieldType = fieldType.Elem() minQuery = 0 } else if fieldType.Kind() == reflect.Array { maxQuery, minQuery = fieldType.Len(), fieldType.Len() fieldType = fieldType.Elem() } else if fieldType.Kind() == reflect.Slice { fieldType = fieldType.Elem() maxQuery, minQuery = -1, 0 } if pathValueConvertTable[fieldType.Kind()] == nil { if _, ok := fieldType.MethodByName("FromString"); !ok { cachedUnsupportedPathValueFields[t] = field return nil, UnsupportedPathValueType{inner: field.Type} } } fields = append(fields, structQueryFields{ PathKey: pathKey, FieldKey: field.Name, Type: fieldType, MaxQuery: maxQuery, MinQuery: minQuery, Match: match, }) cachedStructQueryFields[t] = fields } } else { return nil, UnsupportedQueryType{inner: v.Type()} } return fields, nil } func (query *Query[T]) FromRequest(r *http.Request) error { v := reflect.ValueOf(query).Elem().FieldByName("Data") fields, err := findReflectQueryFields(v) if err != nil { return err } q := r.URL.Query() for _, field := range fields { values := q[field.PathKey] if len(values) < field.MinQuery || (field.MaxQuery > 0 && len(values) > field.MaxQuery) { return QueryValueNotFitIn{Count: len(values), field: field} } structField := v.FieldByName(field.FieldKey) if structField.Kind() == reflect.Slice && structField.Len() < len(values) { if structField.Cap() < len(values) { structField.SetCap(len(values)) } structField.SetLen(len(values)) } for idx, value := range values { if structField.Kind() != reflect.Array && structField.Kind() != reflect.Slice { if idx != len(values)-1 { // only the last takes effect continue } } // check match regexp if field.Match != nil && !field.Match.MatchString(value) { return InvalidPathValue{Match: field.Match, Value: value} } // convert to elem value convert := pathValueConvertTable[field.Type.Kind()] result, err := convert(value) if err != nil { return err } reflectValue := reflect.ValueOf(result) if structField.Kind() != reflect.Array && structField.Kind() != reflect.Slice { if field.MinQuery == 0 { newReflectValue := reflect.New(field.Type) newReflectValue.Elem().Set(reflectValue) reflectValue = newReflectValue } structField.Set(reflectValue) } else { structField.Index(idx).Set(reflectValue) } } } return nil }