202 lines
4.7 KiB
Go
202 lines
4.7 KiB
Go
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
|
|
}
|