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"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tagKeyMultiline = "multiline"
|
const (
|
||||||
|
tagFieldName = "toml"
|
||||||
|
tagFieldComment = "comment"
|
||||||
|
tagCommented = "commented"
|
||||||
|
tagMultiline = "multiline"
|
||||||
|
)
|
||||||
|
|
||||||
type tomlOpts struct {
|
type tomlOpts struct {
|
||||||
name string
|
name string
|
||||||
@@ -31,6 +36,20 @@ var encOptsDefaults = encOpts{
|
|||||||
quoteMapKeys: false,
|
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 timeType = reflect.TypeOf(time.Time{})
|
||||||
var marshalerType = reflect.TypeOf(new(Marshaler)).Elem()
|
var marshalerType = reflect.TypeOf(new(Marshaler)).Elem()
|
||||||
|
|
||||||
@@ -145,13 +164,15 @@ func Marshal(v interface{}) ([]byte, error) {
|
|||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
w io.Writer
|
w io.Writer
|
||||||
encOpts
|
encOpts
|
||||||
|
annotation
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEncoder returns a new encoder that writes to w.
|
// NewEncoder returns a new encoder that writes to w.
|
||||||
func NewEncoder(w io.Writer) *Encoder {
|
func NewEncoder(w io.Writer) *Encoder {
|
||||||
return &Encoder{
|
return &Encoder{
|
||||||
w: w,
|
w: w,
|
||||||
encOpts: encOptsDefaults,
|
encOpts: encOptsDefaults,
|
||||||
|
annotation: annotationDefault,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +218,30 @@ func (e *Encoder) ArraysWithOneElementPerLine(v bool) *Encoder {
|
|||||||
return e
|
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) {
|
func (e *Encoder) marshal(v interface{}) ([]byte, error) {
|
||||||
mtype := reflect.TypeOf(v)
|
mtype := reflect.TypeOf(v)
|
||||||
if mtype.Kind() != reflect.Struct {
|
if mtype.Kind() != reflect.Struct {
|
||||||
@@ -227,7 +272,7 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
|
|||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
for i := 0; i < mtype.NumField(); i++ {
|
for i := 0; i < mtype.NumField(); i++ {
|
||||||
mtypef, mvalf := mtype.Field(i), mval.Field(i)
|
mtypef, mvalf := mtype.Field(i), mval.Field(i)
|
||||||
opts := tomlOptions(mtypef)
|
opts := tomlOptions(mtypef, e.annotation)
|
||||||
if opts.include && (!opts.omitempty || !isZero(mvalf)) {
|
if opts.include && (!opts.omitempty || !isZero(mvalf)) {
|
||||||
val, err := e.valueToToml(mtypef.Type, mvalf)
|
val, err := e.valueToToml(mtypef.Type, mvalf)
|
||||||
if err != nil {
|
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
|
// Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for
|
||||||
// sub-structs, and only definite types can be unmarshaled.
|
// sub-structs, and only definite types can be unmarshaled.
|
||||||
func (t *Tree) Unmarshal(v interface{}) error {
|
func (t *Tree) Unmarshal(v interface{}) error {
|
||||||
d := Decoder{tval: t}
|
d := Decoder{tval: t, tagName: tagFieldName}
|
||||||
return d.unmarshal(v)
|
return d.unmarshal(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,6 +407,7 @@ type Decoder struct {
|
|||||||
r io.Reader
|
r io.Reader
|
||||||
tval *Tree
|
tval *Tree
|
||||||
encOpts
|
encOpts
|
||||||
|
tagName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDecoder returns a new decoder that reads from r.
|
// NewDecoder returns a new decoder that reads from r.
|
||||||
@@ -369,6 +415,7 @@ func NewDecoder(r io.Reader) *Decoder {
|
|||||||
return &Decoder{
|
return &Decoder{
|
||||||
r: r,
|
r: r,
|
||||||
encOpts: encOptsDefaults,
|
encOpts: encOptsDefaults,
|
||||||
|
tagName: tagFieldName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,6 +432,12 @@ func (d *Decoder) Decode(v interface{}) error {
|
|||||||
return d.unmarshal(v)
|
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 {
|
func (d *Decoder) unmarshal(v interface{}) error {
|
||||||
mtype := reflect.TypeOf(v)
|
mtype := reflect.TypeOf(v)
|
||||||
if mtype.Kind() != reflect.Ptr || mtype.Elem().Kind() != reflect.Struct {
|
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()
|
mval = reflect.New(mtype).Elem()
|
||||||
for i := 0; i < mtype.NumField(); i++ {
|
for i := 0; i < mtype.NumField(); i++ {
|
||||||
mtypef := mtype.Field(i)
|
mtypef := mtype.Field(i)
|
||||||
opts := tomlOptions(mtypef)
|
an := annotation{tag: d.tagName}
|
||||||
|
opts := tomlOptions(mtypef, an)
|
||||||
if opts.include {
|
if opts.include {
|
||||||
baseKey := opts.name
|
baseKey := opts.name
|
||||||
keysToTry := []string{baseKey, strings.ToLower(baseKey), strings.ToTitle(baseKey)}
|
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
|
return mval, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func tomlOptions(vf reflect.StructField) tomlOpts {
|
func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
|
||||||
tag := vf.Tag.Get("toml")
|
tag := vf.Tag.Get(an.tag)
|
||||||
parse := strings.Split(tag, ",")
|
parse := strings.Split(tag, ",")
|
||||||
var comment string
|
var comment string
|
||||||
if c := vf.Tag.Get("comment"); c != "" {
|
if c := vf.Tag.Get(an.comment); c != "" {
|
||||||
comment = c
|
comment = c
|
||||||
}
|
}
|
||||||
commented, _ := strconv.ParseBool(vf.Tag.Get("commented"))
|
commented, _ := strconv.ParseBool(vf.Tag.Get(an.commented))
|
||||||
multiline, _ := strconv.ParseBool(vf.Tag.Get(tagKeyMultiline))
|
multiline, _ := strconv.ParseBool(vf.Tag.Get(an.multiline))
|
||||||
result := tomlOpts{name: vf.Name, comment: comment, commented: commented, multiline: multiline, include: true, omitempty: false}
|
result := tomlOpts{name: vf.Name, comment: comment, commented: commented, multiline: multiline, include: true, omitempty: false}
|
||||||
if parse[0] != "" {
|
if parse[0] != "" {
|
||||||
if parse[0] == "-" && len(parse) == 1 {
|
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)
|
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