fiber/binder/mapping.go

244 lines
5.8 KiB
Go

package binder
import (
"errors"
"reflect"
"strings"
"sync"
"github.com/gofiber/utils/v2"
"github.com/valyala/bytebufferpool"
"github.com/gofiber/fiber/v3/internal/schema"
)
// ParserConfig form decoder config for SetParserDecoder
type ParserConfig struct {
SetAliasTag string
ParserType []ParserType
IgnoreUnknownKeys bool
ZeroEmpty bool
}
// ParserType require two element, type and converter for register.
// Use ParserType with BodyParser for parsing custom type in form data.
type ParserType struct {
Customtype any
Converter func(string) reflect.Value
}
var (
// decoderPoolMap helps to improve binders
decoderPoolMap = map[string]*sync.Pool{}
// tags is used to classify parser's pool
tags = []string{HeaderBinder.Name(), RespHeaderBinder.Name(), CookieBinder.Name(), QueryBinder.Name(), FormBinder.Name(), URIBinder.Name()}
)
// SetParserDecoder allow globally change the option of form decoder, update decoderPool
func SetParserDecoder(parserConfig ParserConfig) {
for _, tag := range tags {
decoderPoolMap[tag] = &sync.Pool{New: func() any {
return decoderBuilder(parserConfig)
}}
}
}
func decoderBuilder(parserConfig ParserConfig) any {
decoder := schema.NewDecoder()
decoder.IgnoreUnknownKeys(parserConfig.IgnoreUnknownKeys)
if parserConfig.SetAliasTag != "" {
decoder.SetAliasTag(parserConfig.SetAliasTag)
}
for _, v := range parserConfig.ParserType {
decoder.RegisterConverter(reflect.ValueOf(v.Customtype).Interface(), v.Converter)
}
decoder.ZeroEmpty(parserConfig.ZeroEmpty)
return decoder
}
func init() {
for _, tag := range tags {
decoderPoolMap[tag] = &sync.Pool{New: func() any {
return decoderBuilder(ParserConfig{
IgnoreUnknownKeys: true,
ZeroEmpty: true,
})
}}
}
}
// parse data into the map or struct
func parse(aliasTag string, out any, data map[string][]string) error {
ptrVal := reflect.ValueOf(out)
// Get pointer value
if ptrVal.Kind() == reflect.Ptr {
ptrVal = ptrVal.Elem()
}
// Parse into the map
if ptrVal.Kind() == reflect.Map && ptrVal.Type().Key().Kind() == reflect.String {
return parseToMap(ptrVal.Interface(), data)
}
// Parse into the struct
return parseToStruct(aliasTag, out, data)
}
// Parse data into the struct with gorilla/schema
func parseToStruct(aliasTag string, out any, data map[string][]string) error {
// Get decoder from pool
schemaDecoder := decoderPoolMap[aliasTag].Get().(*schema.Decoder) //nolint:errcheck,forcetypeassert // not needed
defer decoderPoolMap[aliasTag].Put(schemaDecoder)
// Set alias tag
schemaDecoder.SetAliasTag(aliasTag)
return schemaDecoder.Decode(out, data)
}
// Parse data into the map
// thanks to https://github.com/gin-gonic/gin/blob/master/binding/binding.go
func parseToMap(ptr any, data map[string][]string) error {
elem := reflect.TypeOf(ptr).Elem()
// map[string][]string
if elem.Kind() == reflect.Slice {
newMap, ok := ptr.(map[string][]string)
if !ok {
return ErrMapNotConvertable
}
for k, v := range data {
newMap[k] = v
}
return nil
}
// map[string]string
newMap, ok := ptr.(map[string]string)
if !ok {
return ErrMapNotConvertable
}
for k, v := range data {
newMap[k] = v[len(v)-1]
}
return nil
}
func parseParamSquareBrackets(k string) (string, error) {
bb := bytebufferpool.Get()
defer bytebufferpool.Put(bb)
kbytes := []byte(k)
openBracketsCount := 0
for i, b := range kbytes {
if b == '[' {
openBracketsCount++
if i+1 < len(kbytes) && kbytes[i+1] != ']' {
if err := bb.WriteByte('.'); err != nil {
return "", err //nolint:wrapcheck // unnecessary to wrap it
}
}
continue
}
if b == ']' {
openBracketsCount--
if openBracketsCount < 0 {
return "", errors.New("unmatched brackets")
}
continue
}
if err := bb.WriteByte(b); err != nil {
return "", err //nolint:wrapcheck // unnecessary to wrap it
}
}
if openBracketsCount > 0 {
return "", errors.New("unmatched brackets")
}
return bb.String(), nil
}
func equalFieldType(out any, kind reflect.Kind, key string) bool {
// Get type of interface
outTyp := reflect.TypeOf(out).Elem()
key = utils.ToLower(key)
// Support maps
if outTyp.Kind() == reflect.Map && outTyp.Key().Kind() == reflect.String {
return true
}
// Must be a struct to match a field
if outTyp.Kind() != reflect.Struct {
return false
}
// Copy interface to an value to be used
outVal := reflect.ValueOf(out).Elem()
// Loop over each field
for i := 0; i < outTyp.NumField(); i++ {
// Get field value data
structField := outVal.Field(i)
// Can this field be changed?
if !structField.CanSet() {
continue
}
// Get field key data
typeField := outTyp.Field(i)
// Get type of field key
structFieldKind := structField.Kind()
// Does the field type equals input?
if structFieldKind != kind {
// Is the field an embedded struct?
if structFieldKind == reflect.Struct {
// Loop over embedded struct fields
for j := 0; j < structField.NumField(); j++ {
structFieldField := structField.Field(j)
// Can this embedded field be changed?
if !structFieldField.CanSet() {
continue
}
// Is the embedded struct field type equal to the input?
if structFieldField.Kind() == kind {
return true
}
}
}
continue
}
// Get tag from field if exist
inputFieldName := typeField.Tag.Get(QueryBinder.Name())
if inputFieldName == "" {
inputFieldName = typeField.Name
} else {
inputFieldName = strings.Split(inputFieldName, ",")[0]
}
// Compare field/tag with provided key
if utils.ToLower(inputFieldName) == key {
return true
}
}
return false
}
// Get content type from content type header
func FilterFlags(content string) string {
for i, char := range content {
if char == ' ' || char == ';' {
return content[:i]
}
}
return content
}