Skip to content

Commit

Permalink
Add embedded struct support + options
Browse files Browse the repository at this point in the history
  • Loading branch information
Dean Karn committed Jul 30, 2017
1 parent 20d0406 commit 920e9aa
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 9 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package form
============
<img align="right" src="https://raw.githubusercontent.com/go-playground/form/master/logo.jpg">![Project status](https://img.shields.io/badge/version-2.3.0-green.svg)
<img align="right" src="https://raw.githubusercontent.com/go-playground/form/master/logo.jpg">![Project status](https://img.shields.io/badge/version-3.0.0-green.svg)
[![Build Status](https://semaphoreci.com/api/v1/joeybloggs/form/branches/master/badge.svg)](https://semaphoreci.com/joeybloggs/form)
[![Coverage Status](https://coveralls.io/repos/github/go-playground/form/badge.svg?branch=master)](https://coveralls.io/github/go-playground/form?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/form)](https://goreportcard.com/report/github.com/go-playground/form)
Expand Down
65 changes: 65 additions & 0 deletions _examples/decoder-embedded/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"fmt"
"log"
"net/url"

"github.com/go-playground/form"
)

// A ...
type A struct {
Field string
}

// B ...
type B struct {
A
Field string
}

// use a single instance of Decoder, it caches struct info
var decoder *form.Decoder

func main() {
decoder = form.NewDecoder()

// this simulates the results of http.Request's ParseForm() function
values := parseFormB()

var b B

// must pass a pointer
err := decoder.Decode(&b, values)
if err != nil {
log.Panic(err)
}

fmt.Printf("%#v\n", b)

values = parseFormAB()

// must pass a pointer
err = decoder.Decode(&b, values)
if err != nil {
log.Panic(err)
}

fmt.Printf("%#v\n", b)
}

// this simulates the results of http.Request's ParseForm() function
func parseFormB() url.Values {
return url.Values{
"Field": []string{"B FieldVal"},
}
}

// this simulates the results of http.Request's ParseForm() function
func parseFormAB() url.Values {
return url.Values{
"Field": []string{"B FieldVal"},
"A.Field": []string{"A FieldVal"},
}
}
File renamed without changes.
58 changes: 58 additions & 0 deletions _examples/encoder-embedded/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package main

import (
"fmt"
"log"

"github.com/go-playground/form"
)

// A ...
type A struct {
Field string
}

// B ...
type B struct {
A
Field string
}

// use a single instance of Encoder, it caches struct info
var encoder *form.Encoder

func main() {

type A struct {
Field string
}

type B struct {
A
Field string
}

b := B{
A: A{
Field: "A Val",
},
Field: "B Val",
}

encoder = form.NewEncoder()

v, err := encoder.Encode(b)
if err != nil {
log.Panic(err)
}

fmt.Printf("%#v\n", v)

encoder.SetAnonymousMode(form.AnonymousSeparate)
v, err = encoder.Encode(b)
if err != nil {
log.Panic(err)
}

fmt.Printf("%#v\n", v)
}
File renamed without changes.
27 changes: 23 additions & 4 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,34 @@ package form

import (
"reflect"
"sort"
"strings"
"sync"
"sync/atomic"
)

type cacheFields []cachedField

func (s cacheFields) Len() int {
return len(s)
}

func (s cacheFields) Less(i, j int) bool {
return !s[i].isAnonymous
}

func (s cacheFields) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

type cachedField struct {
idx int
name string
idx int
name string
isAnonymous bool
}

type cachedStruct struct {
fields []cachedField
fields cacheFields
}

type structCacheMap struct {
Expand Down Expand Up @@ -74,6 +90,7 @@ func (s *structCacheMap) parseStruct(mode Mode, current reflect.Value, key refle

fld = typ.Field(i)

// fmt.Println("PkgPath:", fld.PkgPath, " Anonymous:", fld.Anonymous, " Name:", fld.Name)
if fld.PkgPath != blank && !fld.Anonymous {
continue
}
Expand All @@ -88,6 +105,7 @@ func (s *structCacheMap) parseStruct(mode Mode, current reflect.Value, key refle
}
}

// fmt.Println("Ignore:", name == ignore)
if name == ignore {
continue
}
Expand All @@ -100,9 +118,10 @@ func (s *structCacheMap) parseStruct(mode Mode, current reflect.Value, key refle
name = fld.Name
}

cs.fields = append(cs.fields, cachedField{idx: i, name: name})
cs.fields = append(cs.fields, cachedField{idx: i, name: name, isAnonymous: fld.Anonymous})
}

sort.Sort(cs.fields)
s.Set(typ, cs)

s.lock.Unlock()
Expand Down
6 changes: 6 additions & 0 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ func (d *decoder) traverseStruct(v reflect.Value, typ reflect.Type, namespace []

namespace = namespace[:l]

if f.isAnonymous {
if d.setFieldByType(v.Field(f.idx), namespace, 0) {
set = true
}
}

if first {
namespace = append(namespace, f.name...)
} else {
Expand Down
35 changes: 35 additions & 0 deletions decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1506,3 +1506,38 @@ func TestDecoderRegisterTagNameFunc(t *testing.T) {
Equal(t, test.Value, "joeybloggs")
Equal(t, test.Ignore, "")
}

func TestDecoderEmbedModes(t *testing.T) {

type A struct {
Field string
}

type B struct {
A
Field string
}

var b B

decoder := NewDecoder()

values := url.Values{
"Field": []string{"Value"},
}

err := decoder.Decode(&b, values)
Equal(t, err, nil)
Equal(t, b.Field, "Value")
Equal(t, b.A.Field, "Value")

values = url.Values{
"Field": []string{"B Val"},
"A.Field": []string{"A Val"},
}

err = decoder.Decode(&b, values)
Equal(t, err, nil)
Equal(t, b.Field, "B Val")
Equal(t, b.A.Field, "A Val")
}
6 changes: 5 additions & 1 deletion encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,13 @@ func (e *encoder) traverseStruct(v reflect.Value, namespace []byte, idx int) {
}

for _, f := range s.fields {

namespace = namespace[:l]

if f.isAnonymous && e.e.embedAnonymous {
e.setFieldByType(v.Field(f.idx), namespace, idx)
continue
}

if first {
namespace = append(namespace, f.name...)
} else {
Expand Down
34 changes: 34 additions & 0 deletions encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1286,3 +1286,37 @@ func TestEncoderRegisterTagNameFunc(t *testing.T) {
Equal(t, len(values), 1)
Equal(t, values["name"][0], "Joeybloggs")
}

func TestEncoderEmbedModes(t *testing.T) {

type A struct {
Field string
}

type B struct {
A
Field string
}

b := B{
A: A{
Field: "A Val",
},
Field: "B Val",
}

encoder := NewEncoder()

values, err := encoder.Encode(b)
Equal(t, err, nil)
Equal(t, len(values), 1)
Equal(t, values["Field"][0], "B Val")
Equal(t, values["Field"][1], "A Val")

encoder.SetAnonymousMode(AnonymousSeparate)
values, err = encoder.Encode(b)
Equal(t, err, nil)
Equal(t, len(values), 2)
Equal(t, values["Field"][0], "B Val")
Equal(t, values["A.Field"][0], "A Val")
}
18 changes: 18 additions & 0 deletions form.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,21 @@ const (
// and that tag is not the ignore '-' tag
ModeExplicit
)

// AnonymousMode specifies how data should be rolled up
// or separated from anonymous structs
type AnonymousMode uint8

const (
// AnonymousEmbed embeds anonymous data when encoding
// eg. type A struct { Field string }
// type B struct { A, Field string }
// encode results: url.Values{"Field":[]string{"B FieldVal", "A FieldVal"}}
AnonymousEmbed AnonymousMode = iota

// AnonymousSeparate does not embed anonymous data when encoding
// eg. type A struct { Field string }
// type B struct { A, Field string }
// encode results: url.Values{"Field":[]string{"B FieldVal"}, "A.Field":[]string{"A FieldVal"}}
AnonymousSeparate
)
14 changes: 11 additions & 3 deletions form_encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,17 @@ type Encoder struct {
structCache *structCacheMap
customTypeFuncs map[reflect.Type]EncodeCustomTypeFunc
dataPool *sync.Pool
embedAnonymous bool
}

// NewEncoder creates a new encoder instance with sane defaults
func NewEncoder() *Encoder {

e := &Encoder{
tagName: "form",
mode: ModeImplicit,
structCache: newStructCacheMap(),
tagName: "form",
mode: ModeImplicit,
structCache: newStructCacheMap(),
embedAnonymous: true,
}

e.dataPool = &sync.Pool{New: func() interface{} {
Expand All @@ -82,6 +84,12 @@ func (e *Encoder) SetMode(mode Mode) {
e.mode = mode
}

// SetAnonymousMode sets the mode the encoder should run
// Default is AnonymousEmbed
func (e *Encoder) SetAnonymousMode(mode AnonymousMode) {
e.embedAnonymous = mode == AnonymousEmbed
}

// RegisterTagNameFunc registers a custom tag name parser function
// NOTE: This method is not thread-safe it is intended that these all be registered prior to any parsing
//
Expand Down

0 comments on commit 920e9aa

Please sign in to comment.