Allow to change default tags for Decoder and Encoder (#241)
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 #238
This commit is contained in:
committed by
Thomas Pelletier
parent
e33f654429
commit
90d6f96e9e
+65
-11
@@ -11,7 +11,12 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const tagKeyMultiline = "multiline"
|
||||
const (
|
||||
tagFieldName = "toml"
|
||||
tagFieldComment = "comment"
|
||||
tagCommented = "commented"
|
||||
tagMultiline = "multiline"
|
||||
)
|
||||
|
||||
type tomlOpts struct {
|
||||
name string
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -362,6 +407,7 @@ type Decoder struct {
|
||||
r io.Reader
|
||||
tval *Tree
|
||||
encOpts
|
||||
tagName string
|
||||
}
|
||||
|
||||
// NewDecoder returns a new decoder that reads from r.
|
||||
@@ -369,6 +415,7 @@ func NewDecoder(r io.Reader) *Decoder {
|
||||
return &Decoder{
|
||||
r: r,
|
||||
encOpts: encOptsDefaults,
|
||||
tagName: tagFieldName,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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)}
|
||||
@@ -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 {
|
||||
|
||||
+198
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user