1package gohcl
2
3import (
4	"encoding/json"
5	"fmt"
6	"reflect"
7	"testing"
8
9	"github.com/davecgh/go-spew/spew"
10	"github.com/hashicorp/hcl2/hcl"
11	hclJSON "github.com/hashicorp/hcl2/hcl/json"
12	"github.com/zclconf/go-cty/cty"
13)
14
15func TestDecodeBody(t *testing.T) {
16	deepEquals := func(other interface{}) func(v interface{}) bool {
17		return func(v interface{}) bool {
18			return reflect.DeepEqual(v, other)
19		}
20	}
21
22	type withNameExpression struct {
23		Name hcl.Expression `hcl:"name"`
24	}
25
26	tests := []struct {
27		Body      map[string]interface{}
28		Target    interface{}
29		Check     func(v interface{}) bool
30		DiagCount int
31	}{
32		{
33			map[string]interface{}{},
34			struct{}{},
35			deepEquals(struct{}{}),
36			0,
37		},
38		{
39			map[string]interface{}{},
40			struct {
41				Name string `hcl:"name"`
42			}{},
43			deepEquals(struct {
44				Name string `hcl:"name"`
45			}{}),
46			1, // name is required
47		},
48		{
49			map[string]interface{}{},
50			struct {
51				Name *string `hcl:"name"`
52			}{},
53			deepEquals(struct {
54				Name *string `hcl:"name"`
55			}{}),
56			0,
57		}, // name nil
58		{
59			map[string]interface{}{},
60			struct {
61				Name string `hcl:"name,optional"`
62			}{},
63			deepEquals(struct {
64				Name string `hcl:"name,optional"`
65			}{}),
66			0,
67		}, // name optional
68		{
69			map[string]interface{}{},
70			withNameExpression{},
71			func(v interface{}) bool {
72				if v == nil {
73					return false
74				}
75
76				wne, valid := v.(withNameExpression)
77				if !valid {
78					return false
79				}
80
81				if wne.Name == nil {
82					return false
83				}
84
85				nameVal, _ := wne.Name.Value(nil)
86				if !nameVal.IsNull() {
87					return false
88				}
89
90				return true
91			},
92			0,
93		},
94		{
95			map[string]interface{}{
96				"name": "Ermintrude",
97			},
98			withNameExpression{},
99			func(v interface{}) bool {
100				if v == nil {
101					return false
102				}
103
104				wne, valid := v.(withNameExpression)
105				if !valid {
106					return false
107				}
108
109				if wne.Name == nil {
110					return false
111				}
112
113				nameVal, _ := wne.Name.Value(nil)
114				if !nameVal.Equals(cty.StringVal("Ermintrude")).True() {
115					return false
116				}
117
118				return true
119			},
120			0,
121		},
122		{
123			map[string]interface{}{
124				"name": "Ermintrude",
125			},
126			struct {
127				Name string `hcl:"name"`
128			}{},
129			deepEquals(struct {
130				Name string `hcl:"name"`
131			}{"Ermintrude"}),
132			0,
133		},
134		{
135			map[string]interface{}{
136				"name": "Ermintrude",
137				"age":  23,
138			},
139			struct {
140				Name string `hcl:"name"`
141			}{},
142			deepEquals(struct {
143				Name string `hcl:"name"`
144			}{"Ermintrude"}),
145			1, // Extraneous "age" property
146		},
147		{
148			map[string]interface{}{
149				"name": "Ermintrude",
150				"age":  50,
151			},
152			struct {
153				Name  string         `hcl:"name"`
154				Attrs hcl.Attributes `hcl:",remain"`
155			}{},
156			func(gotI interface{}) bool {
157				got := gotI.(struct {
158					Name  string         `hcl:"name"`
159					Attrs hcl.Attributes `hcl:",remain"`
160				})
161				return got.Name == "Ermintrude" && len(got.Attrs) == 1 && got.Attrs["age"] != nil
162			},
163			0,
164		},
165		{
166			map[string]interface{}{
167				"name": "Ermintrude",
168				"age":  50,
169			},
170			struct {
171				Name   string   `hcl:"name"`
172				Remain hcl.Body `hcl:",remain"`
173			}{},
174			func(gotI interface{}) bool {
175				got := gotI.(struct {
176					Name   string   `hcl:"name"`
177					Remain hcl.Body `hcl:",remain"`
178				})
179
180				attrs, _ := got.Remain.JustAttributes()
181
182				return got.Name == "Ermintrude" && len(attrs) == 1 && attrs["age"] != nil
183			},
184			0,
185		},
186		{
187			map[string]interface{}{
188				"name":   "Ermintrude",
189				"living": true,
190			},
191			struct {
192				Name   string               `hcl:"name"`
193				Remain map[string]cty.Value `hcl:",remain"`
194			}{},
195			deepEquals(struct {
196				Name   string               `hcl:"name"`
197				Remain map[string]cty.Value `hcl:",remain"`
198			}{
199				Name: "Ermintrude",
200				Remain: map[string]cty.Value{
201					"living": cty.True,
202				},
203			}),
204			0,
205		},
206		{
207			map[string]interface{}{
208				"noodle": map[string]interface{}{},
209			},
210			struct {
211				Noodle struct{} `hcl:"noodle,block"`
212			}{},
213			func(gotI interface{}) bool {
214				// Generating no diagnostics is good enough for this one.
215				return true
216			},
217			0,
218		},
219		{
220			map[string]interface{}{
221				"noodle": []map[string]interface{}{{}},
222			},
223			struct {
224				Noodle struct{} `hcl:"noodle,block"`
225			}{},
226			func(gotI interface{}) bool {
227				// Generating no diagnostics is good enough for this one.
228				return true
229			},
230			0,
231		},
232		{
233			map[string]interface{}{
234				"noodle": []map[string]interface{}{{}, {}},
235			},
236			struct {
237				Noodle struct{} `hcl:"noodle,block"`
238			}{},
239			func(gotI interface{}) bool {
240				// Generating one diagnostic is good enough for this one.
241				return true
242			},
243			1,
244		},
245		{
246			map[string]interface{}{},
247			struct {
248				Noodle struct{} `hcl:"noodle,block"`
249			}{},
250			func(gotI interface{}) bool {
251				// Generating one diagnostic is good enough for this one.
252				return true
253			},
254			1,
255		},
256		{
257			map[string]interface{}{
258				"noodle": []map[string]interface{}{},
259			},
260			struct {
261				Noodle struct{} `hcl:"noodle,block"`
262			}{},
263			func(gotI interface{}) bool {
264				// Generating one diagnostic is good enough for this one.
265				return true
266			},
267			1,
268		},
269		{
270			map[string]interface{}{
271				"noodle": map[string]interface{}{},
272			},
273			struct {
274				Noodle *struct{} `hcl:"noodle,block"`
275			}{},
276			func(gotI interface{}) bool {
277				return gotI.(struct {
278					Noodle *struct{} `hcl:"noodle,block"`
279				}).Noodle != nil
280			},
281			0,
282		},
283		{
284			map[string]interface{}{
285				"noodle": []map[string]interface{}{{}},
286			},
287			struct {
288				Noodle *struct{} `hcl:"noodle,block"`
289			}{},
290			func(gotI interface{}) bool {
291				return gotI.(struct {
292					Noodle *struct{} `hcl:"noodle,block"`
293				}).Noodle != nil
294			},
295			0,
296		},
297		{
298			map[string]interface{}{
299				"noodle": []map[string]interface{}{},
300			},
301			struct {
302				Noodle *struct{} `hcl:"noodle,block"`
303			}{},
304			func(gotI interface{}) bool {
305				return gotI.(struct {
306					Noodle *struct{} `hcl:"noodle,block"`
307				}).Noodle == nil
308			},
309			0,
310		},
311		{
312			map[string]interface{}{
313				"noodle": []map[string]interface{}{{}, {}},
314			},
315			struct {
316				Noodle *struct{} `hcl:"noodle,block"`
317			}{},
318			func(gotI interface{}) bool {
319				// Generating one diagnostic is good enough for this one.
320				return true
321			},
322			1,
323		},
324		{
325			map[string]interface{}{
326				"noodle": []map[string]interface{}{},
327			},
328			struct {
329				Noodle []struct{} `hcl:"noodle,block"`
330			}{},
331			func(gotI interface{}) bool {
332				noodle := gotI.(struct {
333					Noodle []struct{} `hcl:"noodle,block"`
334				}).Noodle
335				return len(noodle) == 0
336			},
337			0,
338		},
339		{
340			map[string]interface{}{
341				"noodle": []map[string]interface{}{{}},
342			},
343			struct {
344				Noodle []struct{} `hcl:"noodle,block"`
345			}{},
346			func(gotI interface{}) bool {
347				noodle := gotI.(struct {
348					Noodle []struct{} `hcl:"noodle,block"`
349				}).Noodle
350				return len(noodle) == 1
351			},
352			0,
353		},
354		{
355			map[string]interface{}{
356				"noodle": []map[string]interface{}{{}, {}},
357			},
358			struct {
359				Noodle []struct{} `hcl:"noodle,block"`
360			}{},
361			func(gotI interface{}) bool {
362				noodle := gotI.(struct {
363					Noodle []struct{} `hcl:"noodle,block"`
364				}).Noodle
365				return len(noodle) == 2
366			},
367			0,
368		},
369		{
370			map[string]interface{}{
371				"noodle": map[string]interface{}{},
372			},
373			struct {
374				Noodle struct {
375					Name string `hcl:"name,label"`
376				} `hcl:"noodle,block"`
377			}{},
378			func(gotI interface{}) bool {
379				// Generating two diagnostics is good enough for this one.
380				// (one for the missing noodle block and the other for
381				// the JSON serialization detecting the missing level of
382				// heirarchy for the label.)
383				return true
384			},
385			2,
386		},
387		{
388			map[string]interface{}{
389				"noodle": map[string]interface{}{
390					"foo_foo": map[string]interface{}{},
391				},
392			},
393			struct {
394				Noodle struct {
395					Name string `hcl:"name,label"`
396				} `hcl:"noodle,block"`
397			}{},
398			func(gotI interface{}) bool {
399				noodle := gotI.(struct {
400					Noodle struct {
401						Name string `hcl:"name,label"`
402					} `hcl:"noodle,block"`
403				}).Noodle
404				return noodle.Name == "foo_foo"
405			},
406			0,
407		},
408		{
409			map[string]interface{}{
410				"noodle": map[string]interface{}{
411					"foo_foo": map[string]interface{}{},
412					"bar_baz": map[string]interface{}{},
413				},
414			},
415			struct {
416				Noodle struct {
417					Name string `hcl:"name,label"`
418				} `hcl:"noodle,block"`
419			}{},
420			func(gotI interface{}) bool {
421				// One diagnostic is enough for this one.
422				return true
423			},
424			1,
425		},
426		{
427			map[string]interface{}{
428				"noodle": map[string]interface{}{
429					"foo_foo": map[string]interface{}{},
430					"bar_baz": map[string]interface{}{},
431				},
432			},
433			struct {
434				Noodles []struct {
435					Name string `hcl:"name,label"`
436				} `hcl:"noodle,block"`
437			}{},
438			func(gotI interface{}) bool {
439				noodles := gotI.(struct {
440					Noodles []struct {
441						Name string `hcl:"name,label"`
442					} `hcl:"noodle,block"`
443				}).Noodles
444				return len(noodles) == 2 && (noodles[0].Name == "foo_foo" || noodles[0].Name == "bar_baz") && (noodles[1].Name == "foo_foo" || noodles[1].Name == "bar_baz") && noodles[0].Name != noodles[1].Name
445			},
446			0,
447		},
448		{
449			map[string]interface{}{
450				"noodle": map[string]interface{}{
451					"foo_foo": map[string]interface{}{
452						"type": "rice",
453					},
454				},
455			},
456			struct {
457				Noodle struct {
458					Name string `hcl:"name,label"`
459					Type string `hcl:"type"`
460				} `hcl:"noodle,block"`
461			}{},
462			func(gotI interface{}) bool {
463				noodle := gotI.(struct {
464					Noodle struct {
465						Name string `hcl:"name,label"`
466						Type string `hcl:"type"`
467					} `hcl:"noodle,block"`
468				}).Noodle
469				return noodle.Name == "foo_foo" && noodle.Type == "rice"
470			},
471			0,
472		},
473
474		{
475			map[string]interface{}{
476				"name": "Ermintrude",
477				"age":  34,
478			},
479			map[string]string(nil),
480			deepEquals(map[string]string{
481				"name": "Ermintrude",
482				"age":  "34",
483			}),
484			0,
485		},
486		{
487			map[string]interface{}{
488				"name": "Ermintrude",
489				"age":  89,
490			},
491			map[string]*hcl.Attribute(nil),
492			func(gotI interface{}) bool {
493				got := gotI.(map[string]*hcl.Attribute)
494				return len(got) == 2 && got["name"] != nil && got["age"] != nil
495			},
496			0,
497		},
498		{
499			map[string]interface{}{
500				"name": "Ermintrude",
501				"age":  13,
502			},
503			map[string]hcl.Expression(nil),
504			func(gotI interface{}) bool {
505				got := gotI.(map[string]hcl.Expression)
506				return len(got) == 2 && got["name"] != nil && got["age"] != nil
507			},
508			0,
509		},
510		{
511			map[string]interface{}{
512				"name":   "Ermintrude",
513				"living": true,
514			},
515			map[string]cty.Value(nil),
516			deepEquals(map[string]cty.Value{
517				"name":   cty.StringVal("Ermintrude"),
518				"living": cty.True,
519			}),
520			0,
521		},
522	}
523
524	for i, test := range tests {
525		// For convenience here we're going to use the JSON parser
526		// to process the given body.
527		buf, err := json.Marshal(test.Body)
528		if err != nil {
529			t.Fatalf("error JSON-encoding body for test %d: %s", i, err)
530		}
531
532		t.Run(string(buf), func(t *testing.T) {
533			file, diags := hclJSON.Parse(buf, "test.json")
534			if len(diags) != 0 {
535				t.Fatalf("diagnostics while parsing: %s", diags.Error())
536			}
537
538			targetVal := reflect.New(reflect.TypeOf(test.Target))
539
540			diags = DecodeBody(file.Body, nil, targetVal.Interface())
541			if len(diags) != test.DiagCount {
542				t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
543				for _, diag := range diags {
544					t.Logf(" - %s", diag.Error())
545				}
546			}
547			got := targetVal.Elem().Interface()
548			if !test.Check(got) {
549				t.Errorf("wrong result\ngot:  %s", spew.Sdump(got))
550			}
551		})
552	}
553
554}
555
556func TestDecodeExpression(t *testing.T) {
557	tests := []struct {
558		Value     cty.Value
559		Target    interface{}
560		Want      interface{}
561		DiagCount int
562	}{
563		{
564			cty.StringVal("hello"),
565			"",
566			"hello",
567			0,
568		},
569		{
570			cty.StringVal("hello"),
571			cty.NilVal,
572			cty.StringVal("hello"),
573			0,
574		},
575		{
576			cty.NumberIntVal(2),
577			"",
578			"2",
579			0,
580		},
581		{
582			cty.StringVal("true"),
583			false,
584			true,
585			0,
586		},
587		{
588			cty.NullVal(cty.String),
589			"",
590			"",
591			1, // null value is not allowed
592		},
593		{
594			cty.UnknownVal(cty.String),
595			"",
596			"",
597			1, // value must be known
598		},
599		{
600			cty.ListVal([]cty.Value{cty.True}),
601			false,
602			false,
603			1, // bool required
604		},
605	}
606
607	for i, test := range tests {
608		t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
609			expr := &fixedExpression{test.Value}
610
611			targetVal := reflect.New(reflect.TypeOf(test.Target))
612
613			diags := DecodeExpression(expr, nil, targetVal.Interface())
614			if len(diags) != test.DiagCount {
615				t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
616				for _, diag := range diags {
617					t.Logf(" - %s", diag.Error())
618				}
619			}
620			got := targetVal.Elem().Interface()
621			if !reflect.DeepEqual(got, test.Want) {
622				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
623			}
624		})
625	}
626}
627
628type fixedExpression struct {
629	val cty.Value
630}
631
632func (e *fixedExpression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
633	return e.val, nil
634}
635
636func (e *fixedExpression) Range() (r hcl.Range) {
637	return
638}
639func (e *fixedExpression) StartRange() (r hcl.Range) {
640	return
641}
642
643func (e *fixedExpression) Variables() []hcl.Traversal {
644	return nil
645}
646