Skip to content

Commit

Permalink
Allow to change default tags for Decoder and Encoder (pelletier#241)
Browse files Browse the repository at this point in the history
Decoder: allow to customize default field name tag "toml" on decoding.
Example:
```
type doc struct {
    title `file:"header"`
}
```

Encoder: allow to customize tags for encoding struct to toml.
Example:
```
type doc struct {
    title `file:"header" description:"document title"`
}
```

Fixes pelletier#238
  • Loading branch information
asenyshyn authored and pelletier committed Sep 21, 2018
1 parent e33f654 commit 90d6f96
Show file tree
Hide file tree
Showing 2 changed files with 263 additions and 11 deletions.
76 changes: 65 additions & 11 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import (
"time"
)

const tagKeyMultiline = "multiline"
const (
tagFieldName = "toml"
tagFieldComment = "comment"
tagCommented = "commented"
tagMultiline = "multiline"
)

type tomlOpts struct {
name string
Expand All @@ -31,6 +36,20 @@ var encOptsDefaults = encOpts{
quoteMapKeys: false,
}

type annotation struct {
tag string
comment string
commented string
multiline string
}

var annotationDefault = annotation{
tag: tagFieldName,
comment: tagFieldComment,
commented: tagCommented,
multiline: tagMultiline,
}

var timeType = reflect.TypeOf(time.Time{})
var marshalerType = reflect.TypeOf(new(Marshaler)).Elem()

Expand Down Expand Up @@ -145,13 +164,15 @@ func Marshal(v interface{}) ([]byte, error) {
type Encoder struct {
w io.Writer
encOpts
annotation
}

// NewEncoder returns a new encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: w,
encOpts: encOptsDefaults,
w: w,
encOpts: encOptsDefaults,
annotation: annotationDefault,
}
}

Expand Down Expand Up @@ -197,6 +218,30 @@ func (e *Encoder) ArraysWithOneElementPerLine(v bool) *Encoder {
return e
}

// SetTagName allows changing default tag "toml"
func (e *Encoder) SetTagName(v string) *Encoder {
e.tag = v
return e
}

// SetTagComment allows changing default tag "comment"
func (e *Encoder) SetTagComment(v string) *Encoder {
e.comment = v
return e
}

// SetTagCommented allows changing default tag "commented"
func (e *Encoder) SetTagCommented(v string) *Encoder {
e.commented = v
return e
}

// SetTagMultiline allows changing default tag "multiline"
func (e *Encoder) SetTagMultiline(v string) *Encoder {
e.multiline = v
return e
}

func (e *Encoder) marshal(v interface{}) ([]byte, error) {
mtype := reflect.TypeOf(v)
if mtype.Kind() != reflect.Struct {
Expand Down Expand Up @@ -227,7 +272,7 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
case reflect.Struct:
for i := 0; i < mtype.NumField(); i++ {
mtypef, mvalf := mtype.Field(i), mval.Field(i)
opts := tomlOptions(mtypef)
opts := tomlOptions(mtypef, e.annotation)
if opts.include && (!opts.omitempty || !isZero(mvalf)) {
val, err := e.valueToToml(mtypef.Type, mvalf)
if err != nil {
Expand Down Expand Up @@ -326,7 +371,7 @@ func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface
// Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for
// sub-structs, and only definite types can be unmarshaled.
func (t *Tree) Unmarshal(v interface{}) error {
d := Decoder{tval: t}
d := Decoder{tval: t, tagName: tagFieldName}
return d.unmarshal(v)
}

Expand Down Expand Up @@ -362,13 +407,15 @@ type Decoder struct {
r io.Reader
tval *Tree
encOpts
tagName string
}

// NewDecoder returns a new decoder that reads from r.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{
r: r,
encOpts: encOptsDefaults,
tagName: tagFieldName,
}
}

Expand All @@ -385,6 +432,12 @@ func (d *Decoder) Decode(v interface{}) error {
return d.unmarshal(v)
}

// SetTagName allows changing default tag "toml"
func (d *Decoder) SetTagName(v string) *Decoder {
d.tagName = v
return d
}

func (d *Decoder) unmarshal(v interface{}) error {
mtype := reflect.TypeOf(v)
if mtype.Kind() != reflect.Ptr || mtype.Elem().Kind() != reflect.Struct {
Expand All @@ -410,7 +463,8 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree) (reflect.Value,
mval = reflect.New(mtype).Elem()
for i := 0; i < mtype.NumField(); i++ {
mtypef := mtype.Field(i)
opts := tomlOptions(mtypef)
an := annotation{tag: d.tagName}
opts := tomlOptions(mtypef, an)
if opts.include {
baseKey := opts.name
keysToTry := []string{baseKey, strings.ToLower(baseKey), strings.ToTitle(baseKey)}
Expand Down Expand Up @@ -560,15 +614,15 @@ func (d *Decoder) unwrapPointer(mtype reflect.Type, tval interface{}) (reflect.V
return mval, nil
}

func tomlOptions(vf reflect.StructField) tomlOpts {
tag := vf.Tag.Get("toml")
func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
tag := vf.Tag.Get(an.tag)
parse := strings.Split(tag, ",")
var comment string
if c := vf.Tag.Get("comment"); c != "" {
if c := vf.Tag.Get(an.comment); c != "" {
comment = c
}
commented, _ := strconv.ParseBool(vf.Tag.Get("commented"))
multiline, _ := strconv.ParseBool(vf.Tag.Get(tagKeyMultiline))
commented, _ := strconv.ParseBool(vf.Tag.Get(an.commented))
multiline, _ := strconv.ParseBool(vf.Tag.Get(an.multiline))
result := tomlOpts{name: vf.Name, comment: comment, commented: commented, multiline: multiline, include: true, omitempty: false}
if parse[0] != "" {
if parse[0] == "-" && len(parse) == 1 {
Expand Down
198 changes: 198 additions & 0 deletions marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -804,3 +804,201 @@ func TestMarshalArrayOnePerLine(t *testing.T) {
t.Errorf("Bad arrays marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, b)
}
}

var customTagTestToml = []byte(`
[postgres]
password = "bvalue"
user = "avalue"
[[postgres.My]]
My = "Foo"
[[postgres.My]]
My = "Baar"
`)

func TestMarshalCustomTag(t *testing.T) {
type TypeC struct {
My string
}
type TypeB struct {
AttrA string `file:"user"`
AttrB string `file:"password"`
My []TypeC
}
type TypeA struct {
TypeB TypeB `file:"postgres"`
}

ta := []TypeC{{My: "Foo"}, {My: "Baar"}}
config := TypeA{TypeB{AttrA: "avalue", AttrB: "bvalue", My: ta}}
var buf bytes.Buffer
err := NewEncoder(&buf).SetTagName("file").Encode(config)
if err != nil {
t.Fatal(err)
}
expected := customTagTestToml
result := buf.Bytes()
if !bytes.Equal(result, expected) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}

var customCommentTagTestToml = []byte(`
# db connection
[postgres]
# db pass
password = "bvalue"
# db user
user = "avalue"
`)

func TestMarshalCustomComment(t *testing.T) {
type TypeB struct {
AttrA string `toml:"user" descr:"db user"`
AttrB string `toml:"password" descr:"db pass"`
}
type TypeA struct {
TypeB TypeB `toml:"postgres" descr:"db connection"`
}

config := TypeA{TypeB{AttrA: "avalue", AttrB: "bvalue"}}
var buf bytes.Buffer
err := NewEncoder(&buf).SetTagComment("descr").Encode(config)
if err != nil {
t.Fatal(err)
}
expected := customCommentTagTestToml
result := buf.Bytes()
if !bytes.Equal(result, expected) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}

var customCommentedTagTestToml = []byte(`
[postgres]
# password = "bvalue"
# user = "avalue"
`)

func TestMarshalCustomCommented(t *testing.T) {
type TypeB struct {
AttrA string `toml:"user" disable:"true"`
AttrB string `toml:"password" disable:"true"`
}
type TypeA struct {
TypeB TypeB `toml:"postgres"`
}

config := TypeA{TypeB{AttrA: "avalue", AttrB: "bvalue"}}
var buf bytes.Buffer
err := NewEncoder(&buf).SetTagCommented("disable").Encode(config)
if err != nil {
t.Fatal(err)
}
expected := customCommentedTagTestToml
result := buf.Bytes()
if !bytes.Equal(result, expected) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}

var customMultilineTagTestToml = []byte(`int_slice = [
1,
2,
3,
]
`)

func TestMarshalCustomMultiline(t *testing.T) {
type TypeA struct {
AttrA []int `toml:"int_slice" mltln:"true"`
}

config := TypeA{AttrA: []int{1, 2, 3}}
var buf bytes.Buffer
err := NewEncoder(&buf).ArraysWithOneElementPerLine(true).SetTagMultiline("mltln").Encode(config)
if err != nil {
t.Fatal(err)
}
expected := customMultilineTagTestToml
result := buf.Bytes()
if !bytes.Equal(result, expected) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}

var testDocBasicToml = []byte(`
[document]
bool_val = true
date_val = 1979-05-27T07:32:00Z
float_val = 123.4
int_val = 5000
string_val = "Bite me"
uint_val = 5001
`)

type testDocCustomTag struct {
Doc testDocBasicsCustomTag `file:"document"`
}
type testDocBasicsCustomTag struct {
Bool bool `file:"bool_val"`
Date time.Time `file:"date_val"`
Float float32 `file:"float_val"`
Int int `file:"int_val"`
Uint uint `file:"uint_val"`
String *string `file:"string_val"`
unexported int `file:"shouldntBeHere"`
}

var testDocCustomTagData = testDocCustomTag{
Doc: testDocBasicsCustomTag{
Bool: true,
Date: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
Float: 123.4,
Int: 5000,
Uint: 5001,
String: &biteMe,
unexported: 0,
},
}

func TestUnmarshalCustomTag(t *testing.T) {
buf := bytes.NewBuffer(testDocBasicToml)

result := testDocCustomTag{}
err := NewDecoder(buf).SetTagName("file").Decode(&result)
if err != nil {
t.Fatal(err)
}
expected := testDocCustomTagData
if !reflect.DeepEqual(result, expected) {
resStr, _ := json.MarshalIndent(result, "", " ")
expStr, _ := json.MarshalIndent(expected, "", " ")
t.Errorf("Bad unmarshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expStr, resStr)

}
}

func TestUnmarshalMap(t *testing.T) {
m := make(map[string]int)
m["a"] = 1

err := Unmarshal(basicTestToml, m)
if err.Error() != "Only a pointer to struct can be unmarshaled from TOML" {
t.Fail()
}
}

func TestMarshalMap(t *testing.T) {
m := make(map[string]int)
m["a"] = 1

var buf bytes.Buffer
err := NewEncoder(&buf).Encode(m)
if err.Error() != "Only a struct can be marshaled to TOML" {
t.Fail()
}
}

0 comments on commit 90d6f96

Please sign in to comment.