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
16
17import (
18	"bytes"
19	"fmt"
20	"strings"
21	"testing"
22	"time"
23
24	"github.com/stretchr/testify/assert"
25	"github.com/stretchr/testify/require"
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	OldVersionTime time.Duration
53	Others         testNested
54	OthersPtr      *testNested
55	NilPtr         *testNested
56	*TestEmbeded   `ini:"grade"`
57	Unused         int `ini:"-"`
58	Unsigned       uint
59	Omitted        bool     `ini:"omitthis,omitempty"`
60	Shadows        []string `ini:",allowshadow"`
61	ShadowInts     []int    `ini:"Shadows,allowshadow"`
62	BoolPtr        *bool
63	BoolPtrNil     *bool
64	FloatPtr       *float64
65	FloatPtrNil    *float64
66	IntPtr         *int
67	IntPtrNil      *int
68	UintPtr        *uint
69	UintPtrNil     *uint
70	StringPtr      *string
71	StringPtrNil   *string
72	TimePtr        *time.Time
73	TimePtrNil     *time.Time
74	DurationPtr    *time.Duration
75	DurationPtrNil *time.Duration
76}
77
78type testInterface struct {
79	Address    string
80	ListenPort int
81	PrivateKey string
82}
83
84type testPeer struct {
85	PublicKey    string
86	PresharedKey string
87	AllowedIPs   []string `delim:","`
88}
89
90type testNonUniqueSectionsStruct struct {
91	Interface testInterface
92	Peer      []testPeer `ini:",nonunique"`
93}
94
95type BaseStruct struct {
96	Base bool
97}
98
99type testExtend struct {
100	BaseStruct `ini:",extends"`
101	Extend     bool
102}
103
104const confDataStruct = `
105NAME = Unknwon
106Age = 21
107Male = true
108Money = 1.25
109Born = 1993-10-07T20:17:05Z
110Duration = 2h45m
111OldVersionTime = 30
112Unsigned = 3
113omitthis = true
114Shadows = 1, 2
115Shadows = 3, 4
116BoolPtr = false
117FloatPtr = 0
118IntPtr = 0
119UintPtr = 0
120StringPtr = ""
121TimePtr = 0001-01-01T00:00:00Z
122DurationPtr = 0s
123
124[Others]
125Cities = HangZhou|Boston
126Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z
127Years = 1993,1994
128Numbers = 10010,10086
129Ages = 18,19
130Populations = 12345678,98765432
131Coordinates = 192.168,10.11
132Flags       = true,false
133Note = Hello world!
134
135[OthersPtr]
136Cities = HangZhou|Boston
137Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z
138Years = 1993,1994
139Numbers = 10010,10086
140Ages = 18,19
141Populations = 12345678,98765432
142Coordinates = 192.168,10.11
143Flags       = true,false
144Note = Hello world!
145
146[grade]
147GPA = 2.8
148
149[foo.bar]
150Here = there
151When = then
152
153[extended]
154Base = true
155Extend = true
156`
157
158const confNonUniqueSectionDataStruct = `[Interface]
159Address    = 10.2.0.1/24
160ListenPort = 34777
161PrivateKey = privServerKey
162
163[Peer]
164PublicKey    = pubClientKey
165PresharedKey = psKey
166AllowedIPs   = 10.2.0.2/32,fd00:2::2/128
167
168[Peer]
169PublicKey    = pubClientKey2
170PresharedKey = psKey2
171AllowedIPs   = 10.2.0.3/32,fd00:2::3/128
172
173`
174
175type unsupport struct {
176	Byte byte
177}
178
179type unsupport2 struct {
180	Others struct {
181		Cities byte
182	}
183}
184
185type Unsupport3 struct {
186	Cities byte
187}
188
189type unsupport4 struct {
190	*Unsupport3 `ini:"Others"`
191}
192
193type defaultValue struct {
194	Name     string
195	Age      int
196	Male     bool
197	Optional *bool
198	Money    float64
199	Born     time.Time
200	Cities   []string
201}
202
203type fooBar struct {
204	Here, When string
205}
206
207const invalidDataConfStruct = `
208Name =
209Age = age
210Male = 123
211Money = money
212Born = nil
213Cities =
214`
215
216func Test_MapToStruct(t *testing.T) {
217	t.Run("map to struct", func(t *testing.T) {
218		t.Run("map file to struct", func(t *testing.T) {
219			ts := new(testStruct)
220			assert.NoError(t, MapTo(ts, []byte(confDataStruct)))
221
222			assert.Equal(t, "Unknwon", ts.Name)
223			assert.Equal(t, 21, ts.Age)
224			assert.True(t, ts.Male)
225			assert.Equal(t, 1.25, ts.Money)
226			assert.Equal(t, uint(3), ts.Unsigned)
227
228			ti, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
229			require.NoError(t, err)
230			assert.Equal(t, ti.String(), ts.Born.String())
231
232			dur, err := time.ParseDuration("2h45m")
233			require.NoError(t, err)
234			assert.Equal(t, dur.Seconds(), ts.Time.Seconds())
235
236			assert.Equal(t, 30*time.Second, ts.OldVersionTime*time.Second)
237
238			assert.Equal(t, "HangZhou,Boston", strings.Join(ts.Others.Cities, ","))
239			assert.Equal(t, ti.String(), ts.Others.Visits[0].String())
240			assert.Equal(t, "[1993 1994]", fmt.Sprint(ts.Others.Years))
241			assert.Equal(t, "[10010 10086]", fmt.Sprint(ts.Others.Numbers))
242			assert.Equal(t, "[18 19]", fmt.Sprint(ts.Others.Ages))
243			assert.Equal(t, "[12345678 98765432]", fmt.Sprint(ts.Others.Populations))
244			assert.Equal(t, "[192.168 10.11]", fmt.Sprint(ts.Others.Coordinates))
245			assert.Equal(t, "[true false]", fmt.Sprint(ts.Others.Flags))
246			assert.Equal(t, "Hello world!", ts.Others.Note)
247			assert.Equal(t, 2.8, ts.TestEmbeded.GPA)
248
249			assert.Equal(t, "HangZhou,Boston", strings.Join(ts.OthersPtr.Cities, ","))
250			assert.Equal(t, ti.String(), ts.OthersPtr.Visits[0].String())
251			assert.Equal(t, "[1993 1994]", fmt.Sprint(ts.OthersPtr.Years))
252			assert.Equal(t, "[10010 10086]", fmt.Sprint(ts.OthersPtr.Numbers))
253			assert.Equal(t, "[18 19]", fmt.Sprint(ts.OthersPtr.Ages))
254			assert.Equal(t, "[12345678 98765432]", fmt.Sprint(ts.OthersPtr.Populations))
255			assert.Equal(t, "[192.168 10.11]", fmt.Sprint(ts.OthersPtr.Coordinates))
256			assert.Equal(t, "[true false]", fmt.Sprint(ts.OthersPtr.Flags))
257			assert.Equal(t, "Hello world!", ts.OthersPtr.Note)
258
259			assert.Nil(t, ts.NilPtr)
260
261			assert.Equal(t, false, *ts.BoolPtr)
262			assert.Nil(t, ts.BoolPtrNil)
263			assert.Equal(t, float64(0), *ts.FloatPtr)
264			assert.Nil(t, ts.FloatPtrNil)
265			assert.Equal(t, 0, *ts.IntPtr)
266			assert.Nil(t, ts.IntPtrNil)
267			assert.Equal(t, uint(0), *ts.UintPtr)
268			assert.Nil(t, ts.UintPtrNil)
269			assert.Equal(t, "", *ts.StringPtr)
270			assert.Nil(t, ts.StringPtrNil)
271			assert.NotNil(t, *ts.TimePtr)
272			assert.Nil(t, ts.TimePtrNil)
273			assert.Equal(t, time.Duration(0), *ts.DurationPtr)
274			assert.Nil(t, ts.DurationPtrNil)
275		})
276
277		t.Run("map section to struct", func(t *testing.T) {
278			foobar := new(fooBar)
279			f, err := Load([]byte(confDataStruct))
280			require.NoError(t, err)
281
282			assert.NoError(t, f.Section("foo.bar").MapTo(foobar))
283			assert.Equal(t, "there", foobar.Here)
284			assert.Equal(t, "then", foobar.When)
285		})
286
287		t.Run("map to non-pointer struct", func(t *testing.T) {
288			f, err := Load([]byte(confDataStruct))
289			require.NoError(t, err)
290			require.NotNil(t, f)
291
292			assert.Error(t, f.MapTo(testStruct{}))
293		})
294
295		t.Run("map to unsupported type", func(t *testing.T) {
296			f, err := Load([]byte(confDataStruct))
297			require.NoError(t, err)
298			require.NotNil(t, f)
299
300			f.NameMapper = func(raw string) string {
301				if raw == "Byte" {
302					return "NAME"
303				}
304				return raw
305			}
306			assert.Error(t, f.MapTo(&unsupport{}))
307			assert.Error(t, f.MapTo(&unsupport2{}))
308			assert.Error(t, f.MapTo(&unsupport4{}))
309		})
310
311		t.Run("map to omitempty field", func(t *testing.T) {
312			ts := new(testStruct)
313			assert.NoError(t, MapTo(ts, []byte(confDataStruct)))
314
315			assert.Equal(t, true, ts.Omitted)
316		})
317
318		t.Run("map with shadows", func(t *testing.T) {
319			f, err := LoadSources(LoadOptions{AllowShadows: true}, []byte(confDataStruct))
320			require.NoError(t, err)
321			ts := new(testStruct)
322			assert.NoError(t, f.MapTo(ts))
323
324			assert.Equal(t, "1 2 3 4", strings.Join(ts.Shadows, " "))
325			assert.Equal(t, "[1 2 3 4]", fmt.Sprintf("%v", ts.ShadowInts))
326		})
327
328		t.Run("map from invalid data source", func(t *testing.T) {
329			assert.Error(t, MapTo(&testStruct{}, "hi"))
330		})
331
332		t.Run("map to wrong types and gain default values", func(t *testing.T) {
333			f, err := Load([]byte(invalidDataConfStruct))
334			require.NoError(t, err)
335
336			ti, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
337			require.NoError(t, err)
338			dv := &defaultValue{"Joe", 10, true, nil, 1.25, ti, []string{"HangZhou", "Boston"}}
339			assert.NoError(t, f.MapTo(dv))
340			assert.Equal(t, "Joe", dv.Name)
341			assert.Equal(t, 10, dv.Age)
342			assert.True(t, dv.Male)
343			assert.Equal(t, 1.25, dv.Money)
344			assert.Equal(t, ti.String(), dv.Born.String())
345			assert.Equal(t, "HangZhou,Boston", strings.Join(dv.Cities, ","))
346		})
347
348		t.Run("map to extended base", func(t *testing.T) {
349			f, err := Load([]byte(confDataStruct))
350			require.NoError(t, err)
351			require.NotNil(t, f)
352			te := testExtend{}
353			assert.NoError(t, f.Section("extended").MapTo(&te))
354			assert.True(t, te.Base)
355			assert.True(t, te.Extend)
356		})
357	})
358
359	t.Run("map to struct in strict mode", func(t *testing.T) {
360		f, err := Load([]byte(`
361name=bruce
362age=a30`))
363		require.NoError(t, err)
364
365		type Strict struct {
366			Name string `ini:"name"`
367			Age  int    `ini:"age"`
368		}
369		s := new(Strict)
370
371		assert.Error(t, f.Section("").StrictMapTo(s))
372	})
373
374	t.Run("map slice in strict mode", func(t *testing.T) {
375		f, err := Load([]byte(`
376names=alice, bruce`))
377		require.NoError(t, err)
378
379		type Strict struct {
380			Names []string `ini:"names"`
381		}
382		s := new(Strict)
383
384		assert.NoError(t, f.Section("").StrictMapTo(s))
385		assert.Equal(t, "[alice bruce]", fmt.Sprint(s.Names))
386	})
387}
388
389func Test_MapToStructNonUniqueSections(t *testing.T) {
390	t.Run("map to struct non unique", func(t *testing.T) {
391		t.Run("map file to struct non unique", func(t *testing.T) {
392			f, err := LoadSources(LoadOptions{AllowNonUniqueSections: true}, []byte(confNonUniqueSectionDataStruct))
393			require.NoError(t, err)
394			ts := new(testNonUniqueSectionsStruct)
395
396			assert.NoError(t, f.MapTo(ts))
397
398			assert.Equal(t, "10.2.0.1/24", ts.Interface.Address)
399			assert.Equal(t, 34777, ts.Interface.ListenPort)
400			assert.Equal(t, "privServerKey", ts.Interface.PrivateKey)
401
402			assert.Equal(t, "pubClientKey", ts.Peer[0].PublicKey)
403			assert.Equal(t, "psKey", ts.Peer[0].PresharedKey)
404			assert.Equal(t, "10.2.0.2/32", ts.Peer[0].AllowedIPs[0])
405			assert.Equal(t, "fd00:2::2/128", ts.Peer[0].AllowedIPs[1])
406
407			assert.Equal(t, "pubClientKey2", ts.Peer[1].PublicKey)
408			assert.Equal(t, "psKey2", ts.Peer[1].PresharedKey)
409			assert.Equal(t, "10.2.0.3/32", ts.Peer[1].AllowedIPs[0])
410			assert.Equal(t, "fd00:2::3/128", ts.Peer[1].AllowedIPs[1])
411		})
412
413		t.Run("map non unique section to struct", func(t *testing.T) {
414			newPeer := new(testPeer)
415			newPeerSlice := make([]testPeer, 0)
416
417			f, err := LoadSources(LoadOptions{AllowNonUniqueSections: true}, []byte(confNonUniqueSectionDataStruct))
418			require.NoError(t, err)
419
420			// try only first one
421			assert.NoError(t, f.Section("Peer").MapTo(newPeer))
422			assert.Equal(t, "pubClientKey", newPeer.PublicKey)
423			assert.Equal(t, "psKey", newPeer.PresharedKey)
424			assert.Equal(t, "10.2.0.2/32", newPeer.AllowedIPs[0])
425			assert.Equal(t, "fd00:2::2/128", newPeer.AllowedIPs[1])
426
427			// try all
428			assert.NoError(t, f.Section("Peer").MapTo(&newPeerSlice))
429			assert.Equal(t, "pubClientKey", newPeerSlice[0].PublicKey)
430			assert.Equal(t, "psKey", newPeerSlice[0].PresharedKey)
431			assert.Equal(t, "10.2.0.2/32", newPeerSlice[0].AllowedIPs[0])
432			assert.Equal(t, "fd00:2::2/128", newPeerSlice[0].AllowedIPs[1])
433
434			assert.Equal(t, "pubClientKey2", newPeerSlice[1].PublicKey)
435			assert.Equal(t, "psKey2", newPeerSlice[1].PresharedKey)
436			assert.Equal(t, "10.2.0.3/32", newPeerSlice[1].AllowedIPs[0])
437			assert.Equal(t, "fd00:2::3/128", newPeerSlice[1].AllowedIPs[1])
438		})
439
440		t.Run("map non unique sections with subsections to struct", func(t *testing.T) {
441			iniFile, err := LoadSources(LoadOptions{AllowNonUniqueSections: true}, strings.NewReader(`
442[Section]
443FieldInSubSection = 1
444FieldInSubSection2 = 2
445FieldInSection = 3
446
447[Section]
448FieldInSubSection = 4
449FieldInSubSection2 = 5
450FieldInSection = 6
451`))
452			require.NoError(t, err)
453
454			type SubSection struct {
455				FieldInSubSection string `ini:"FieldInSubSection"`
456			}
457			type SubSection2 struct {
458				FieldInSubSection2 string `ini:"FieldInSubSection2"`
459			}
460
461			type Section struct {
462				SubSection     `ini:"Section"`
463				SubSection2    `ini:"Section"`
464				FieldInSection string `ini:"FieldInSection"`
465			}
466
467			type File struct {
468				Sections []Section `ini:"Section,nonunique"`
469			}
470
471			f := new(File)
472			err = iniFile.MapTo(f)
473			require.NoError(t, err)
474
475			assert.Equal(t, "1", f.Sections[0].FieldInSubSection)
476			assert.Equal(t, "2", f.Sections[0].FieldInSubSection2)
477			assert.Equal(t, "3", f.Sections[0].FieldInSection)
478
479			assert.Equal(t, "4", f.Sections[1].FieldInSubSection)
480			assert.Equal(t, "5", f.Sections[1].FieldInSubSection2)
481			assert.Equal(t, "6", f.Sections[1].FieldInSection)
482		})
483	})
484}
485
486func Test_ReflectFromStruct(t *testing.T) {
487	t.Run("reflect from struct", func(t *testing.T) {
488		type Embeded struct {
489			Dates       []time.Time `delim:"|" comment:"Time data"`
490			Places      []string
491			Years       []int
492			Numbers     []int64
493			Ages        []uint
494			Populations []uint64
495			Coordinates []float64
496			Flags       []bool
497			None        []int
498		}
499		type Author struct {
500			Name      string `ini:"NAME"`
501			Male      bool
502			Optional  *bool
503			Age       int `comment:"Author's age"`
504			Height    uint
505			GPA       float64
506			Date      time.Time
507			NeverMind string `ini:"-"`
508			ignored   string
509			*Embeded  `ini:"infos" comment:"Embeded section"`
510		}
511
512		ti, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
513		require.NoError(t, err)
514		a := &Author{"Unknwon", true, nil, 21, 100, 2.8, ti, "", "ignored",
515			&Embeded{
516				[]time.Time{ti, ti},
517				[]string{"HangZhou", "Boston"},
518				[]int{1993, 1994},
519				[]int64{10010, 10086},
520				[]uint{18, 19},
521				[]uint64{12345678, 98765432},
522				[]float64{192.168, 10.11},
523				[]bool{true, false},
524				[]int{},
525			}}
526		cfg := Empty()
527		assert.NoError(t, ReflectFrom(cfg, a))
528
529		var buf bytes.Buffer
530		_, err = cfg.WriteTo(&buf)
531		require.NoError(t, err)
532		assert.Equal(t, `NAME     = Unknwon
533Male     = true
534Optional =
535; Author's age
536Age      = 21
537Height   = 100
538GPA      = 2.8
539Date     = 1993-10-07T20:17:05Z
540
541; Embeded section
542[infos]
543; Time data
544Dates       = 1993-10-07T20:17:05Z|1993-10-07T20:17:05Z
545Places      = HangZhou,Boston
546Years       = 1993,1994
547Numbers     = 10010,10086
548Ages        = 18,19
549Populations = 12345678,98765432
550Coordinates = 192.168,10.11
551Flags       = true,false
552None        =
553
554`,
555			buf.String(),
556		)
557
558		t.Run("reflect from non-point struct", func(t *testing.T) {
559			assert.Error(t, ReflectFrom(cfg, Author{}))
560		})
561
562		t.Run("reflect from struct with omitempty", func(t *testing.T) {
563			cfg := Empty()
564			type SpecialStruct struct {
565				FirstName  string    `ini:"first_name"`
566				LastName   string    `ini:"last_name,omitempty"`
567				JustOmitMe string    `ini:"omitempty"`
568				LastLogin  time.Time `ini:"last_login,omitempty"`
569				LastLogin2 time.Time `ini:",omitempty"`
570				NotEmpty   int       `ini:"omitempty"`
571				Number     int64     `ini:",omitempty"`
572				Ages       uint      `ini:",omitempty"`
573				Population uint64    `ini:",omitempty"`
574				Coordinate float64   `ini:",omitempty"`
575				Flag       bool      `ini:",omitempty"`
576				Note       *string   `ini:",omitempty"`
577			}
578			special := &SpecialStruct{
579				FirstName: "John",
580				LastName:  "Doe",
581				NotEmpty:  9,
582			}
583
584			assert.NoError(t, ReflectFrom(cfg, special))
585
586			var buf bytes.Buffer
587			_, err = cfg.WriteTo(&buf)
588			assert.Equal(t, `first_name = John
589last_name  = Doe
590omitempty  = 9
591
592`,
593				buf.String(),
594			)
595		})
596
597		t.Run("reflect from struct with non-anonymous structure pointer", func(t *testing.T) {
598			cfg := Empty()
599			type Rpc struct {
600				Enable  bool   `ini:"enable"`
601				Type    string `ini:"type"`
602				Address string `ini:"addr"`
603				Name    string `ini:"name"`
604			}
605			type Cfg struct {
606				Rpc *Rpc `ini:"rpc"`
607			}
608
609			config := &Cfg{
610				Rpc: &Rpc{
611					Enable:  true,
612					Type:    "type",
613					Address: "address",
614					Name:    "name",
615				},
616			}
617			assert.NoError(t, cfg.ReflectFrom(config))
618
619			var buf bytes.Buffer
620			_, err = cfg.WriteTo(&buf)
621			assert.Equal(t, `[rpc]
622enable = true
623type   = type
624addr   = address
625name   = name
626
627`,
628				buf.String(),
629			)
630		})
631	})
632}
633
634func Test_ReflectFromStructNonUniqueSections(t *testing.T) {
635	t.Run("reflect from struct with non unique sections", func(t *testing.T) {
636		nonUnique := &testNonUniqueSectionsStruct{
637			Interface: testInterface{
638				Address:    "10.2.0.1/24",
639				ListenPort: 34777,
640				PrivateKey: "privServerKey",
641			},
642			Peer: []testPeer{
643				{
644					PublicKey:    "pubClientKey",
645					PresharedKey: "psKey",
646					AllowedIPs:   []string{"10.2.0.2/32,fd00:2::2/128"},
647				},
648				{
649					PublicKey:    "pubClientKey2",
650					PresharedKey: "psKey2",
651					AllowedIPs:   []string{"10.2.0.3/32,fd00:2::3/128"},
652				},
653			},
654		}
655
656		cfg := Empty(LoadOptions{
657			AllowNonUniqueSections: true,
658		})
659
660		assert.NoError(t, ReflectFrom(cfg, nonUnique))
661
662		var buf bytes.Buffer
663		_, err := cfg.WriteTo(&buf)
664		require.NoError(t, err)
665		assert.Equal(t, confNonUniqueSectionDataStruct, buf.String())
666
667		// note: using ReflectFrom from should overwrite the existing sections
668		err = cfg.Section("Peer").ReflectFrom([]*testPeer{
669			{
670				PublicKey:    "pubClientKey3",
671				PresharedKey: "psKey3",
672				AllowedIPs:   []string{"10.2.0.4/32,fd00:2::4/128"},
673			},
674			{
675				PublicKey:    "pubClientKey4",
676				PresharedKey: "psKey4",
677				AllowedIPs:   []string{"10.2.0.5/32,fd00:2::5/128"},
678			},
679		})
680
681		require.NoError(t, err)
682
683		buf = bytes.Buffer{}
684		_, err = cfg.WriteTo(&buf)
685		require.NoError(t, err)
686		assert.Equal(t, `[Interface]
687Address    = 10.2.0.1/24
688ListenPort = 34777
689PrivateKey = privServerKey
690
691[Peer]
692PublicKey    = pubClientKey3
693PresharedKey = psKey3
694AllowedIPs   = 10.2.0.4/32,fd00:2::4/128
695
696[Peer]
697PublicKey    = pubClientKey4
698PresharedKey = psKey4
699AllowedIPs   = 10.2.0.5/32,fd00:2::5/128
700
701`,
702			buf.String(),
703		)
704
705		// note: using ReflectFrom from should overwrite the existing sections
706		err = cfg.Section("Peer").ReflectFrom(&testPeer{
707			PublicKey:    "pubClientKey5",
708			PresharedKey: "psKey5",
709			AllowedIPs:   []string{"10.2.0.6/32,fd00:2::6/128"},
710		})
711
712		require.NoError(t, err)
713
714		buf = bytes.Buffer{}
715		_, err = cfg.WriteTo(&buf)
716		require.NoError(t, err)
717		assert.Equal(t, `[Interface]
718Address    = 10.2.0.1/24
719ListenPort = 34777
720PrivateKey = privServerKey
721
722[Peer]
723PublicKey    = pubClientKey5
724PresharedKey = psKey5
725AllowedIPs   = 10.2.0.6/32,fd00:2::6/128
726
727`,
728			buf.String(),
729		)
730	})
731}
732
733// Inspired by https://github.com/go-ini/ini/issues/196
734func TestMapToAndReflectFromStructWithShadows(t *testing.T) {
735	t.Run("map to struct and then reflect with shadows should generate original config content", func(t *testing.T) {
736		type include struct {
737			Paths []string `ini:"path,omitempty,allowshadow"`
738		}
739
740		cfg, err := LoadSources(LoadOptions{
741			AllowShadows: true,
742		}, []byte(`
743[include]
744path = /tmp/gpm-profiles/test5.profile
745path = /tmp/gpm-profiles/test1.profile`))
746		require.NoError(t, err)
747
748		sec := cfg.Section("include")
749		inc := new(include)
750		err = sec.MapTo(inc)
751		require.NoError(t, err)
752
753		err = sec.ReflectFrom(inc)
754		require.NoError(t, err)
755
756		var buf bytes.Buffer
757		_, err = cfg.WriteTo(&buf)
758		require.NoError(t, err)
759		assert.Equal(t, `[include]
760path = /tmp/gpm-profiles/test5.profile
761path = /tmp/gpm-profiles/test1.profile
762
763`,
764			buf.String(),
765		)
766
767		t.Run("reflect from struct with shadows", func(t *testing.T) {
768			cfg := Empty(LoadOptions{
769				AllowShadows: true,
770			})
771			type ShadowStruct struct {
772				StringArray      []string    `ini:"sa,allowshadow"`
773				EmptyStringArrat []string    `ini:"empty,omitempty,allowshadow"`
774				Allowshadow      []string    `ini:"allowshadow,allowshadow"`
775				Dates            []time.Time `ini:",allowshadow"`
776				Places           []string    `ini:",allowshadow"`
777				Years            []int       `ini:",allowshadow"`
778				Numbers          []int64     `ini:",allowshadow"`
779				Ages             []uint      `ini:",allowshadow"`
780				Populations      []uint64    `ini:",allowshadow"`
781				Coordinates      []float64   `ini:",allowshadow"`
782				Flags            []bool      `ini:",allowshadow"`
783				None             []int       `ini:",allowshadow"`
784			}
785
786			shadow := &ShadowStruct{
787				StringArray: []string{"s1", "s2"},
788				Allowshadow: []string{"s3", "s4"},
789				Dates: []time.Time{time.Date(2020, 9, 12, 00, 00, 00, 651387237, time.UTC),
790					time.Date(2020, 9, 12, 00, 00, 00, 651387237, time.UTC)},
791				Places:      []string{"HangZhou", "Boston"},
792				Years:       []int{1993, 1994},
793				Numbers:     []int64{10010, 10086},
794				Ages:        []uint{18, 19},
795				Populations: []uint64{12345678, 98765432},
796				Coordinates: []float64{192.168, 10.11},
797				Flags:       []bool{true, false},
798				None:        []int{},
799			}
800
801			assert.NoError(t, ReflectFrom(cfg, shadow))
802
803			var buf bytes.Buffer
804			_, err := cfg.WriteTo(&buf)
805			require.NoError(t, err)
806			assert.Equal(t, `sa          = s1
807sa          = s2
808allowshadow = s3
809allowshadow = s4
810Dates       = 2020-09-12T00:00:00Z
811Places      = HangZhou
812Places      = Boston
813Years       = 1993
814Years       = 1994
815Numbers     = 10010
816Numbers     = 10086
817Ages        = 18
818Ages        = 19
819Populations = 12345678
820Populations = 98765432
821Coordinates = 192.168
822Coordinates = 10.11
823Flags       = true
824Flags       = false
825None        =
826
827`,
828				buf.String(),
829			)
830		})
831	})
832}
833
834type testMapper struct {
835	PackageName string
836}
837
838func Test_NameGetter(t *testing.T) {
839	t.Run("test name mappers", func(t *testing.T) {
840		assert.NoError(t, MapToWithMapper(&testMapper{}, TitleUnderscore, []byte("packag_name=ini")))
841
842		cfg, err := Load([]byte("PACKAGE_NAME=ini"))
843		require.NoError(t, err)
844		require.NotNil(t, cfg)
845
846		cfg.NameMapper = SnackCase
847		tg := new(testMapper)
848		assert.NoError(t, cfg.MapTo(tg))
849		assert.Equal(t, "ini", tg.PackageName)
850	})
851}
852
853type testDurationStruct struct {
854	Duration time.Duration `ini:"Duration"`
855}
856
857func Test_Duration(t *testing.T) {
858	t.Run("duration less than 16m50s", func(t *testing.T) {
859		ds := new(testDurationStruct)
860		assert.NoError(t, MapTo(ds, []byte("Duration=16m49s")))
861
862		dur, err := time.ParseDuration("16m49s")
863		require.NoError(t, err)
864		assert.Equal(t, dur.Seconds(), ds.Duration.Seconds())
865	})
866}
867
868type Employer struct {
869	Name  string
870	Title string
871}
872
873type Employers []*Employer
874
875func (es Employers) ReflectINIStruct(f *File) error {
876	for _, e := range es {
877		f.Section(e.Name).Key("Title").SetValue(e.Title)
878	}
879	return nil
880}
881
882// Inspired by https://github.com/go-ini/ini/issues/199
883func Test_StructReflector(t *testing.T) {
884	t.Run("reflect with StructReflector interface", func(t *testing.T) {
885		p := &struct {
886			FirstName string
887			Employer  Employers
888		}{
889			FirstName: "Andrew",
890			Employer: []*Employer{
891				{
892					Name:  `Employer "VMware"`,
893					Title: "Staff II Engineer",
894				},
895				{
896					Name:  `Employer "EMC"`,
897					Title: "Consultant Engineer",
898				},
899			},
900		}
901
902		f := Empty()
903		assert.NoError(t, f.ReflectFrom(p))
904
905		var buf bytes.Buffer
906		_, err := f.WriteTo(&buf)
907		require.NoError(t, err)
908
909		assert.Equal(t, `FirstName = Andrew
910
911[Employer "VMware"]
912Title = Staff II Engineer
913
914[Employer "EMC"]
915Title = Consultant Engineer
916
917`,
918			buf.String(),
919		)
920	})
921}
922