feat: stealing the gorilla/schema for us, lol.

This commit is contained in:
Andrey Parhomenko 2024-01-10 22:42:50 +03:00
parent e86beb23d0
commit 67132f5824
13 changed files with 4071 additions and 49 deletions

View file

@ -2,8 +2,8 @@ package main
import (
"vultras.su/core/bond"
"vultras.su/core/bond/methods"
"vultras.su/core/bond/contents"
//"vultras.su/core/bond/methods"
"vultras.su/core/bond/statuses"
"fmt"
"io"
"net/url"
@ -16,66 +16,30 @@ type GetNotesOptions struct {
var root = bond.Root(bond.Path().
Def(
"", bond.Func(func(c *bond.Context) {
c.W.Write([]byte("This is the index page"))
}),
).Def(
"hello",
bond.Path().Def(
"en", bond.Func(func(c *bond.Context) {
c.Printf("Hello, World!")
}),
).Def(
"ru",
bond.Func(func(c *bond.Context) {
c.Printf("Привет, Мир!")
}),
),
).Def(
"web", bond.Static("./static"),
).Def(
"test", bond.Func(func(c *bond.Context) {
c.SetContentType(contents.Plain)
c.Printf(
"Path: %q\n"+
"Content-Type: %q\n",
c.Path(), c.ContentType(),
)
c.Printf("Query:\n")
for k, vs := range c.Query() {
c.Printf("\t%q:\n", k)
for _, v := range vs {
c.Printf("\t\t%q\n", v)
}
}
}),
).Def(
"get-notes",
bond.Method().Def(
methods.Get,
bond.Func(func(c *bond.Context) {
opts := GetNotesOptions{}
c.Scan(&opts)
c.Printf("%v", opts)
}),
),
).Def(
"hook",
/*bond.Method().Def(
methods.Post,*/
bond.Func(func(c *bond.Context){
fmt.Printf("content-type: %q", c.ContentType())
fmt.Printf("Content-Type: %q\n", c.ContentType())
body, err := io.ReadAll(c.R.Body)
if err != nil {
fmt.Printf("err:%s\n", err)
return
}
fmt.Printf("rawBody: %q\n", body)
unesc, err := url.QueryUnescape(string(body))
if err != nil {
fmt.Printf("err:%s\n", err)
return
}
fmt.Printf("unescapeBody: %q\n", unesc)
values, err := url.ParseQuery(string(body))
if err != nil {
fmt.Printf("err:%s\n", err)
return
}
fmt.Printf("values: %q", values)
fmt.Printf("Values: %q\n", values)
c.SetStatus(statuses.OK)
}),
//),
))

View file

@ -26,6 +26,7 @@ func (c *Context) Method() ReqMethod {
// Set the reply status code.
func (c *Context) SetStatus(status Status) {
c.W.WriteHeader(int(status))
}
// Set the reply content type.

20
schema/.editorconfig Normal file
View file

@ -0,0 +1,20 @@
; https://editorconfig.org/
root = true
[*]
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
indent_style = tab
indent_size = 4
[*.md]
indent_size = 4
trim_trailing_whitespace = false
eclint_indent_style = unset

305
schema/cache.go Normal file
View file

@ -0,0 +1,305 @@
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package schema
import (
"errors"
"reflect"
"strconv"
"strings"
"sync"
)
var errInvalidPath = errors.New("schema: invalid path")
// newCache returns a new cache.
func newCache() *cache {
c := cache{
m: make(map[reflect.Type]*structInfo),
regconv: make(map[reflect.Type]Converter),
tag: "schema",
}
return &c
}
// cache caches meta-data about a struct.
type cache struct {
l sync.RWMutex
m map[reflect.Type]*structInfo
regconv map[reflect.Type]Converter
tag string
}
// registerConverter registers a converter function for a custom type.
func (c *cache) registerConverter(value interface{}, converterFunc Converter) {
c.regconv[reflect.TypeOf(value)] = converterFunc
}
// parsePath parses a path in dotted notation verifying that it is a valid
// path to a struct field.
//
// It returns "path parts" which contain indices to fields to be used by
// reflect.Value.FieldByString(). Multiple parts are required for slices of
// structs.
func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) {
var struc *structInfo
var field *fieldInfo
var index64 int64
var err error
parts := make([]pathPart, 0)
path := make([]string, 0)
keys := strings.Split(p, ".")
for i := 0; i < len(keys); i++ {
if t.Kind() != reflect.Struct {
return nil, errInvalidPath
}
if struc = c.get(t); struc == nil {
return nil, errInvalidPath
}
if field = struc.get(keys[i]); field == nil {
return nil, errInvalidPath
}
// Valid field. Append index.
path = append(path, field.name)
if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) {
// Parse a special case: slices of structs.
// i+1 must be the slice index.
//
// Now that struct can implements TextUnmarshaler interface,
// we don't need to force the struct's fields to appear in the path.
// So checking i+2 is not necessary anymore.
i++
if i+1 > len(keys) {
return nil, errInvalidPath
}
if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil {
return nil, errInvalidPath
}
parts = append(parts, pathPart{
path: path,
field: field,
index: int(index64),
})
path = make([]string, 0)
// Get the next struct type, dropping ptrs.
if field.typ.Kind() == reflect.Ptr {
t = field.typ.Elem()
} else {
t = field.typ
}
if t.Kind() == reflect.Slice {
t = t.Elem()
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
}
} else if field.typ.Kind() == reflect.Ptr {
t = field.typ.Elem()
} else {
t = field.typ
}
}
// Add the remaining.
parts = append(parts, pathPart{
path: path,
field: field,
index: -1,
})
return parts, nil
}
// get returns a cached structInfo, creating it if necessary.
func (c *cache) get(t reflect.Type) *structInfo {
c.l.RLock()
info := c.m[t]
c.l.RUnlock()
if info == nil {
info = c.create(t, "")
c.l.Lock()
c.m[t] = info
c.l.Unlock()
}
return info
}
// create creates a structInfo with meta-data about a struct.
func (c *cache) create(t reflect.Type, parentAlias string) *structInfo {
info := &structInfo{}
var anonymousInfos []*structInfo
for i := 0; i < t.NumField(); i++ {
if f := c.createField(t.Field(i), parentAlias); f != nil {
info.fields = append(info.fields, f)
if ft := indirectType(f.typ); ft.Kind() == reflect.Struct && f.isAnonymous {
anonymousInfos = append(anonymousInfos, c.create(ft, f.canonicalAlias))
}
}
}
for i, a := range anonymousInfos {
others := []*structInfo{info}
others = append(others, anonymousInfos[:i]...)
others = append(others, anonymousInfos[i+1:]...)
for _, f := range a.fields {
if !containsAlias(others, f.alias) {
info.fields = append(info.fields, f)
}
}
}
return info
}
// createField creates a fieldInfo for the given field.
func (c *cache) createField(field reflect.StructField, parentAlias string) *fieldInfo {
alias, options := fieldAlias(field, c.tag)
if alias == "-" {
// Ignore this field.
return nil
}
canonicalAlias := alias
if parentAlias != "" {
canonicalAlias = parentAlias + "." + alias
}
// Check if the type is supported and don't cache it if not.
// First let's get the basic type.
isSlice, isStruct := false, false
ft := field.Type
m := isTextUnmarshaler(reflect.Zero(ft))
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
if isSlice = ft.Kind() == reflect.Slice; isSlice {
ft = ft.Elem()
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
}
if ft.Kind() == reflect.Array {
ft = ft.Elem()
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
}
if isStruct = ft.Kind() == reflect.Struct; !isStruct {
if c.converter(ft) == nil && builtinConverters[ft.Kind()] == nil {
// Type is not supported.
return nil
}
}
return &fieldInfo{
typ: field.Type,
name: field.Name,
alias: alias,
canonicalAlias: canonicalAlias,
unmarshalerInfo: m,
isSliceOfStructs: isSlice && isStruct,
isAnonymous: field.Anonymous,
isRequired: options.Contains("required"),
}
}
// converter returns the converter for a type.
func (c *cache) converter(t reflect.Type) Converter {
return c.regconv[t]
}
// ----------------------------------------------------------------------------
type structInfo struct {
fields []*fieldInfo
}
func (i *structInfo) get(alias string) *fieldInfo {
for _, field := range i.fields {
if strings.EqualFold(field.alias, alias) {
return field
}
}
return nil
}
func containsAlias(infos []*structInfo, alias string) bool {
for _, info := range infos {
if info.get(alias) != nil {
return true
}
}
return false
}
type fieldInfo struct {
typ reflect.Type
// name is the field name in the struct.
name string
alias string
// canonicalAlias is almost the same as the alias, but is prefixed with
// an embedded struct field alias in dotted notation if this field is
// promoted from the struct.
// For instance, if the alias is "N" and this field is an embedded field
// in a struct "X", canonicalAlias will be "X.N".
canonicalAlias string
// unmarshalerInfo contains information regarding the
// encoding.TextUnmarshaler implementation of the field type.
unmarshalerInfo unmarshaler
// isSliceOfStructs indicates if the field type is a slice of structs.
isSliceOfStructs bool
// isAnonymous indicates whether the field is embedded in the struct.
isAnonymous bool
isRequired bool
}
func (f *fieldInfo) paths(prefix string) []string {
if f.alias == f.canonicalAlias {
return []string{prefix + f.alias}
}
return []string{prefix + f.alias, prefix + f.canonicalAlias}
}
type pathPart struct {
field *fieldInfo
path []string // path to the field: walks structs using field names.
index int // struct index in slices of structs.
}
// ----------------------------------------------------------------------------
func indirectType(typ reflect.Type) reflect.Type {
if typ.Kind() == reflect.Ptr {
return typ.Elem()
}
return typ
}
// fieldAlias parses a field tag to get a field alias.
func fieldAlias(field reflect.StructField, tagName string) (alias string, options tagOptions) {
if tag := field.Tag.Get(tagName); tag != "" {
alias, options = parseTag(tag)
}
if alias == "" {
alias = field.Name
}
return alias, options
}
// tagOptions is the string following a comma in a struct field's tag, or
// the empty string. It does not include the leading comma.
type tagOptions []string
// parseTag splits a struct field's url tag into its name and comma-separated
// options.
func parseTag(tag string) (string, tagOptions) {
s := strings.Split(tag, ",")
return s[0], s[1:]
}
// Contains checks whether the tagOptions contains the specified option.
func (o tagOptions) Contains(option string) bool {
for _, s := range o {
if s == option {
return true
}
}
return false
}

145
schema/converter.go Normal file
View file

@ -0,0 +1,145 @@
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package schema
import (
"reflect"
"strconv"
)
type Converter func(string) reflect.Value
var (
invalidValue = reflect.Value{}
boolType = reflect.Bool
float32Type = reflect.Float32
float64Type = reflect.Float64
intType = reflect.Int
int8Type = reflect.Int8
int16Type = reflect.Int16
int32Type = reflect.Int32
int64Type = reflect.Int64
stringType = reflect.String
uintType = reflect.Uint
uint8Type = reflect.Uint8
uint16Type = reflect.Uint16
uint32Type = reflect.Uint32
uint64Type = reflect.Uint64
)
// Default converters for basic types.
var builtinConverters = map[reflect.Kind]Converter{
boolType: convertBool,
float32Type: convertFloat32,
float64Type: convertFloat64,
intType: convertInt,
int8Type: convertInt8,
int16Type: convertInt16,
int32Type: convertInt32,
int64Type: convertInt64,
stringType: convertString,
uintType: convertUint,
uint8Type: convertUint8,
uint16Type: convertUint16,
uint32Type: convertUint32,
uint64Type: convertUint64,
}
func convertBool(value string) reflect.Value {
if value == "on" {
return reflect.ValueOf(true)
} else if v, err := strconv.ParseBool(value); err == nil {
return reflect.ValueOf(v)
}
return invalidValue
}
func convertFloat32(value string) reflect.Value {
if v, err := strconv.ParseFloat(value, 32); err == nil {
return reflect.ValueOf(float32(v))
}
return invalidValue
}
func convertFloat64(value string) reflect.Value {
if v, err := strconv.ParseFloat(value, 64); err == nil {
return reflect.ValueOf(v)
}
return invalidValue
}
func convertInt(value string) reflect.Value {
if v, err := strconv.ParseInt(value, 10, 0); err == nil {
return reflect.ValueOf(int(v))
}
return invalidValue
}
func convertInt8(value string) reflect.Value {
if v, err := strconv.ParseInt(value, 10, 8); err == nil {
return reflect.ValueOf(int8(v))
}
return invalidValue
}
func convertInt16(value string) reflect.Value {
if v, err := strconv.ParseInt(value, 10, 16); err == nil {
return reflect.ValueOf(int16(v))
}
return invalidValue
}
func convertInt32(value string) reflect.Value {
if v, err := strconv.ParseInt(value, 10, 32); err == nil {
return reflect.ValueOf(int32(v))
}
return invalidValue
}
func convertInt64(value string) reflect.Value {
if v, err := strconv.ParseInt(value, 10, 64); err == nil {
return reflect.ValueOf(v)
}
return invalidValue
}
func convertString(value string) reflect.Value {
return reflect.ValueOf(value)
}
func convertUint(value string) reflect.Value {
if v, err := strconv.ParseUint(value, 10, 0); err == nil {
return reflect.ValueOf(uint(v))
}
return invalidValue
}
func convertUint8(value string) reflect.Value {
if v, err := strconv.ParseUint(value, 10, 8); err == nil {
return reflect.ValueOf(uint8(v))
}
return invalidValue
}
func convertUint16(value string) reflect.Value {
if v, err := strconv.ParseUint(value, 10, 16); err == nil {
return reflect.ValueOf(uint16(v))
}
return invalidValue
}
func convertUint32(value string) reflect.Value {
if v, err := strconv.ParseUint(value, 10, 32); err == nil {
return reflect.ValueOf(uint32(v))
}
return invalidValue
}
func convertUint64(value string) reflect.Value {
if v, err := strconv.ParseUint(value, 10, 64); err == nil {
return reflect.ValueOf(v)
}
return invalidValue
}

521
schema/decoder.go Normal file
View file

@ -0,0 +1,521 @@
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package schema
import (
"encoding"
"errors"
"fmt"
"reflect"
"strings"
)
// NewDecoder returns a new Decoder.
func NewDecoder() *Decoder {
return &Decoder{cache: newCache()}
}
// Decoder decodes values from a map[string][]string to a struct.
type Decoder struct {
cache *cache
zeroEmpty bool
ignoreUnknownKeys bool
}
// SetAliasTag changes the tag used to locate custom field aliases.
// The default tag is "schema".
func (d *Decoder) SetAliasTag(tag string) {
d.cache.tag = tag
}
// ZeroEmpty controls the behaviour when the decoder encounters empty values
// in a map.
// If z is true and a key in the map has the empty string as a value
// then the corresponding struct field is set to the zero value.
// If z is false then empty strings are ignored.
//
// The default value is false, that is empty values do not change
// the value of the struct field.
func (d *Decoder) ZeroEmpty(z bool) {
d.zeroEmpty = z
}
// IgnoreUnknownKeys controls the behaviour when the decoder encounters unknown
// keys in the map.
// If i is true and an unknown field is encountered, it is ignored. This is
// similar to how unknown keys are handled by encoding/json.
// If i is false then Decode will return an error. Note that any valid keys
// will still be decoded in to the target struct.
//
// To preserve backwards compatibility, the default value is false.
func (d *Decoder) IgnoreUnknownKeys(i bool) {
d.ignoreUnknownKeys = i
}
// RegisterConverter registers a converter function for a custom type.
func (d *Decoder) RegisterConverter(value interface{}, converterFunc Converter) {
d.cache.registerConverter(value, converterFunc)
}
// Decode decodes a map[string][]string to a struct.
//
// The first parameter must be a pointer to a struct.
//
// The second parameter is a map, typically url.Values from an HTTP request.
// Keys are "paths" in dotted notation to the struct fields and nested structs.
//
// See the package documentation for a full explanation of the mechanics.
func (d *Decoder) Decode(dst interface{}, src map[string][]string) error {
v := reflect.ValueOf(dst)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
return errors.New("schema: interface must be a pointer to struct")
}
v = v.Elem()
t := v.Type()
errors := MultiError{}
for path, values := range src {
if parts, err := d.cache.parsePath(path, t); err == nil {
if err = d.decode(v, path, parts, values); err != nil {
errors[path] = err
}
} else if !d.ignoreUnknownKeys {
errors[path] = UnknownKeyError{Key: path}
}
}
errors.merge(d.checkRequired(t, src))
if len(errors) > 0 {
return errors
}
return nil
}
// checkRequired checks whether required fields are empty
//
// check type t recursively if t has struct fields.
//
// src is the source map for decoding, we use it here to see if those required fields are included in src
func (d *Decoder) checkRequired(t reflect.Type, src map[string][]string) MultiError {
m, errs := d.findRequiredFields(t, "", "")
for key, fields := range m {
if isEmptyFields(fields, src) {
errs[key] = EmptyFieldError{Key: key}
}
}
return errs
}
// findRequiredFields recursively searches the struct type t for required fields.
//
// canonicalPrefix and searchPrefix are used to resolve full paths in dotted notation
// for nested struct fields. canonicalPrefix is a complete path which never omits
// any embedded struct fields. searchPrefix is a user-friendly path which may omit
// some embedded struct fields to point promoted fields.
func (d *Decoder) findRequiredFields(t reflect.Type, canonicalPrefix, searchPrefix string) (map[string][]fieldWithPrefix, MultiError) {
struc := d.cache.get(t)
if struc == nil {
// unexpect, cache.get never return nil
return nil, MultiError{canonicalPrefix + "*": errors.New("cache fail")}
}
m := map[string][]fieldWithPrefix{}
errs := MultiError{}
for _, f := range struc.fields {
if f.typ.Kind() == reflect.Struct {
fcprefix := canonicalPrefix + f.canonicalAlias + "."
for _, fspath := range f.paths(searchPrefix) {
fm, ferrs := d.findRequiredFields(f.typ, fcprefix, fspath+".")
for key, fields := range fm {
m[key] = append(m[key], fields...)
}
errs.merge(ferrs)
}
}
if f.isRequired {
key := canonicalPrefix + f.canonicalAlias
m[key] = append(m[key], fieldWithPrefix{
fieldInfo: f,
prefix: searchPrefix,
})
}
}
return m, errs
}
type fieldWithPrefix struct {
*fieldInfo
prefix string
}
// isEmptyFields returns true if all of specified fields are empty.
func isEmptyFields(fields []fieldWithPrefix, src map[string][]string) bool {
for _, f := range fields {
for _, path := range f.paths(f.prefix) {
v, ok := src[path]
if ok && !isEmpty(f.typ, v) {
return false
}
for key := range src {
if !isEmpty(f.typ, src[key]) && strings.HasPrefix(key, path) {
return false
}
}
}
}
return true
}
// isEmpty returns true if value is empty for specific type
func isEmpty(t reflect.Type, value []string) bool {
if len(value) == 0 {
return true
}
switch t.Kind() {
case boolType, float32Type, float64Type, intType, int8Type, int32Type, int64Type, stringType, uint8Type, uint16Type, uint32Type, uint64Type:
return len(value[0]) == 0
}
return false
}
// decode fills a struct field using a parsed path.
func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values []string) error {
// Get the field walking the struct fields by index.
for _, name := range parts[0].path {
if v.Type().Kind() == reflect.Ptr {
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
v = v.Elem()
}
// alloc embedded structs
if v.Type().Kind() == reflect.Struct {
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous {
field.Set(reflect.New(field.Type().Elem()))
}
}
}
v = v.FieldByName(name)
}
// Don't even bother for unexported fields.
if !v.CanSet() {
return nil
}
// Dereference if needed.
t := v.Type()
if t.Kind() == reflect.Ptr {
t = t.Elem()
if v.IsNil() {
v.Set(reflect.New(t))
}
v = v.Elem()
}
// Slice of structs. Let's go recursive.
if len(parts) > 1 {
idx := parts[0].index
if v.IsNil() || v.Len() < idx+1 {
value := reflect.MakeSlice(t, idx+1, idx+1)
if v.Len() < idx+1 {
// Resize it.
reflect.Copy(value, v)
}
v.Set(value)
}
return d.decode(v.Index(idx), path, parts[1:], values)
}
// Get the converter early in case there is one for a slice type.
conv := d.cache.converter(t)
m := isTextUnmarshaler(v)
if conv == nil && t.Kind() == reflect.Slice && m.IsSliceElement {
var items []reflect.Value
elemT := t.Elem()
isPtrElem := elemT.Kind() == reflect.Ptr
if isPtrElem {
elemT = elemT.Elem()
}
// Try to get a converter for the element type.
conv := d.cache.converter(elemT)
if conv == nil {
conv = builtinConverters[elemT.Kind()]
if conv == nil {
// As we are not dealing with slice of structs here, we don't need to check if the type
// implements TextUnmarshaler interface
return fmt.Errorf("schema: converter not found for %v", elemT)
}
}
for key, value := range values {
if value == "" {
if d.zeroEmpty {
items = append(items, reflect.Zero(elemT))
}
} else if m.IsValid {
u := reflect.New(elemT)
if m.IsSliceElementPtr {
u = reflect.New(reflect.PtrTo(elemT).Elem())
}
if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)); err != nil {
return ConversionError{
Key: path,
Type: t,
Index: key,
Err: err,
}
}
if m.IsSliceElementPtr {
items = append(items, u.Elem().Addr())
} else if u.Kind() == reflect.Ptr {
items = append(items, u.Elem())
} else {
items = append(items, u)
}
} else if item := conv(value); item.IsValid() {
if isPtrElem {
ptr := reflect.New(elemT)
ptr.Elem().Set(item)
item = ptr
}
if item.Type() != elemT && !isPtrElem {
item = item.Convert(elemT)
}
items = append(items, item)
} else {
if strings.Contains(value, ",") {
values := strings.Split(value, ",")
for _, value := range values {
if value == "" {
if d.zeroEmpty {
items = append(items, reflect.Zero(elemT))
}
} else if item := conv(value); item.IsValid() {
if isPtrElem {
ptr := reflect.New(elemT)
ptr.Elem().Set(item)
item = ptr
}
if item.Type() != elemT && !isPtrElem {
item = item.Convert(elemT)
}
items = append(items, item)
} else {
return ConversionError{
Key: path,
Type: elemT,
Index: key,
}
}
}
} else {
return ConversionError{
Key: path,
Type: elemT,
Index: key,
}
}
}
}
value := reflect.Append(reflect.MakeSlice(t, 0, 0), items...)
v.Set(value)
} else {
val := ""
// Use the last value provided if any values were provided
if len(values) > 0 {
val = values[len(values)-1]
}
if conv != nil {
if value := conv(val); value.IsValid() {
v.Set(value.Convert(t))
} else {
return ConversionError{
Key: path,
Type: t,
Index: -1,
}
}
} else if m.IsValid {
if m.IsPtr {
u := reflect.New(v.Type())
if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(val)); err != nil {
return ConversionError{
Key: path,
Type: t,
Index: -1,
Err: err,
}
}
v.Set(reflect.Indirect(u))
} else {
// If the value implements the encoding.TextUnmarshaler interface
// apply UnmarshalText as the converter
if err := m.Unmarshaler.UnmarshalText([]byte(val)); err != nil {
return ConversionError{
Key: path,
Type: t,
Index: -1,
Err: err,
}
}
}
} else if val == "" {
if d.zeroEmpty {
v.Set(reflect.Zero(t))
}
} else if conv := builtinConverters[t.Kind()]; conv != nil {
if value := conv(val); value.IsValid() {
v.Set(value.Convert(t))
} else {
return ConversionError{
Key: path,
Type: t,
Index: -1,
}
}
} else {
return fmt.Errorf("schema: converter not found for %v", t)
}
}
return nil
}
func isTextUnmarshaler(v reflect.Value) unmarshaler {
// Create a new unmarshaller instance
m := unmarshaler{}
if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid {
return m
}
// As the UnmarshalText function should be applied to the pointer of the
// type, we check that type to see if it implements the necessary
// method.
if m.Unmarshaler, m.IsValid = reflect.New(v.Type()).Interface().(encoding.TextUnmarshaler); m.IsValid {
m.IsPtr = true
return m
}
// if v is []T or *[]T create new T
t := v.Type()
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() == reflect.Slice {
// Check if the slice implements encoding.TextUnmarshaller
if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid {
return m
}
// If t is a pointer slice, check if its elements implement
// encoding.TextUnmarshaler
m.IsSliceElement = true
if t = t.Elem(); t.Kind() == reflect.Ptr {
t = reflect.PtrTo(t.Elem())
v = reflect.Zero(t)
m.IsSliceElementPtr = true
m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler)
return m
}
}
v = reflect.New(t)
m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler)
return m
}
// TextUnmarshaler helpers ----------------------------------------------------
// unmarshaller contains information about a TextUnmarshaler type
type unmarshaler struct {
Unmarshaler encoding.TextUnmarshaler
// IsValid indicates whether the resolved type indicated by the other
// flags implements the encoding.TextUnmarshaler interface.
IsValid bool
// IsPtr indicates that the resolved type is the pointer of the original
// type.
IsPtr bool
// IsSliceElement indicates that the resolved type is a slice element of
// the original type.
IsSliceElement bool
// IsSliceElementPtr indicates that the resolved type is a pointer to a
// slice element of the original type.
IsSliceElementPtr bool
}
// Errors ---------------------------------------------------------------------
// ConversionError stores information about a failed conversion.
type ConversionError struct {
Key string // key from the source map.
Type reflect.Type // expected type of elem
Index int // index for multi-value fields; -1 for single-value fields.
Err error // low-level error (when it exists)
}
func (e ConversionError) Error() string {
var output string
if e.Index < 0 {
output = fmt.Sprintf("schema: error converting value for %q", e.Key)
} else {
output = fmt.Sprintf("schema: error converting value for index %d of %q",
e.Index, e.Key)
}
if e.Err != nil {
output = fmt.Sprintf("%s. Details: %s", output, e.Err)
}
return output
}
// UnknownKeyError stores information about an unknown key in the source map.
type UnknownKeyError struct {
Key string // key from the source map.
}
func (e UnknownKeyError) Error() string {
return fmt.Sprintf("schema: invalid path %q", e.Key)
}
// EmptyFieldError stores information about an empty required field.
type EmptyFieldError struct {
Key string // required key in the source map.
}
func (e EmptyFieldError) Error() string {
return fmt.Sprintf("%v is empty", e.Key)
}
// MultiError stores multiple decoding errors.
//
// Borrowed from the App Engine SDK.
type MultiError map[string]error
func (e MultiError) Error() string {
s := ""
for _, err := range e {
s = err.Error()
break
}
switch len(e) {
case 0:
return "(0 errors)"
case 1:
return s
case 2:
return s + " (and 1 other error)"
}
return fmt.Sprintf("%s (and %d other errors)", s, len(e)-1)
}
func (e MultiError) merge(errors MultiError) {
for key, err := range errors {
if e[key] == nil {
e[key] = err
}
}
}

2057
schema/decoder_test.go Normal file

File diff suppressed because it is too large Load diff

148
schema/doc.go Normal file
View file

@ -0,0 +1,148 @@
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package gorilla/schema fills a struct with form values.
The basic usage is really simple. Given this struct:
type Person struct {
Name string
Phone string
}
...we can fill it passing a map to the Decode() function:
values := map[string][]string{
"Name": {"John"},
"Phone": {"999-999-999"},
}
person := new(Person)
decoder := schema.NewDecoder()
decoder.Decode(person, values)
This is just a simple example and it doesn't make a lot of sense to create
the map manually. Typically it will come from a http.Request object and
will be of type url.Values, http.Request.Form, or http.Request.MultipartForm:
func MyHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
// Handle error
}
decoder := schema.NewDecoder()
// r.PostForm is a map of our POST form values
err := decoder.Decode(person, r.PostForm)
if err != nil {
// Handle error
}
// Do something with person.Name or person.Phone
}
Note: it is a good idea to set a Decoder instance as a package global,
because it caches meta-data about structs, and an instance can be shared safely:
var decoder = schema.NewDecoder()
To define custom names for fields, use a struct tag "schema". To not populate
certain fields, use a dash for the name and it will be ignored:
type Person struct {
Name string `schema:"name"` // custom name
Phone string `schema:"phone"` // custom name
Admin bool `schema:"-"` // this field is never set
}
The supported field types in the destination struct are:
* bool
* float variants (float32, float64)
* int variants (int, int8, int16, int32, int64)
* string
* uint variants (uint, uint8, uint16, uint32, uint64)
* struct
* a pointer to one of the above types
* a slice or a pointer to a slice of one of the above types
Non-supported types are simply ignored, however custom types can be registered
to be converted.
To fill nested structs, keys must use a dotted notation as the "path" for the
field. So for example, to fill the struct Person below:
type Phone struct {
Label string
Number string
}
type Person struct {
Name string
Phone Phone
}
...the source map must have the keys "Name", "Phone.Label" and "Phone.Number".
This means that an HTML form to fill a Person struct must look like this:
<form>
<input type="text" name="Name">
<input type="text" name="Phone.Label">
<input type="text" name="Phone.Number">
</form>
Single values are filled using the first value for a key from the source map.
Slices are filled using all values for a key from the source map. So to fill
a Person with multiple Phone values, like:
type Person struct {
Name string
Phones []Phone
}
...an HTML form that accepts three Phone values would look like this:
<form>
<input type="text" name="Name">
<input type="text" name="Phones.0.Label">
<input type="text" name="Phones.0.Number">
<input type="text" name="Phones.1.Label">
<input type="text" name="Phones.1.Number">
<input type="text" name="Phones.2.Label">
<input type="text" name="Phones.2.Number">
</form>
Notice that only for slices of structs the slice index is required.
This is needed for disambiguation: if the nested struct also had a slice
field, we could not translate multiple values to it if we did not use an
index for the parent struct.
There's also the possibility to create a custom type that implements the
TextUnmarshaler interface, and in this case there's no need to register
a converter, like:
type Person struct {
Emails []Email
}
type Email struct {
*mail.Address
}
func (e *Email) UnmarshalText(text []byte) (err error) {
e.Address, err = mail.ParseAddress(string(text))
return
}
...an HTML form that accepts three Email values would look like this:
<form>
<input type="email" name="Emails.0">
<input type="email" name="Emails.1">
<input type="email" name="Emails.2">
</form>
*/
package schema

214
schema/encoder.go Normal file
View file

@ -0,0 +1,214 @@
package schema
import (
"errors"
"fmt"
"log"
"reflect"
"strconv"
)
type encoderFunc func(reflect.Value) string
// Encoder encodes values from a struct into url.Values.
type Encoder struct {
cache *cache
regenc map[reflect.Type]encoderFunc
}
// NewEncoder returns a new Encoder with defaults.
func NewEncoder() *Encoder {
return &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)}
}
// Encode encodes a struct into map[string][]string.
//
// Intended for use with url.Values.
func (e *Encoder) Encode(src interface{}, dst map[string][]string) error {
v := reflect.ValueOf(src)
return e.encode(v, dst)
}
// RegisterEncoder registers a converter for encoding a custom type.
func (e *Encoder) RegisterEncoder(value interface{}, encoder func(reflect.Value) string) {
e.regenc[reflect.TypeOf(value)] = encoder
}
// SetAliasTag changes the tag used to locate custom field aliases.
// The default tag is "schema".
func (e *Encoder) SetAliasTag(tag string) {
e.cache.tag = tag
}
// isValidStructPointer test if input value is a valid struct pointer.
func isValidStructPointer(v reflect.Value) bool {
return v.Type().Kind() == reflect.Ptr && v.Elem().IsValid() && v.Elem().Type().Kind() == reflect.Struct
}
func isZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Func:
case reflect.Map, reflect.Slice:
return v.IsNil() || v.Len() == 0
case reflect.Array:
z := true
for i := 0; i < v.Len(); i++ {
z = z && isZero(v.Index(i))
}
return z
case reflect.Struct:
type zero interface {
IsZero() bool
}
if v.Type().Implements(reflect.TypeOf((*zero)(nil)).Elem()) {
iz := v.MethodByName("IsZero").Call([]reflect.Value{})[0]
return iz.Interface().(bool)
}
z := true
for i := 0; i < v.NumField(); i++ {
z = z && isZero(v.Field(i))
}
return z
}
// Compare other types directly:
z := reflect.Zero(v.Type())
return v.Interface() == z.Interface()
}
func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error {
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return errors.New("schema: interface must be a struct")
}
t := v.Type()
errors := MultiError{}
for i := 0; i < v.NumField(); i++ {
name, opts := fieldAlias(t.Field(i), e.cache.tag)
if name == "-" {
continue
}
// Encode struct pointer types if the field is a valid pointer and a struct.
if isValidStructPointer(v.Field(i)) && !e.hasCustomEncoder(v.Field(i).Type()) {
err := e.encode(v.Field(i).Elem(), dst)
if err != nil {
log.Fatal(err)
}
continue
}
encFunc := typeEncoder(v.Field(i).Type(), e.regenc)
// Encode non-slice types and custom implementations immediately.
if encFunc != nil {
value := encFunc(v.Field(i))
if opts.Contains("omitempty") && isZero(v.Field(i)) {
continue
}
dst[name] = append(dst[name], value)
continue
}
if v.Field(i).Type().Kind() == reflect.Struct {
err := e.encode(v.Field(i), dst)
if err != nil {
log.Fatal(err)
}
continue
}
if v.Field(i).Type().Kind() == reflect.Slice {
encFunc = typeEncoder(v.Field(i).Type().Elem(), e.regenc)
}
if encFunc == nil {
errors[v.Field(i).Type().String()] = fmt.Errorf("schema: encoder not found for %v", v.Field(i))
continue
}
// Encode a slice.
if v.Field(i).Len() == 0 && opts.Contains("omitempty") {
continue
}
dst[name] = []string{}
for j := 0; j < v.Field(i).Len(); j++ {
dst[name] = append(dst[name], encFunc(v.Field(i).Index(j)))
}
}
if len(errors) > 0 {
return errors
}
return nil
}
func (e *Encoder) hasCustomEncoder(t reflect.Type) bool {
_, exists := e.regenc[t]
return exists
}
func typeEncoder(t reflect.Type, reg map[reflect.Type]encoderFunc) encoderFunc {
if f, ok := reg[t]; ok {
return f
}
switch t.Kind() {
case reflect.Bool:
return encodeBool
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return encodeInt
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return encodeUint
case reflect.Float32:
return encodeFloat32
case reflect.Float64:
return encodeFloat64
case reflect.Ptr:
f := typeEncoder(t.Elem(), reg)
return func(v reflect.Value) string {
if v.IsNil() {
return "null"
}
return f(v.Elem())
}
case reflect.String:
return encodeString
default:
return nil
}
}
func encodeBool(v reflect.Value) string {
return strconv.FormatBool(v.Bool())
}
func encodeInt(v reflect.Value) string {
return strconv.FormatInt(int64(v.Int()), 10)
}
func encodeUint(v reflect.Value) string {
return strconv.FormatUint(uint64(v.Uint()), 10)
}
func encodeFloat(v reflect.Value, bits int) string {
return strconv.FormatFloat(v.Float(), 'f', 6, bits)
}
func encodeFloat32(v reflect.Value) string {
return encodeFloat(v, 32)
}
func encodeFloat64(v reflect.Value) string {
return encodeFloat(v, 64)
}
func encodeString(v reflect.Value) string {
return v.String()
}

525
schema/encoder_test.go Normal file
View file

@ -0,0 +1,525 @@
package schema
import (
"fmt"
"reflect"
"testing"
"time"
)
type E1 struct {
F01 int `schema:"f01"`
F02 int `schema:"-"`
F03 string `schema:"f03"`
F04 string `schema:"f04,omitempty"`
F05 bool `schema:"f05"`
F06 bool `schema:"f06"`
F07 *string `schema:"f07"`
F08 *int8 `schema:"f08"`
F09 float64 `schema:"f09"`
F10 func() `schema:"f10"`
F11 inner
}
type inner struct {
F12 int
}
func TestFilled(t *testing.T) {
f07 := "seven"
var f08 int8 = 8
s := &E1{
F01: 1,
F02: 2,
F03: "three",
F04: "four",
F05: true,
F06: false,
F07: &f07,
F08: &f08,
F09: 1.618,
F10: func() {},
F11: inner{12},
}
vals := make(map[string][]string)
errs := NewEncoder().Encode(s, vals)
valExists(t, "f01", "1", vals)
valNotExists(t, "f02", vals)
valExists(t, "f03", "three", vals)
valExists(t, "f05", "true", vals)
valExists(t, "f06", "false", vals)
valExists(t, "f07", "seven", vals)
valExists(t, "f08", "8", vals)
valExists(t, "f09", "1.618000", vals)
valExists(t, "F12", "12", vals)
emptyErr := MultiError{}
if errs.Error() == emptyErr.Error() {
t.Errorf("Expected error got %v", errs)
}
}
type Aa int
type E3 struct {
F01 bool `schema:"f01"`
F02 float32 `schema:"f02"`
F03 float64 `schema:"f03"`
F04 int `schema:"f04"`
F05 int8 `schema:"f05"`
F06 int16 `schema:"f06"`
F07 int32 `schema:"f07"`
F08 int64 `schema:"f08"`
F09 string `schema:"f09"`
F10 uint `schema:"f10"`
F11 uint8 `schema:"f11"`
F12 uint16 `schema:"f12"`
F13 uint32 `schema:"f13"`
F14 uint64 `schema:"f14"`
F15 Aa `schema:"f15"`
}
// Test compatibility with default decoder types.
func TestCompat(t *testing.T) {
src := &E3{
F01: true,
F02: 4.2,
F03: 4.3,
F04: -42,
F05: -43,
F06: -44,
F07: -45,
F08: -46,
F09: "foo",
F10: 42,
F11: 43,
F12: 44,
F13: 45,
F14: 46,
F15: 1,
}
dst := &E3{}
vals := make(map[string][]string)
encoder := NewEncoder()
decoder := NewDecoder()
encoder.RegisterEncoder(src.F15, func(reflect.Value) string { return "1" })
decoder.RegisterConverter(src.F15, func(string) reflect.Value { return reflect.ValueOf(1) })
err := encoder.Encode(src, vals)
if err != nil {
t.Errorf("Encoder has non-nil error: %v", err)
}
err = decoder.Decode(dst, vals)
if err != nil {
t.Errorf("Decoder has non-nil error: %v", err)
}
if *src != *dst {
t.Errorf("Decoder-Encoder compatibility: expected %v, got %v\n", src, dst)
}
}
func TestEmpty(t *testing.T) {
s := &E1{
F01: 1,
F02: 2,
F03: "three",
}
estr := "schema: encoder not found for <nil>"
vals := make(map[string][]string)
err := NewEncoder().Encode(s, vals)
if err.Error() != estr {
t.Errorf("Expected: %s, got %v", estr, err)
}
valExists(t, "f03", "three", vals)
valNotExists(t, "f04", vals)
}
func TestStruct(t *testing.T) {
estr := "schema: interface must be a struct"
vals := make(map[string][]string)
err := NewEncoder().Encode("hello world", vals)
if err.Error() != estr {
t.Errorf("Expected: %s, got %v", estr, err)
}
}
func TestSlices(t *testing.T) {
type oneAsWord int
ones := []oneAsWord{1, 2}
s1 := &struct {
ones []oneAsWord `schema:"ones"`
ints []int `schema:"ints"`
nonempty []int `schema:"nonempty"`
empty []int `schema:"empty,omitempty"`
}{ones, []int{1, 1}, []int{}, []int{}}
vals := make(map[string][]string)
encoder := NewEncoder()
encoder.RegisterEncoder(ones[0], func(v reflect.Value) string { return "one" })
err := encoder.Encode(s1, vals)
if err != nil {
t.Errorf("Encoder has non-nil error: %v", err)
}
valsExist(t, "ones", []string{"one", "one"}, vals)
valsExist(t, "ints", []string{"1", "1"}, vals)
valsExist(t, "nonempty", []string{}, vals)
valNotExists(t, "empty", vals)
}
func TestCompatSlices(t *testing.T) {
type oneAsWord int
type s1 struct {
Ones []oneAsWord `schema:"ones"`
Ints []int `schema:"ints"`
}
ones := []oneAsWord{1, 1}
src := &s1{ones, []int{1, 1}}
vals := make(map[string][]string)
dst := &s1{}
encoder := NewEncoder()
encoder.RegisterEncoder(ones[0], func(v reflect.Value) string { return "one" })
decoder := NewDecoder()
decoder.RegisterConverter(ones[0], func(s string) reflect.Value {
if s == "one" {
return reflect.ValueOf(1)
}
return reflect.ValueOf(2)
})
err := encoder.Encode(src, vals)
if err != nil {
t.Errorf("Encoder has non-nil error: %v", err)
}
err = decoder.Decode(dst, vals)
if err != nil {
t.Errorf("Dncoder has non-nil error: %v", err)
}
if len(src.Ints) != len(dst.Ints) || len(src.Ones) != len(dst.Ones) {
t.Fatalf("Expected %v, got %v", src, dst)
}
for i, v := range src.Ones {
if dst.Ones[i] != v {
t.Fatalf("Expected %v, got %v", v, dst.Ones[i])
}
}
for i, v := range src.Ints {
if dst.Ints[i] != v {
t.Fatalf("Expected %v, got %v", v, dst.Ints[i])
}
}
}
func TestRegisterEncoder(t *testing.T) {
type oneAsWord int
type twoAsWord int
type oneSliceAsWord []int
s1 := &struct {
oneAsWord
twoAsWord
oneSliceAsWord
}{1, 2, []int{1, 1}}
v1 := make(map[string][]string)
encoder := NewEncoder()
encoder.RegisterEncoder(s1.oneAsWord, func(v reflect.Value) string { return "one" })
encoder.RegisterEncoder(s1.twoAsWord, func(v reflect.Value) string { return "two" })
encoder.RegisterEncoder(s1.oneSliceAsWord, func(v reflect.Value) string { return "one" })
err := encoder.Encode(s1, v1)
if err != nil {
t.Errorf("Encoder has non-nil error: %v", err)
}
valExists(t, "oneAsWord", "one", v1)
valExists(t, "twoAsWord", "two", v1)
valExists(t, "oneSliceAsWord", "one", v1)
}
func TestEncoderOrder(t *testing.T) {
type builtinEncoderSimple int
type builtinEncoderSimpleOverridden int
type builtinEncoderSlice []int
type builtinEncoderSliceOverridden []int
type builtinEncoderStruct struct{ nr int }
type builtinEncoderStructOverridden struct{ nr int }
s1 := &struct {
builtinEncoderSimple `schema:"simple"`
builtinEncoderSimpleOverridden `schema:"simple_overridden"`
builtinEncoderSlice `schema:"slice"`
builtinEncoderSliceOverridden `schema:"slice_overridden"`
builtinEncoderStruct `schema:"struct"`
builtinEncoderStructOverridden `schema:"struct_overridden"`
}{
1,
1,
[]int{2},
[]int{2},
builtinEncoderStruct{3},
builtinEncoderStructOverridden{3},
}
v1 := make(map[string][]string)
encoder := NewEncoder()
encoder.RegisterEncoder(s1.builtinEncoderSimpleOverridden, func(v reflect.Value) string { return "one" })
encoder.RegisterEncoder(s1.builtinEncoderSliceOverridden, func(v reflect.Value) string { return "two" })
encoder.RegisterEncoder(s1.builtinEncoderStructOverridden, func(v reflect.Value) string { return "three" })
err := encoder.Encode(s1, v1)
if err != nil {
t.Errorf("Encoder has non-nil error: %v", err)
}
valExists(t, "simple", "1", v1)
valExists(t, "simple_overridden", "one", v1)
valExists(t, "slice", "2", v1)
valExists(t, "slice_overridden", "two", v1)
valExists(t, "nr", "3", v1)
valExists(t, "struct_overridden", "three", v1)
}
func valExists(t *testing.T, key string, expect string, result map[string][]string) {
valsExist(t, key, []string{expect}, result)
}
func valsExist(t *testing.T, key string, expect []string, result map[string][]string) {
vals, ok := result[key]
if !ok {
t.Fatalf("Key not found. Expected: %s", key)
}
if len(expect) != len(vals) {
t.Fatalf("Expected: %v, got: %v", expect, vals)
}
for i, v := range expect {
if vals[i] != v {
t.Fatalf("Unexpected value. Expected: %v, got %v", v, vals[i])
}
}
}
func valNotExists(t *testing.T, key string, result map[string][]string) {
if val, ok := result[key]; ok {
t.Error("Key not omitted. Expected: empty; got: " + val[0] + ".")
}
}
func valsLength(t *testing.T, expectedLength int, result map[string][]string) {
length := len(result)
if length != expectedLength {
t.Errorf("Expected length of %v, but got %v", expectedLength, length)
}
}
func noError(t *testing.T, err error) {
if err != nil {
t.Errorf("Unexpected error. Got %v", err)
}
}
type E4 struct {
ID string `json:"id"`
}
func TestEncoderSetAliasTag(t *testing.T) {
data := map[string][]string{}
s := E4{
ID: "foo",
}
encoder := NewEncoder()
encoder.SetAliasTag("json")
err := encoder.Encode(&s, data)
if err != nil {
t.Fatalf("Failed to encode: %v", err)
}
valExists(t, "id", "foo", data)
}
type E5 struct {
F01 int `schema:"f01,omitempty"`
F02 string `schema:"f02,omitempty"`
F03 *string `schema:"f03,omitempty"`
F04 *int8 `schema:"f04,omitempty"`
F05 float64 `schema:"f05,omitempty"`
F06 E5F06 `schema:"f06,omitempty"`
F07 E5F06 `schema:"f07,omitempty"`
F08 []string `schema:"f08,omitempty"`
F09 []string `schema:"f09,omitempty"`
}
type E5F06 struct {
F0601 string `schema:"f0601,omitempty"`
}
func TestEncoderWithOmitempty(t *testing.T) {
vals := map[string][]string{}
s := E5{
F02: "test",
F07: E5F06{
F0601: "test",
},
F09: []string{"test"},
}
encoder := NewEncoder()
err := encoder.Encode(&s, vals)
if err != nil {
t.Fatalf("Failed to encode: %v", err)
}
valNotExists(t, "f01", vals)
valExists(t, "f02", "test", vals)
valNotExists(t, "f03", vals)
valNotExists(t, "f04", vals)
valNotExists(t, "f05", vals)
valNotExists(t, "f06", vals)
valExists(t, "f0601", "test", vals)
valNotExists(t, "f08", vals)
valsExist(t, "f09", []string{"test"}, vals)
}
type E6 struct {
F01 *inner
F02 *inner
F03 *inner `schema:",omitempty"`
}
func TestStructPointer(t *testing.T) {
vals := map[string][]string{}
s := E6{
F01: &inner{2},
}
encoder := NewEncoder()
err := encoder.Encode(&s, vals)
if err != nil {
t.Fatalf("Failed to encode: %v", err)
}
valExists(t, "F12", "2", vals)
valExists(t, "F02", "null", vals)
valNotExists(t, "F03", vals)
}
func TestRegisterEncoderCustomArrayType(t *testing.T) {
type CustomInt []int
type S1 struct {
SomeInts CustomInt `schema:",omitempty"`
}
ss := []S1{
{},
{CustomInt{}},
{CustomInt{1, 2, 3}},
}
for s := range ss {
vals := map[string][]string{}
encoder := NewEncoder()
encoder.RegisterEncoder(CustomInt{}, func(value reflect.Value) string {
return fmt.Sprint(value.Interface())
})
err := encoder.Encode(ss[s], vals)
if err != nil {
t.Fatalf("Failed to encode: %v", err)
}
}
}
func TestRegisterEncoderStructIsZero(t *testing.T) {
type S1 struct {
SomeTime1 time.Time `schema:"tim1,omitempty"`
SomeTime2 time.Time `schema:"tim2,omitempty"`
}
ss := []*S1{
{
SomeTime1: time.Date(2020, 8, 4, 13, 30, 1, 0, time.UTC),
},
}
for s := range ss {
vals := map[string][]string{}
encoder := NewEncoder()
encoder.RegisterEncoder(time.Time{}, func(value reflect.Value) string {
return value.Interface().(time.Time).Format(time.RFC3339Nano)
})
err := encoder.Encode(ss[s], vals)
if err != nil {
t.Errorf("Encoder has non-nil error: %v", err)
}
ta, ok := vals["tim1"]
if !ok {
t.Error("expected tim1 to be present")
}
if len(ta) != 1 {
t.Error("expected tim1 to be present")
}
if ta[0] != "2020-08-04T13:30:01Z" {
t.Error("expected correct tim1 time")
}
_, ok = vals["tim2"]
if ok {
t.Error("expected tim1 not to be present")
}
}
}
func TestRegisterEncoderWithPtrType(t *testing.T) {
type CustomTime struct {
time time.Time
}
type S1 struct {
DateStart *CustomTime
DateEnd *CustomTime
Empty *CustomTime `schema:"empty,omitempty"`
}
ss := S1{
DateStart: &CustomTime{time: time.Now()},
DateEnd: nil,
}
encoder := NewEncoder()
encoder.RegisterEncoder(&CustomTime{}, func(value reflect.Value) string {
if value.IsNil() {
return ""
}
custom := value.Interface().(*CustomTime)
return custom.time.String()
})
vals := map[string][]string{}
err := encoder.Encode(ss, vals)
noError(t, err)
valsLength(t, 2, vals)
valExists(t, "DateStart", ss.DateStart.time.String(), vals)
valExists(t, "DateEnd", "", vals)
}

27
schema/license.txt Normal file
View file

@ -0,0 +1,27 @@
Copyright (c) 2023 The Gorilla Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

94
schema/readme.md Normal file
View file

@ -0,0 +1,94 @@
# gorilla/schema
![testing](https://github.com/gorilla/schema/actions/workflows/test.yml/badge.svg)
[![codecov](https://codecov.io/github/gorilla/schema/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/schema)
[![godoc](https://godoc.org/github.com/gorilla/schema?status.svg)](https://godoc.org/github.com/gorilla/schema)
[![sourcegraph](https://sourcegraph.com/github.com/gorilla/schema/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/schema?badge)
![Gorilla Logo](https://github.com/gorilla/.github/assets/53367916/d92caabf-98e0-473e-bfbf-ab554ba435e5)
Package gorilla/schema converts structs to and from form values.
## Example
Here's a quick example: we parse POST form values and then decode them into a struct:
```go
// Set a Decoder instance as a package global, because it caches
// meta-data about structs, and an instance can be shared safely.
var decoder = schema.NewDecoder()
type Person struct {
Name string
Phone string
}
func MyHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
// Handle error
}
var person Person
// r.PostForm is a map of our POST form values
err = decoder.Decode(&person, r.PostForm)
if err != nil {
// Handle error
}
// Do something with person.Name or person.Phone
}
```
Conversely, contents of a struct can be encoded into form values. Here's a variant of the previous example using the Encoder:
```go
var encoder = schema.NewEncoder()
func MyHttpRequest() {
person := Person{"Jane Doe", "555-5555"}
form := url.Values{}
err := encoder.Encode(person, form)
if err != nil {
// Handle error
}
// Use form values, for example, with an http client
client := new(http.Client)
res, err := client.PostForm("http://my-api.test", form)
}
```
To define custom names for fields, use a struct tag "schema". To not populate certain fields, use a dash for the name and it will be ignored:
```go
type Person struct {
Name string `schema:"name,required"` // custom name, must be supplied
Phone string `schema:"phone"` // custom name
Admin bool `schema:"-"` // this field is never set
}
```
The supported field types in the struct are:
* bool
* float variants (float32, float64)
* int variants (int, int8, int16, int32, int64)
* string
* uint variants (uint, uint8, uint16, uint32, uint64)
* struct
* a pointer to one of the above types
* a slice or a pointer to a slice of one of the above types
Unsupported types are simply ignored, however custom types can be registered to be converted.
More examples are available on the Gorilla website: https://www.gorillatoolkit.org/pkg/schema
## License
BSD licensed. See the LICENSE file for details.

View file

@ -6,3 +6,4 @@ func Redirect(u string, status Status) Handler {
c.Redirect(u, status)
})
}