1// Copyright 2014 Unknwon
2//
3// Licensed under the Apache License, Version 2.0 (the "License"): you may
4// not use this file except in compliance with the License. You may obtain
5// a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations
13// under the License.
14
15package ini_test
16
17import (
18	"bytes"
19	"fmt"
20	"strings"
21	"testing"
22	"time"
23
24	. "github.com/smartystreets/goconvey/convey"
25	"gopkg.in/ini.v1"
26)
27
28type testNested struct {
29	Cities      []string `delim:"|"`
30	Visits      []time.Time
31	Years       []int
32	Numbers     []int64
33	Ages        []uint
34	Populations []uint64
35	Coordinates []float64
36	Flags       []bool
37	Note        string
38	Unused      int `ini:"-"`
39}
40
41type TestEmbeded struct {
42	GPA float64
43}
44
45type testStruct struct {
46	Name           string `ini:"NAME"`
47	Age            int
48	Male           bool
49	Money          float64
50	Born           time.Time
51	Time           time.Duration `ini:"Duration"`
52	Others         testNested
53	OthersPtr      *testNested
54	NilPtr         *testNested
55	*TestEmbeded   `ini:"grade"`
56	Unused         int `ini:"-"`
57	Unsigned       uint
58	Omitted        bool     `ini:"omitthis,omitempty"`
59	Shadows        []string `ini:",,allowshadow"`
60	ShadowInts     []int    `ini:"Shadows,,allowshadow"`
61	BoolPtr        *bool
62	BoolPtrNil     *bool
63	FloatPtr       *float64
64	FloatPtrNil    *float64
65	IntPtr         *int
66	IntPtrNil      *int
67	UintPtr        *uint
68	UintPtrNil     *uint
69	StringPtr      *string
70	StringPtrNil   *string
71	TimePtr        *time.Time
72	TimePtrNil     *time.Time
73	DurationPtr    *time.Duration
74	DurationPtrNil *time.Duration
75}
76
77const _CONF_DATA_STRUCT = `
78NAME = Unknwon
79Age = 21
80Male = true
81Money = 1.25
82Born = 1993-10-07T20:17:05Z
83Duration = 2h45m
84Unsigned = 3
85omitthis = true
86Shadows = 1, 2
87Shadows = 3, 4
88BoolPtr = false
89FloatPtr = 0
90IntPtr = 0
91UintPtr = 0
92StringPtr = ""
93TimePtr = 0001-01-01T00:00:00Z
94DurationPtr = 0s
95
96[Others]
97Cities = HangZhou|Boston
98Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z
99Years = 1993,1994
100Numbers = 10010,10086
101Ages = 18,19
102Populations = 12345678,98765432
103Coordinates = 192.168,10.11
104Flags       = true,false
105Note = Hello world!
106
107[OthersPtr]
108Cities = HangZhou|Boston
109Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z
110Years = 1993,1994
111Numbers = 10010,10086
112Ages = 18,19
113Populations = 12345678,98765432
114Coordinates = 192.168,10.11
115Flags       = true,false
116Note = Hello world!
117
118[grade]
119GPA = 2.8
120
121[foo.bar]
122Here = there
123When = then
124`
125
126type unsupport struct {
127	Byte byte
128}
129
130type unsupport2 struct {
131	Others struct {
132		Cities byte
133	}
134}
135
136type Unsupport3 struct {
137	Cities byte
138}
139
140type unsupport4 struct {
141	*Unsupport3 `ini:"Others"`
142}
143
144type defaultValue struct {
145	Name     string
146	Age      int
147	Male     bool
148	Optional *bool
149	Money    float64
150	Born     time.Time
151	Cities   []string
152}
153
154type fooBar struct {
155	Here, When string
156}
157
158const _INVALID_DATA_CONF_STRUCT = `
159Name =
160Age = age
161Male = 123
162Money = money
163Born = nil
164Cities =
165`
166
167func Test_MapToStruct(t *testing.T) {
168	Convey("Map to struct", t, func() {
169		Convey("Map file to struct", func() {
170			ts := new(testStruct)
171			So(ini.MapTo(ts, []byte(_CONF_DATA_STRUCT)), ShouldBeNil)
172
173			So(ts.Name, ShouldEqual, "Unknwon")
174			So(ts.Age, ShouldEqual, 21)
175			So(ts.Male, ShouldBeTrue)
176			So(ts.Money, ShouldEqual, 1.25)
177			So(ts.Unsigned, ShouldEqual, 3)
178
179			t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
180			So(err, ShouldBeNil)
181			So(ts.Born.String(), ShouldEqual, t.String())
182
183			dur, err := time.ParseDuration("2h45m")
184			So(err, ShouldBeNil)
185			So(ts.Time.Seconds(), ShouldEqual, dur.Seconds())
186
187			So(strings.Join(ts.Others.Cities, ","), ShouldEqual, "HangZhou,Boston")
188			So(ts.Others.Visits[0].String(), ShouldEqual, t.String())
189			So(fmt.Sprint(ts.Others.Years), ShouldEqual, "[1993 1994]")
190			So(fmt.Sprint(ts.Others.Numbers), ShouldEqual, "[10010 10086]")
191			So(fmt.Sprint(ts.Others.Ages), ShouldEqual, "[18 19]")
192			So(fmt.Sprint(ts.Others.Populations), ShouldEqual, "[12345678 98765432]")
193			So(fmt.Sprint(ts.Others.Coordinates), ShouldEqual, "[192.168 10.11]")
194			So(fmt.Sprint(ts.Others.Flags), ShouldEqual, "[true false]")
195			So(ts.Others.Note, ShouldEqual, "Hello world!")
196			So(ts.TestEmbeded.GPA, ShouldEqual, 2.8)
197
198			So(strings.Join(ts.OthersPtr.Cities, ","), ShouldEqual, "HangZhou,Boston")
199			So(ts.OthersPtr.Visits[0].String(), ShouldEqual, t.String())
200			So(fmt.Sprint(ts.OthersPtr.Years), ShouldEqual, "[1993 1994]")
201			So(fmt.Sprint(ts.OthersPtr.Numbers), ShouldEqual, "[10010 10086]")
202			So(fmt.Sprint(ts.OthersPtr.Ages), ShouldEqual, "[18 19]")
203			So(fmt.Sprint(ts.OthersPtr.Populations), ShouldEqual, "[12345678 98765432]")
204			So(fmt.Sprint(ts.OthersPtr.Coordinates), ShouldEqual, "[192.168 10.11]")
205			So(fmt.Sprint(ts.OthersPtr.Flags), ShouldEqual, "[true false]")
206			So(ts.OthersPtr.Note, ShouldEqual, "Hello world!")
207
208			So(ts.NilPtr, ShouldBeNil)
209
210			So(*ts.BoolPtr, ShouldEqual, false)
211			So(ts.BoolPtrNil, ShouldEqual, nil)
212			So(*ts.FloatPtr, ShouldEqual, 0)
213			So(ts.FloatPtrNil, ShouldEqual, nil)
214			So(*ts.IntPtr, ShouldEqual, 0)
215			So(ts.IntPtrNil, ShouldEqual, nil)
216			So(*ts.UintPtr, ShouldEqual, 0)
217			So(ts.UintPtrNil, ShouldEqual, nil)
218			So(*ts.StringPtr, ShouldEqual, "")
219			So(ts.StringPtrNil, ShouldEqual, nil)
220			So(*ts.TimePtr, ShouldNotEqual, nil)
221			So(ts.TimePtrNil, ShouldEqual, nil)
222			So(*ts.DurationPtr, ShouldEqual, 0)
223			So(ts.DurationPtrNil, ShouldEqual, nil)
224
225		})
226
227		Convey("Map section to struct", func() {
228			foobar := new(fooBar)
229			f, err := ini.Load([]byte(_CONF_DATA_STRUCT))
230			So(err, ShouldBeNil)
231
232			So(f.Section("foo.bar").MapTo(foobar), ShouldBeNil)
233			So(foobar.Here, ShouldEqual, "there")
234			So(foobar.When, ShouldEqual, "then")
235		})
236
237		Convey("Map to non-pointer struct", func() {
238			f, err := ini.Load([]byte(_CONF_DATA_STRUCT))
239			So(err, ShouldBeNil)
240			So(f, ShouldNotBeNil)
241
242			So(f.MapTo(testStruct{}), ShouldNotBeNil)
243		})
244
245		Convey("Map to unsupported type", func() {
246			f, err := ini.Load([]byte(_CONF_DATA_STRUCT))
247			So(err, ShouldBeNil)
248			So(f, ShouldNotBeNil)
249
250			f.NameMapper = func(raw string) string {
251				if raw == "Byte" {
252					return "NAME"
253				}
254				return raw
255			}
256			So(f.MapTo(&unsupport{}), ShouldNotBeNil)
257			So(f.MapTo(&unsupport2{}), ShouldNotBeNil)
258			So(f.MapTo(&unsupport4{}), ShouldNotBeNil)
259		})
260
261		Convey("Map to omitempty field", func() {
262			ts := new(testStruct)
263			So(ini.MapTo(ts, []byte(_CONF_DATA_STRUCT)), ShouldBeNil)
264
265			So(ts.Omitted, ShouldEqual, true)
266		})
267
268		Convey("Map with shadows", func() {
269			f, err := ini.LoadSources(ini.LoadOptions{AllowShadows: true}, []byte(_CONF_DATA_STRUCT))
270			So(err, ShouldBeNil)
271			ts := new(testStruct)
272			So(f.MapTo(ts), ShouldBeNil)
273
274			So(strings.Join(ts.Shadows, " "), ShouldEqual, "1 2 3 4")
275			So(fmt.Sprintf("%v", ts.ShadowInts), ShouldEqual, "[1 2 3 4]")
276		})
277
278		Convey("Map from invalid data source", func() {
279			So(ini.MapTo(&testStruct{}, "hi"), ShouldNotBeNil)
280		})
281
282		Convey("Map to wrong types and gain default values", func() {
283			f, err := ini.Load([]byte(_INVALID_DATA_CONF_STRUCT))
284			So(err, ShouldBeNil)
285
286			t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
287			So(err, ShouldBeNil)
288			dv := &defaultValue{"Joe", 10, true, nil, 1.25, t, []string{"HangZhou", "Boston"}}
289			So(f.MapTo(dv), ShouldBeNil)
290			So(dv.Name, ShouldEqual, "Joe")
291			So(dv.Age, ShouldEqual, 10)
292			So(dv.Male, ShouldBeTrue)
293			So(dv.Money, ShouldEqual, 1.25)
294			So(dv.Born.String(), ShouldEqual, t.String())
295			So(strings.Join(dv.Cities, ","), ShouldEqual, "HangZhou,Boston")
296		})
297	})
298
299	Convey("Map to struct in strict mode", t, func() {
300		f, err := ini.Load([]byte(`
301name=bruce
302age=a30`))
303		So(err, ShouldBeNil)
304
305		type Strict struct {
306			Name string `ini:"name"`
307			Age  int    `ini:"age"`
308		}
309		s := new(Strict)
310
311		So(f.Section("").StrictMapTo(s), ShouldNotBeNil)
312	})
313
314	Convey("Map slice in strict mode", t, func() {
315		f, err := ini.Load([]byte(`
316names=alice, bruce`))
317		So(err, ShouldBeNil)
318
319		type Strict struct {
320			Names []string `ini:"names"`
321		}
322		s := new(Strict)
323
324		So(f.Section("").StrictMapTo(s), ShouldBeNil)
325		So(fmt.Sprint(s.Names), ShouldEqual, "[alice bruce]")
326	})
327}
328
329func Test_ReflectFromStruct(t *testing.T) {
330	Convey("Reflect from struct", t, func() {
331		type Embeded struct {
332			Dates       []time.Time `delim:"|" comment:"Time data"`
333			Places      []string
334			Years       []int
335			Numbers     []int64
336			Ages        []uint
337			Populations []uint64
338			Coordinates []float64
339			Flags       []bool
340			None        []int
341		}
342		type Author struct {
343			Name      string `ini:"NAME"`
344			Male      bool
345			Optional  *bool
346			Age       int `comment:"Author's age"`
347			Height    uint
348			GPA       float64
349			Date      time.Time
350			NeverMind string `ini:"-"`
351			*Embeded  `ini:"infos" comment:"Embeded section"`
352		}
353
354		t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
355		So(err, ShouldBeNil)
356		a := &Author{"Unknwon", true, nil, 21, 100, 2.8, t, "",
357			&Embeded{
358				[]time.Time{t, t},
359				[]string{"HangZhou", "Boston"},
360				[]int{1993, 1994},
361				[]int64{10010, 10086},
362				[]uint{18, 19},
363				[]uint64{12345678, 98765432},
364				[]float64{192.168, 10.11},
365				[]bool{true, false},
366				[]int{},
367			}}
368		cfg := ini.Empty()
369		So(ini.ReflectFrom(cfg, a), ShouldBeNil)
370
371		var buf bytes.Buffer
372		_, err = cfg.WriteTo(&buf)
373		So(err, ShouldBeNil)
374		So(buf.String(), ShouldEqual, `NAME     = Unknwon
375Male     = true
376Optional =
377; Author's age
378Age      = 21
379Height   = 100
380GPA      = 2.8
381Date     = 1993-10-07T20:17:05Z
382
383; Embeded section
384[infos]
385; Time data
386Dates       = 1993-10-07T20:17:05Z|1993-10-07T20:17:05Z
387Places      = HangZhou,Boston
388Years       = 1993,1994
389Numbers     = 10010,10086
390Ages        = 18,19
391Populations = 12345678,98765432
392Coordinates = 192.168,10.11
393Flags       = true,false
394None        =
395
396`)
397
398		Convey("Reflect from non-point struct", func() {
399			So(ini.ReflectFrom(cfg, Author{}), ShouldNotBeNil)
400		})
401
402		Convey("Reflect from struct with omitempty", func() {
403			cfg := ini.Empty()
404			type SpecialStruct struct {
405				FirstName  string    `ini:"first_name"`
406				LastName   string    `ini:"last_name"`
407				JustOmitMe string    `ini:"omitempty"`
408				LastLogin  time.Time `ini:"last_login,omitempty"`
409				LastLogin2 time.Time `ini:",omitempty"`
410				NotEmpty   int       `ini:"omitempty"`
411			}
412
413			So(ini.ReflectFrom(cfg, &SpecialStruct{FirstName: "John", LastName: "Doe", NotEmpty: 9}), ShouldBeNil)
414
415			var buf bytes.Buffer
416			_, err = cfg.WriteTo(&buf)
417			So(buf.String(), ShouldEqual, `first_name = John
418last_name  = Doe
419omitempty  = 9
420
421`)
422		})
423	})
424}
425
426// Inspired by https://github.com/go-ini/ini/issues/196
427func TestMapToAndReflectFromStructWithShadows(t *testing.T) {
428	Convey("Map to struct and then reflect with shadows should generate original config content", t, func() {
429		type include struct {
430			Paths []string `ini:"path,omitempty,allowshadow"`
431		}
432
433		cfg, err := ini.LoadSources(ini.LoadOptions{
434			AllowShadows: true,
435		}, []byte(`
436[include]
437path = /tmp/gpm-profiles/test5.profile
438path = /tmp/gpm-profiles/test1.profile`))
439		So(err, ShouldBeNil)
440
441		sec := cfg.Section("include")
442		inc := new(include)
443		err = sec.MapTo(inc)
444		So(err, ShouldBeNil)
445
446		err = sec.ReflectFrom(inc)
447		So(err, ShouldBeNil)
448
449		var buf bytes.Buffer
450		_, err = cfg.WriteTo(&buf)
451		So(err, ShouldBeNil)
452		So(buf.String(), ShouldEqual, `[include]
453path = /tmp/gpm-profiles/test5.profile
454path = /tmp/gpm-profiles/test1.profile
455
456`)
457	})
458}
459
460type testMapper struct {
461	PackageName string
462}
463
464func Test_NameGetter(t *testing.T) {
465	Convey("Test name mappers", t, func() {
466		So(ini.MapToWithMapper(&testMapper{}, ini.TitleUnderscore, []byte("packag_name=ini")), ShouldBeNil)
467
468		cfg, err := ini.Load([]byte("PACKAGE_NAME=ini"))
469		So(err, ShouldBeNil)
470		So(cfg, ShouldNotBeNil)
471
472		cfg.NameMapper = ini.AllCapsUnderscore
473		tg := new(testMapper)
474		So(cfg.MapTo(tg), ShouldBeNil)
475		So(tg.PackageName, ShouldEqual, "ini")
476	})
477}
478
479type testDurationStruct struct {
480	Duration time.Duration `ini:"Duration"`
481}
482
483func Test_Duration(t *testing.T) {
484	Convey("Duration less than 16m50s", t, func() {
485		ds := new(testDurationStruct)
486		So(ini.MapTo(ds, []byte("Duration=16m49s")), ShouldBeNil)
487
488		dur, err := time.ParseDuration("16m49s")
489		So(err, ShouldBeNil)
490		So(ds.Duration.Seconds(), ShouldEqual, dur.Seconds())
491	})
492}
493