| package hcl |
| |
| import ( |
| "io/ioutil" |
| "path/filepath" |
| "reflect" |
| "testing" |
| "time" |
| |
| "google3/third_party/golang/hashicorp/hcl/hcl/ast/ast" |
| "google3/third_party/golang/spew/spew" |
| ) |
| |
| func TestDecode_interface(t *testing.T) { |
| cases := []struct { |
| File string |
| Err bool |
| Out interface{} |
| }{ |
| { |
| "basic.hcl", |
| false, |
| map[string]interface{}{ |
| "foo": "bar", |
| "bar": "${file(\"bing/bong.txt\")}", |
| }, |
| }, |
| { |
| "basic_squish.hcl", |
| false, |
| map[string]interface{}{ |
| "foo": "bar", |
| "bar": "${file(\"bing/bong.txt\")}", |
| "foo-bar": "baz", |
| }, |
| }, |
| { |
| "empty.hcl", |
| false, |
| map[string]interface{}{ |
| "resource": []map[string]interface{}{ |
| map[string]interface{}{ |
| "foo": []map[string]interface{}{ |
| map[string]interface{}{}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| { |
| "tfvars.hcl", |
| false, |
| map[string]interface{}{ |
| "regularvar": "Should work", |
| "map.key1": "Value", |
| "map.key2": "Other value", |
| }, |
| }, |
| { |
| "escape.hcl", |
| false, |
| map[string]interface{}{ |
| "foo": "bar\"baz\\n", |
| "qux": "back\\slash", |
| "bar": "new\nline", |
| "qax": `slash\:colon`, |
| "nested": `${HH\\:mm\\:ss}`, |
| "nestedquotes": `${"\"stringwrappedinquotes\""}`, |
| }, |
| }, |
| { |
| "float.hcl", |
| false, |
| map[string]interface{}{ |
| "a": 1.02, |
| "b": 2, |
| }, |
| }, |
| { |
| "multiline_bad.hcl", |
| true, |
| nil, |
| }, |
| { |
| "multiline_literal.hcl", |
| true, |
| nil, |
| }, |
| { |
| "multiline_literal_with_hil.hcl", |
| false, |
| map[string]interface{}{"multiline_literal_with_hil": "${hello\n world}"}, |
| }, |
| { |
| "multiline_no_marker.hcl", |
| true, |
| nil, |
| }, |
| { |
| "multiline.hcl", |
| false, |
| map[string]interface{}{"foo": "bar\nbaz\n"}, |
| }, |
| { |
| "multiline_indented.hcl", |
| false, |
| map[string]interface{}{"foo": " bar\n baz\n"}, |
| }, |
| { |
| "multiline_no_hanging_indent.hcl", |
| false, |
| map[string]interface{}{"foo": " baz\n bar\n foo\n"}, |
| }, |
| { |
| "multiline_no_eof.hcl", |
| false, |
| map[string]interface{}{"foo": "bar\nbaz\n", "key": "value"}, |
| }, |
| { |
| "multiline.json", |
| false, |
| map[string]interface{}{"foo": "bar\nbaz"}, |
| }, |
| { |
| "null_strings.json", |
| false, |
| map[string]interface{}{ |
| "module": []map[string]interface{}{ |
| map[string]interface{}{ |
| "app": []map[string]interface{}{ |
| map[string]interface{}{"foo": ""}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| { |
| "scientific.json", |
| false, |
| map[string]interface{}{ |
| "a": 1e-10, |
| "b": 1e+10, |
| "c": 1e10, |
| "d": 1.2e-10, |
| "e": 1.2e+10, |
| "f": 1.2e10, |
| }, |
| }, |
| { |
| "scientific.hcl", |
| false, |
| map[string]interface{}{ |
| "a": 1e-10, |
| "b": 1e+10, |
| "c": 1e10, |
| "d": 1.2e-10, |
| "e": 1.2e+10, |
| "f": 1.2e10, |
| }, |
| }, |
| { |
| "terraform_heroku.hcl", |
| false, |
| map[string]interface{}{ |
| "name": "terraform-test-app", |
| "config_vars": []map[string]interface{}{ |
| map[string]interface{}{ |
| "FOO": "bar", |
| }, |
| }, |
| }, |
| }, |
| { |
| "structure_multi.hcl", |
| false, |
| map[string]interface{}{ |
| "foo": []map[string]interface{}{ |
| map[string]interface{}{ |
| "baz": []map[string]interface{}{ |
| map[string]interface{}{"key": 7}, |
| }, |
| }, |
| map[string]interface{}{ |
| "bar": []map[string]interface{}{ |
| map[string]interface{}{"key": 12}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| { |
| "structure_multi.json", |
| false, |
| map[string]interface{}{ |
| "foo": []map[string]interface{}{ |
| map[string]interface{}{ |
| "baz": []map[string]interface{}{ |
| map[string]interface{}{"key": 7}, |
| }, |
| }, |
| map[string]interface{}{ |
| "bar": []map[string]interface{}{ |
| map[string]interface{}{"key": 12}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| { |
| "list_of_lists.hcl", |
| false, |
| map[string]interface{}{ |
| "foo": []interface{}{ |
| []interface{}{"foo"}, |
| []interface{}{"bar"}, |
| }, |
| }, |
| }, |
| { |
| "list_of_maps.hcl", |
| false, |
| map[string]interface{}{ |
| "foo": []interface{}{ |
| map[string]interface{}{"somekey1": "someval1"}, |
| map[string]interface{}{"somekey2": "someval2", "someextrakey": "someextraval"}, |
| }, |
| }, |
| }, |
| { |
| "assign_deep.hcl", |
| false, |
| map[string]interface{}{ |
| "resource": []interface{}{ |
| map[string]interface{}{ |
| "foo": []interface{}{ |
| map[string]interface{}{ |
| "bar": []map[string]interface{}{ |
| map[string]interface{}{}}}}}}}, |
| }, |
| { |
| "structure_list.hcl", |
| false, |
| map[string]interface{}{ |
| "foo": []map[string]interface{}{ |
| map[string]interface{}{ |
| "key": 7, |
| }, |
| map[string]interface{}{ |
| "key": 12, |
| }, |
| }, |
| }, |
| }, |
| { |
| "structure_list.json", |
| false, |
| map[string]interface{}{ |
| "foo": []map[string]interface{}{ |
| map[string]interface{}{ |
| "key": 7, |
| }, |
| map[string]interface{}{ |
| "key": 12, |
| }, |
| }, |
| }, |
| }, |
| { |
| "structure_list_deep.json", |
| false, |
| map[string]interface{}{ |
| "bar": []map[string]interface{}{ |
| map[string]interface{}{ |
| "foo": []map[string]interface{}{ |
| map[string]interface{}{ |
| "name": "terraform_example", |
| "ingress": []map[string]interface{}{ |
| map[string]interface{}{ |
| "from_port": 22, |
| }, |
| map[string]interface{}{ |
| "from_port": 80, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| |
| { |
| "structure_list_empty.json", |
| false, |
| map[string]interface{}{ |
| "foo": []interface{}{}, |
| }, |
| }, |
| |
| { |
| "nested_block_comment.hcl", |
| false, |
| map[string]interface{}{ |
| "bar": "value", |
| }, |
| }, |
| |
| { |
| "unterminated_block_comment.hcl", |
| true, |
| nil, |
| }, |
| |
| { |
| "unterminated_brace.hcl", |
| true, |
| nil, |
| }, |
| |
| { |
| "nested_provider_bad.hcl", |
| true, |
| nil, |
| }, |
| |
| { |
| "object_list.json", |
| false, |
| map[string]interface{}{ |
| "resource": []map[string]interface{}{ |
| map[string]interface{}{ |
| "aws_instance": []map[string]interface{}{ |
| map[string]interface{}{ |
| "db": []map[string]interface{}{ |
| map[string]interface{}{ |
| "vpc": "foo", |
| "provisioner": []map[string]interface{}{ |
| map[string]interface{}{ |
| "file": []map[string]interface{}{ |
| map[string]interface{}{ |
| "source": "foo", |
| "destination": "bar", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| |
| // Terraform GH-8295 sanity test that basic decoding into |
| // interface{} works. |
| { |
| "terraform_variable_invalid.json", |
| false, |
| map[string]interface{}{ |
| "variable": []map[string]interface{}{ |
| map[string]interface{}{ |
| "whatever": "abc123", |
| }, |
| }, |
| }, |
| }, |
| |
| { |
| "interpolate.json", |
| false, |
| map[string]interface{}{ |
| "default": `${replace("europe-west", "-", " ")}`, |
| }, |
| }, |
| |
| { |
| "block_assign.hcl", |
| true, |
| nil, |
| }, |
| |
| { |
| "escape_backslash.hcl", |
| false, |
| map[string]interface{}{ |
| "output": []map[string]interface{}{ |
| map[string]interface{}{ |
| "one": `${replace(var.sub_domain, ".", "\\.")}`, |
| "two": `${replace(var.sub_domain, ".", "\\\\.")}`, |
| "many": `${replace(var.sub_domain, ".", "\\\\\\\\.")}`, |
| }, |
| }, |
| }, |
| }, |
| |
| { |
| "git_crypt.hcl", |
| true, |
| nil, |
| }, |
| |
| { |
| "object_with_bool.hcl", |
| false, |
| map[string]interface{}{ |
| "path": []map[string]interface{}{ |
| map[string]interface{}{ |
| "policy": "write", |
| "permissions": []map[string]interface{}{ |
| map[string]interface{}{ |
| "bool": []interface{}{false}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| } |
| |
| for _, tc := range cases { |
| t.Run(tc.File, func(t *testing.T) { |
| d, err := ioutil.ReadFile(filepath.Join(fixtureDir, tc.File)) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| var out interface{} |
| err = Decode(&out, string(d)) |
| if (err != nil) != tc.Err { |
| t.Fatalf("Input: %s\n\nError: %s", tc.File, err) |
| } |
| |
| if !reflect.DeepEqual(out, tc.Out) { |
| t.Fatalf("Input: %s. Actual, Expected.\n\n%#v\n\n%#v", tc.File, out, tc.Out) |
| } |
| |
| var v interface{} |
| err = Unmarshal(d, &v) |
| if (err != nil) != tc.Err { |
| t.Fatalf("Input: %s\n\nError: %s", tc.File, err) |
| } |
| |
| if !reflect.DeepEqual(v, tc.Out) { |
| t.Fatalf("Input: %s. Actual, Expected.\n\n%#v\n\n%#v", tc.File, out, tc.Out) |
| } |
| }) |
| } |
| } |
| |
| func TestDecode_interfaceInline(t *testing.T) { |
| cases := []struct { |
| Value string |
| Err bool |
| Out interface{} |
| }{ |
| {"t t e{{}}", true, nil}, |
| {"t=0t d {}", true, map[string]interface{}{"t": 0}}, |
| {"v=0E0v d{}", true, map[string]interface{}{"v": float64(0)}}, |
| } |
| |
| for _, tc := range cases { |
| t.Logf("Testing: %q", tc.Value) |
| |
| var out interface{} |
| err := Decode(&out, tc.Value) |
| if (err != nil) != tc.Err { |
| t.Fatalf("Input: %q\n\nError: %s", tc.Value, err) |
| } |
| |
| if !reflect.DeepEqual(out, tc.Out) { |
| t.Fatalf("Input: %q. Actual, Expected.\n\n%#v\n\n%#v", tc.Value, out, tc.Out) |
| } |
| |
| var v interface{} |
| err = Unmarshal([]byte(tc.Value), &v) |
| if (err != nil) != tc.Err { |
| t.Fatalf("Input: %q\n\nError: %s", tc.Value, err) |
| } |
| |
| if !reflect.DeepEqual(v, tc.Out) { |
| t.Fatalf("Input: %q. Actual, Expected.\n\n%#v\n\n%#v", tc.Value, out, tc.Out) |
| } |
| } |
| } |
| |
| func TestDecode_equal(t *testing.T) { |
| cases := []struct { |
| One, Two string |
| }{ |
| { |
| "basic.hcl", |
| "basic.json", |
| }, |
| { |
| "float.hcl", |
| "float.json", |
| }, |
| /* |
| { |
| "structure.hcl", |
| "structure.json", |
| }, |
| */ |
| { |
| "structure.hcl", |
| "structure_flat.json", |
| }, |
| { |
| "terraform_heroku.hcl", |
| "terraform_heroku.json", |
| }, |
| } |
| |
| for _, tc := range cases { |
| p1 := filepath.Join(fixtureDir, tc.One) |
| p2 := filepath.Join(fixtureDir, tc.Two) |
| |
| d1, err := ioutil.ReadFile(p1) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| d2, err := ioutil.ReadFile(p2) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| var i1, i2 interface{} |
| err = Decode(&i1, string(d1)) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| err = Decode(&i2, string(d2)) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| if !reflect.DeepEqual(i1, i2) { |
| t.Fatalf( |
| "%s != %s\n\n%#v\n\n%#v", |
| tc.One, tc.Two, |
| i1, i2) |
| } |
| } |
| } |
| |
| func TestDecode_flatMap(t *testing.T) { |
| var val map[string]map[string]string |
| |
| err := Decode(&val, testReadFile(t, "structure_flatmap.hcl")) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| expected := map[string]map[string]string{ |
| "foo": map[string]string{ |
| "foo": "bar", |
| "key": "7", |
| }, |
| } |
| |
| if !reflect.DeepEqual(val, expected) { |
| t.Fatalf("Actual: %#v\n\nExpected: %#v", val, expected) |
| } |
| } |
| |
| func TestDecode_structure(t *testing.T) { |
| type Embedded interface{} |
| |
| type V struct { |
| Embedded `hcl:"-"` |
| Key int |
| Foo string |
| } |
| |
| var actual V |
| |
| err := Decode(&actual, testReadFile(t, "flat.hcl")) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| expected := V{ |
| Key: 7, |
| Foo: "bar", |
| } |
| |
| if !reflect.DeepEqual(actual, expected) { |
| t.Fatalf("Actual: %#v\n\nExpected: %#v", actual, expected) |
| } |
| } |
| |
| func TestDecode_structurePtr(t *testing.T) { |
| type V struct { |
| Key int |
| Foo string |
| } |
| |
| var actual *V |
| |
| err := Decode(&actual, testReadFile(t, "flat.hcl")) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| expected := &V{ |
| Key: 7, |
| Foo: "bar", |
| } |
| |
| if !reflect.DeepEqual(actual, expected) { |
| t.Fatalf("Actual: %#v\n\nExpected: %#v", actual, expected) |
| } |
| } |
| |
| func TestDecode_structureArray(t *testing.T) { |
| // This test is extracted from a failure in Consul (consul.io), |
| // hence the interesting structure naming. |
| |
| type KeyPolicyType string |
| |
| type KeyPolicy struct { |
| Prefix string `hcl:",key"` |
| Policy KeyPolicyType |
| } |
| |
| type Policy struct { |
| Keys []KeyPolicy `hcl:"key,expand"` |
| } |
| |
| expected := Policy{ |
| Keys: []KeyPolicy{ |
| KeyPolicy{ |
| Prefix: "", |
| Policy: "read", |
| }, |
| KeyPolicy{ |
| Prefix: "foo/", |
| Policy: "write", |
| }, |
| KeyPolicy{ |
| Prefix: "foo/bar/", |
| Policy: "read", |
| }, |
| KeyPolicy{ |
| Prefix: "foo/bar/baz", |
| Policy: "deny", |
| }, |
| }, |
| } |
| |
| files := []string{ |
| "decode_policy.hcl", |
| "decode_policy.json", |
| } |
| |
| for _, f := range files { |
| var actual Policy |
| |
| err := Decode(&actual, testReadFile(t, f)) |
| if err != nil { |
| t.Fatalf("Input: %s\n\nerr: %s", f, err) |
| } |
| |
| if !reflect.DeepEqual(actual, expected) { |
| t.Fatalf("Input: %s\n\nActual: %#v\n\nExpected: %#v", f, actual, expected) |
| } |
| } |
| } |
| |
| func TestDecode_sliceExpand(t *testing.T) { |
| type testInner struct { |
| Name string `hcl:",key"` |
| Key string |
| } |
| |
| type testStruct struct { |
| Services []testInner `hcl:"service,expand"` |
| } |
| |
| expected := testStruct{ |
| Services: []testInner{ |
| testInner{ |
| Name: "my-service-0", |
| Key: "value", |
| }, |
| testInner{ |
| Name: "my-service-1", |
| Key: "value", |
| }, |
| }, |
| } |
| |
| files := []string{ |
| "slice_expand.hcl", |
| } |
| |
| for _, f := range files { |
| t.Logf("Testing: %s", f) |
| |
| var actual testStruct |
| err := Decode(&actual, testReadFile(t, f)) |
| if err != nil { |
| t.Fatalf("Input: %s\n\nerr: %s", f, err) |
| } |
| |
| if !reflect.DeepEqual(actual, expected) { |
| t.Fatalf("Input: %s\n\nActual: %#v\n\nExpected: %#v", f, actual, expected) |
| } |
| } |
| } |
| |
| func TestDecode_structureMap(t *testing.T) { |
| // This test is extracted from a failure in Terraform (terraform.io), |
| // hence the interesting structure naming. |
| |
| type hclVariable struct { |
| Default interface{} |
| Description string |
| Fields []string `hcl:",decodedFields"` |
| } |
| |
| type rawConfig struct { |
| Variable map[string]hclVariable |
| } |
| |
| expected := rawConfig{ |
| Variable: map[string]hclVariable{ |
| "foo": hclVariable{ |
| Default: "bar", |
| Description: "bar", |
| Fields: []string{"Default", "Description"}, |
| }, |
| |
| "amis": hclVariable{ |
| Default: []map[string]interface{}{ |
| map[string]interface{}{ |
| "east": "foo", |
| }, |
| }, |
| Fields: []string{"Default"}, |
| }, |
| }, |
| } |
| |
| files := []string{ |
| "decode_tf_variable.hcl", |
| "decode_tf_variable.json", |
| } |
| |
| for _, f := range files { |
| t.Logf("Testing: %s", f) |
| |
| var actual rawConfig |
| err := Decode(&actual, testReadFile(t, f)) |
| if err != nil { |
| t.Fatalf("Input: %s\n\nerr: %s", f, err) |
| } |
| |
| if !reflect.DeepEqual(actual, expected) { |
| t.Fatalf("Input: %s\n\nActual: %#v\n\nExpected: %#v", f, actual, expected) |
| } |
| } |
| } |
| |
| func TestDecode_structureMapInvalid(t *testing.T) { |
| // Terraform GH-8295 |
| |
| type hclVariable struct { |
| Default interface{} |
| Description string |
| Fields []string `hcl:",decodedFields"` |
| } |
| |
| type rawConfig struct { |
| Variable map[string]*hclVariable |
| } |
| |
| var actual rawConfig |
| err := Decode(&actual, testReadFile(t, "terraform_variable_invalid.json")) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| } |
| |
| func TestDecode_interfaceNonPointer(t *testing.T) { |
| var value interface{} |
| err := Decode(value, testReadFile(t, "basic_int_string.hcl")) |
| if err == nil { |
| t.Fatal("should error") |
| } |
| } |
| |
| func TestDecode_intString(t *testing.T) { |
| var value struct { |
| Count int |
| } |
| |
| err := Decode(&value, testReadFile(t, "basic_int_string.hcl")) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| if value.Count != 3 { |
| t.Fatalf("bad: %#v", value.Count) |
| } |
| } |
| |
| func TestDecode_float32(t *testing.T) { |
| var value struct { |
| A float32 `hcl:"a"` |
| B float32 `hcl:"b"` |
| } |
| |
| err := Decode(&value, testReadFile(t, "float.hcl")) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| if got, want := value.A, float32(1.02); got != want { |
| t.Fatalf("wrong result %#v; want %#v", got, want) |
| } |
| if got, want := value.B, float32(2); got != want { |
| t.Fatalf("wrong result %#v; want %#v", got, want) |
| } |
| } |
| |
| func TestDecode_float64(t *testing.T) { |
| var value struct { |
| A float64 `hcl:"a"` |
| B float64 `hcl:"b"` |
| } |
| |
| err := Decode(&value, testReadFile(t, "float.hcl")) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| if got, want := value.A, float64(1.02); got != want { |
| t.Fatalf("wrong result %#v; want %#v", got, want) |
| } |
| if got, want := value.B, float64(2); got != want { |
| t.Fatalf("wrong result %#v; want %#v", got, want) |
| } |
| } |
| |
| func TestDecode_intStringAliased(t *testing.T) { |
| var value struct { |
| Count time.Duration |
| } |
| |
| err := Decode(&value, testReadFile(t, "basic_int_string.hcl")) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| if value.Count != time.Duration(3) { |
| t.Fatalf("bad: %#v", value.Count) |
| } |
| } |
| |
| func TestDecode_Node(t *testing.T) { |
| // given |
| var value struct { |
| Content ast.Node |
| Nested struct { |
| Content ast.Node |
| } |
| } |
| |
| content := ` |
| content { |
| hello = "world" |
| } |
| ` |
| |
| // when |
| err := Decode(&value, content) |
| |
| // then |
| if err != nil { |
| t.Errorf("unable to decode content, %v", err) |
| return |
| } |
| |
| // verify ast.Node can be decoded later |
| var v map[string]interface{} |
| err = DecodeObject(&v, value.Content) |
| if err != nil { |
| t.Errorf("unable to decode content, %v", err) |
| return |
| } |
| |
| if v["hello"] != "world" { |
| t.Errorf("expected mapping to be returned") |
| } |
| } |
| |
| func TestDecode_NestedNode(t *testing.T) { |
| // given |
| var value struct { |
| Nested struct { |
| Content ast.Node |
| } |
| } |
| |
| content := ` |
| nested "content" { |
| hello = "world" |
| } |
| ` |
| |
| // when |
| err := Decode(&value, content) |
| |
| // then |
| if err != nil { |
| t.Errorf("unable to decode content, %v", err) |
| return |
| } |
| |
| // verify ast.Node can be decoded later |
| var v map[string]interface{} |
| err = DecodeObject(&v, value.Nested.Content) |
| if err != nil { |
| t.Errorf("unable to decode content, %v", err) |
| return |
| } |
| |
| if v["hello"] != "world" { |
| t.Errorf("expected mapping to be returned") |
| } |
| } |
| |
| // https://github.com/hashicorp/hcl/issues/60 |
| func TestDecode_topLevelKeys(t *testing.T) { |
| type Template struct { |
| Source string |
| } |
| |
| templates := struct { |
| Templates []*Template `hcl:"template"` |
| }{} |
| |
| err := Decode(&templates, ` |
| template { |
| source = "blah" |
| } |
| |
| template { |
| source = "blahblah" |
| }`) |
| |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if templates.Templates[0].Source != "blah" { |
| t.Errorf("bad source: %s", templates.Templates[0].Source) |
| } |
| |
| if templates.Templates[1].Source != "blahblah" { |
| t.Errorf("bad source: %s", templates.Templates[1].Source) |
| } |
| } |
| |
| func TestDecode_flattenedJSON(t *testing.T) { |
| // make sure we can also correctly extract a Name key too |
| type V struct { |
| Name string `hcl:",key"` |
| Description string |
| Default map[string]string |
| } |
| type Vars struct { |
| Variable []*V |
| } |
| |
| cases := []struct { |
| JSON string |
| Out interface{} |
| Expected interface{} |
| }{ |
| { // Nested object, no sibling keys |
| JSON: ` |
| { |
| "var_name": { |
| "default": { |
| "key1": "a", |
| "key2": "b" |
| } |
| } |
| } |
| `, |
| Out: &[]*V{}, |
| Expected: &[]*V{ |
| &V{ |
| Name: "var_name", |
| Default: map[string]string{"key1": "a", "key2": "b"}, |
| }, |
| }, |
| }, |
| |
| { // Nested object with a sibling key (this worked previously) |
| JSON: ` |
| { |
| "var_name": { |
| "description": "Described", |
| "default": { |
| "key1": "a", |
| "key2": "b" |
| } |
| } |
| } |
| `, |
| Out: &[]*V{}, |
| Expected: &[]*V{ |
| &V{ |
| Name: "var_name", |
| Description: "Described", |
| Default: map[string]string{"key1": "a", "key2": "b"}, |
| }, |
| }, |
| }, |
| |
| { // Multiple nested objects, one with a sibling key |
| JSON: ` |
| { |
| "variable": { |
| "var_1": { |
| "default": { |
| "key1": "a", |
| "key2": "b" |
| } |
| }, |
| "var_2": { |
| "description": "Described", |
| "default": { |
| "key1": "a", |
| "key2": "b" |
| } |
| } |
| } |
| } |
| `, |
| Out: &Vars{}, |
| Expected: &Vars{ |
| Variable: []*V{ |
| &V{ |
| Name: "var_1", |
| Default: map[string]string{"key1": "a", "key2": "b"}, |
| }, |
| &V{ |
| Name: "var_2", |
| Description: "Described", |
| Default: map[string]string{"key1": "a", "key2": "b"}, |
| }, |
| }, |
| }, |
| }, |
| |
| { // Nested object to maps |
| JSON: ` |
| { |
| "variable": { |
| "var_name": { |
| "description": "Described", |
| "default": { |
| "key1": "a", |
| "key2": "b" |
| } |
| } |
| } |
| } |
| `, |
| Out: &[]map[string]interface{}{}, |
| Expected: &[]map[string]interface{}{ |
| { |
| "variable": []map[string]interface{}{ |
| { |
| "var_name": []map[string]interface{}{ |
| { |
| "description": "Described", |
| "default": []map[string]interface{}{ |
| { |
| "key1": "a", |
| "key2": "b", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| |
| { // Nested object to maps without a sibling key should decode the same as above |
| JSON: ` |
| { |
| "variable": { |
| "var_name": { |
| "default": { |
| "key1": "a", |
| "key2": "b" |
| } |
| } |
| } |
| } |
| `, |
| Out: &[]map[string]interface{}{}, |
| Expected: &[]map[string]interface{}{ |
| { |
| "variable": []map[string]interface{}{ |
| { |
| "var_name": []map[string]interface{}{ |
| { |
| "default": []map[string]interface{}{ |
| { |
| "key1": "a", |
| "key2": "b", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| |
| { // Nested objects, one with a sibling key, and one without |
| JSON: ` |
| { |
| "variable": { |
| "var_1": { |
| "default": { |
| "key1": "a", |
| "key2": "b" |
| } |
| }, |
| "var_2": { |
| "description": "Described", |
| "default": { |
| "key1": "a", |
| "key2": "b" |
| } |
| } |
| } |
| } |
| `, |
| Out: &[]map[string]interface{}{}, |
| Expected: &[]map[string]interface{}{ |
| { |
| "variable": []map[string]interface{}{ |
| { |
| "var_1": []map[string]interface{}{ |
| { |
| "default": []map[string]interface{}{ |
| { |
| "key1": "a", |
| "key2": "b", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| { |
| "variable": []map[string]interface{}{ |
| { |
| "var_2": []map[string]interface{}{ |
| { |
| "description": "Described", |
| "default": []map[string]interface{}{ |
| { |
| "key1": "a", |
| "key2": "b", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| } |
| |
| for i, tc := range cases { |
| err := Decode(tc.Out, tc.JSON) |
| if err != nil { |
| t.Fatalf("[%d] err: %s", i, err) |
| } |
| |
| if !reflect.DeepEqual(tc.Out, tc.Expected) { |
| t.Fatalf("[%d]\ngot: %s\nexpected: %s\n", i, spew.Sdump(tc.Out), spew.Sdump(tc.Expected)) |
| } |
| } |
| } |