initial commit
This commit is contained in:
169
magic/extractors.go
Normal file
169
magic/extractors.go
Normal file
@ -0,0 +1,169 @@
|
||||
package magic
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"git.jeffthecoder.xyz/public/lazyhandler/util"
|
||||
)
|
||||
|
||||
var (
|
||||
extractors = make(map[reflect.Type]func(*http.Request) (any, error))
|
||||
|
||||
extractorsTakesResponseWriter = make(map[reflect.Type]func(http.ResponseWriter, *http.Request) (any, error))
|
||||
)
|
||||
|
||||
type FromRequest interface {
|
||||
FromRequest(*http.Request) error
|
||||
}
|
||||
|
||||
type TakeResponseWriter interface {
|
||||
TakeResponseWriter(http.ResponseWriter)
|
||||
}
|
||||
|
||||
func RegisterExtractor(o any, extractor func(*http.Request) (any, error)) {
|
||||
if t, ok := o.(reflect.Type); ok {
|
||||
slog.With("type", t.String()).Debug("extractor type registered with reflect.Type")
|
||||
extractors[t] = extractor
|
||||
} else if v, ok := o.(reflect.Value); ok {
|
||||
slog.With("type", v.Type().String(), "value", v.Interface()).Debug("extractor type registered with reflect.Value")
|
||||
|
||||
extractors[v.Type()] = extractor
|
||||
} else {
|
||||
t := reflect.TypeOf(o)
|
||||
slog.With("type", t.String(), "value", o).Debug("extractor type registered with object")
|
||||
|
||||
extractors[t] = extractor
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterExtractorGeneric[T any](extractor func(*http.Request) (any, error)) {
|
||||
var pointerToT *T
|
||||
RegisterExtractor(reflect.TypeOf(pointerToT).Elem(), extractor)
|
||||
}
|
||||
|
||||
func RegisterExtractorThatTakesResponseWriter(o any, extractor func(http.ResponseWriter, *http.Request) (any, error)) {
|
||||
if t, ok := o.(reflect.Type); ok {
|
||||
slog.With("type", t.String()).Debug("extractor type registered with reflect.Type")
|
||||
extractorsTakesResponseWriter[t] = extractor
|
||||
} else if v, ok := o.(reflect.Value); ok {
|
||||
slog.With("type", v.Type().String(), "value", v.Interface()).Debug("extractor type registered with reflect.Value")
|
||||
|
||||
extractorsTakesResponseWriter[v.Type()] = extractor
|
||||
} else {
|
||||
t := reflect.TypeOf(o)
|
||||
slog.With("type", t.String(), "value", o).Debug("extractor type registered with object")
|
||||
|
||||
extractorsTakesResponseWriter[t] = extractor
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterExtractorThatTakesResponseWriterGeneric[T any](extractor func(http.ResponseWriter, *http.Request) (any, error)) {
|
||||
var pointerToT *T
|
||||
RegisterExtractorThatTakesResponseWriter(reflect.TypeOf(pointerToT).Elem(), extractor)
|
||||
}
|
||||
|
||||
func GetExtractor(t reflect.Type) (func(http.ResponseWriter, *http.Request) (any, error), bool) {
|
||||
_, isTakeResponseWriter := util.Implements[TakeResponseWriter](t)
|
||||
if t.Kind() == reflect.Pointer {
|
||||
_, ptrIsTakeResponseWriter := util.Implements[TakeResponseWriter](t.Elem())
|
||||
isTakeResponseWriter = isTakeResponseWriter || ptrIsTakeResponseWriter
|
||||
}
|
||||
if _, ok := util.Implements[FromRequest](t); ok {
|
||||
if t.Kind() == reflect.Pointer {
|
||||
// T.Implement(FromRequest) and T is Pointer
|
||||
// create new actual T and call T.FromRequest on it
|
||||
// if error happens, no error is returned. just return nil
|
||||
return func(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
// var t T
|
||||
// return t, t.FromRequest(r)
|
||||
z := reflect.New(t.Elem())
|
||||
|
||||
if isTakeResponseWriter {
|
||||
z.MethodByName("TakeResponseWriter").Call([]reflect.Value{reflect.ValueOf(w)})
|
||||
}
|
||||
|
||||
results := z.MethodByName("FromRequest").Call([]reflect.Value{reflect.ValueOf(r)})
|
||||
if err, ok := results[0].Interface().(error); ok && err != nil {
|
||||
if errResponse, ok := err.(ErrorResponse); ok {
|
||||
return reflect.Zero(t).Interface(), errResponse
|
||||
}
|
||||
return reflect.Zero(t).Interface(), nil
|
||||
}
|
||||
return z.Interface(), nil
|
||||
}, isTakeResponseWriter
|
||||
}
|
||||
// T.Implement(FromRequest) and T is not Pointer
|
||||
// create zero T and call T.FromRequest directly on it
|
||||
return func(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
// var t T
|
||||
// return t, t.FromRequest(r)
|
||||
z := reflect.Zero(t)
|
||||
|
||||
if isTakeResponseWriter {
|
||||
z.MethodByName("TakeResponseWriter").Call([]reflect.Value{reflect.ValueOf(w)})
|
||||
}
|
||||
|
||||
results := z.MethodByName("FromRequest").Call([]reflect.Value{reflect.ValueOf(r)})
|
||||
if err, ok := results[0].Interface().(error); ok {
|
||||
return z.Interface(), err
|
||||
}
|
||||
return z.Interface(), nil
|
||||
}, isTakeResponseWriter
|
||||
} else if v, ok := util.PointerImplements[FromRequest](t); ok {
|
||||
return func(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
// var t T
|
||||
// return t, t.FromRequest(r)
|
||||
z := reflect.New(v.Type().Elem())
|
||||
|
||||
if isTakeResponseWriter {
|
||||
z.MethodByName("TakeResponseWriter").Call([]reflect.Value{reflect.ValueOf(w)})
|
||||
}
|
||||
|
||||
results := z.MethodByName("FromRequest").Call([]reflect.Value{reflect.ValueOf(r)})
|
||||
if err := results[0].Interface(); err == nil {
|
||||
return z.Elem().Interface(), nil
|
||||
}
|
||||
return z.Elem().Interface(), results[0].Interface().(error)
|
||||
}, isTakeResponseWriter
|
||||
}
|
||||
|
||||
if extractor, ok := extractors[t]; ok {
|
||||
return func(_ http.ResponseWriter, r *http.Request) (any, error) {
|
||||
return extractor(r)
|
||||
}, false
|
||||
}
|
||||
|
||||
if t.Kind() == reflect.Pointer {
|
||||
if extractor, ok := extractors[t.Elem()]; ok {
|
||||
return func(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
v, err := extractor(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
}, false
|
||||
}
|
||||
}
|
||||
|
||||
if extractor, ok := extractorsTakesResponseWriter[t]; ok {
|
||||
return func(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
return extractor(w, r)
|
||||
}, true
|
||||
}
|
||||
|
||||
if t.Kind() == reflect.Pointer {
|
||||
if extractor, ok := extractorsTakesResponseWriter[t.Elem()]; ok {
|
||||
return func(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
v, err := extractor(w, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
}, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, isTakeResponseWriter
|
||||
}
|
63
magic/json.go
Normal file
63
magic/json.go
Normal file
@ -0,0 +1,63 @@
|
||||
package magic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type JsonDecodeError struct {
|
||||
inner error
|
||||
}
|
||||
|
||||
func (err JsonDecodeError) Error() string {
|
||||
return "failed to decode json: " + err.inner.Error()
|
||||
}
|
||||
|
||||
func (err JsonDecodeError) WriteResponse(rw http.ResponseWriter) {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
type Json[T any] struct {
|
||||
Data T
|
||||
}
|
||||
|
||||
func NewJson[T any](data T) Json[T] {
|
||||
return Json[T]{
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (data *Json[T]) FromRequest(request *http.Request) error {
|
||||
bodyReader := request.Body
|
||||
defer bodyReader.Close()
|
||||
|
||||
request.Body = http.NoBody
|
||||
|
||||
if err := json.NewDecoder(bodyReader).Decode(&data.Data); err != nil {
|
||||
return JsonDecodeError{inner: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type counter struct {
|
||||
counter int64
|
||||
}
|
||||
|
||||
func (c *counter) Write(b []byte) (int, error) {
|
||||
n := len(b)
|
||||
c.counter += int64(n)
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (container Json[T]) Write(w io.Writer) (int64, error) {
|
||||
counter := &counter{}
|
||||
err := json.NewEncoder(io.MultiWriter(counter, w)).Encode(container.Data)
|
||||
return counter.counter, err
|
||||
}
|
||||
|
||||
func (json Json[T]) WriteResponse(w http.ResponseWriter) {
|
||||
json.Write(w)
|
||||
}
|
12
magic/map.go
Normal file
12
magic/map.go
Normal file
@ -0,0 +1,12 @@
|
||||
package magic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Map map[string]any
|
||||
|
||||
func (m Map) RespondWriter(w http.ResponseWriter) {
|
||||
json.NewEncoder(w).Encode(m)
|
||||
}
|
178
magic/path.go
Normal file
178
magic/path.go
Normal file
@ -0,0 +1,178 @@
|
||||
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
|
||||
}
|
111
magic/path_query_values.go
Normal file
111
magic/path_query_values.go
Normal file
@ -0,0 +1,111 @@
|
||||
package magic
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
pathValueConvertTable = map[reflect.Kind]func(string) (any, error){
|
||||
reflect.String: func(s string) (any, error) {
|
||||
return s, nil
|
||||
},
|
||||
reflect.Bool: func(s string) (any, error) {
|
||||
switch strings.ToLower(s) {
|
||||
case "0", "false", "no", "n":
|
||||
return false, nil
|
||||
case "1", "true", "yes", "y":
|
||||
return true, nil
|
||||
}
|
||||
return nil, InvalidPathValueType{Kind: reflect.Bool, Value: s}
|
||||
},
|
||||
|
||||
reflect.Float64: func(s string) (any, error) {
|
||||
if v, err := strconv.ParseFloat(s, 64); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
},
|
||||
reflect.Float32: func(s string) (any, error) {
|
||||
if v, err := strconv.ParseFloat(s, 32); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return float32(v), nil
|
||||
}
|
||||
},
|
||||
|
||||
reflect.Int: func(s string) (any, error) {
|
||||
if v, err := strconv.ParseInt(s, 10, 64); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return int(v), nil
|
||||
}
|
||||
},
|
||||
reflect.Int8: func(s string) (any, error) {
|
||||
if v, err := strconv.ParseInt(s, 10, 8); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return int8(v), nil
|
||||
}
|
||||
},
|
||||
reflect.Int16: func(s string) (any, error) {
|
||||
if v, err := strconv.ParseInt(s, 10, 16); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return int8(v), nil
|
||||
}
|
||||
},
|
||||
reflect.Int32: func(s string) (any, error) {
|
||||
if v, err := strconv.ParseInt(s, 10, 32); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return int8(v), nil
|
||||
}
|
||||
},
|
||||
reflect.Int64: func(s string) (any, error) {
|
||||
if v, err := strconv.ParseInt(s, 10, 64); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return int8(v), nil
|
||||
}
|
||||
},
|
||||
|
||||
reflect.Uint: func(s string) (any, error) {
|
||||
if v, err := strconv.ParseUint(s, 10, 64); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return uint(v), nil
|
||||
}
|
||||
},
|
||||
reflect.Uint8: func(s string) (any, error) {
|
||||
if v, err := strconv.ParseUint(s, 10, 8); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return uint8(v), nil
|
||||
}
|
||||
},
|
||||
reflect.Uint16: func(s string) (any, error) {
|
||||
if v, err := strconv.ParseUint(s, 10, 16); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return uint16(v), nil
|
||||
}
|
||||
},
|
||||
reflect.Uint32: func(s string) (any, error) {
|
||||
if v, err := strconv.ParseUint(s, 10, 32); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return uint32(v), nil
|
||||
}
|
||||
},
|
||||
reflect.Uint64: func(s string) (any, error) {
|
||||
if v, err := strconv.ParseUint(s, 10, 64); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
201
magic/query.go
Normal file
201
magic/query.go
Normal file
@ -0,0 +1,201 @@
|
||||
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
|
||||
}
|
15
magic/request.go
Normal file
15
magic/request.go
Normal file
@ -0,0 +1,15 @@
|
||||
package magic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterExtractorGeneric[*http.Request](func(r *http.Request) (any, error) {
|
||||
return r, nil
|
||||
})
|
||||
RegisterExtractorGeneric[context.Context](func(r *http.Request) (any, error) {
|
||||
return r.Context(), nil
|
||||
})
|
||||
}
|
41
magic/response.go
Normal file
41
magic/response.go
Normal file
@ -0,0 +1,41 @@
|
||||
package magic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type RespondWriter interface {
|
||||
WriteResponse(http.ResponseWriter)
|
||||
}
|
||||
|
||||
type RespondWriterFunc func(http.ResponseWriter)
|
||||
|
||||
func (fn RespondWriterFunc) WriteResponse(w http.ResponseWriter) {
|
||||
fn(w)
|
||||
}
|
||||
|
||||
type ErrorResponse interface {
|
||||
error
|
||||
WriteResponse(http.ResponseWriter)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterExtractorThatTakesResponseWriterGeneric[http.ResponseWriter](func(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
return w, nil
|
||||
})
|
||||
|
||||
upgrader := &websocket.Upgrader{
|
||||
Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(Map{
|
||||
"error": reason.Error(),
|
||||
})
|
||||
},
|
||||
}
|
||||
RegisterExtractorThatTakesResponseWriterGeneric[*websocket.Conn](func(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
return upgrader.Upgrade(w, r, nil)
|
||||
})
|
||||
}
|
13
magic/state.go
Normal file
13
magic/state.go
Normal file
@ -0,0 +1,13 @@
|
||||
package magic
|
||||
|
||||
import "net/http"
|
||||
|
||||
type State[T any] struct {
|
||||
Data T
|
||||
}
|
||||
|
||||
func RegisterState[T any](data T) {
|
||||
RegisterExtractor(State[T]{}, func(r *http.Request) (any, error) {
|
||||
return State[T]{Data: data}, nil
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user