diff --git a/marshal.go b/marshal.go index cd9128c..500e00a 100644 --- a/marshal.go +++ b/marshal.go @@ -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 { diff --git a/marshal_test.go b/marshal_test.go index 00cbbf3..09b42b4 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -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() + } +}