github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/hclext/decode_test.go (about)

     1  package hclext
     2  
     3  import (
     4  	"reflect"
     5  	"testing"
     6  
     7  	"github.com/google/go-cmp/cmp"
     8  	"github.com/hashicorp/hcl/v2"
     9  	"github.com/hashicorp/hcl/v2/hclsyntax"
    10  )
    11  
    12  func TestDecodeBody(t *testing.T) {
    13  	makeInstantiateType := func(target interface{}) func() interface{} {
    14  		return func() interface{} {
    15  			return reflect.New(reflect.TypeOf(target)).Interface()
    16  		}
    17  	}
    18  	parseExpr := func(src string) hcl.Expression {
    19  		expr, diags := hclsyntax.ParseExpression([]byte(src), "", hcl.InitialPos)
    20  		if diags.HasErrors() {
    21  			panic(diags)
    22  		}
    23  		return expr
    24  	}
    25  	equals := func(other interface{}) func(v interface{}) bool {
    26  		return func(v interface{}) bool {
    27  			return cmp.Equal(v, other)
    28  		}
    29  	}
    30  	noop := func(v interface{}) bool { return true }
    31  
    32  	type withTwoAttributes struct {
    33  		A string `hclext:"a,optional"`
    34  		B string `hclext:"b,optional"`
    35  	}
    36  
    37  	type withNestedBlock struct {
    38  		Plain  string             `hclext:"plain,optional"`
    39  		Nested *withTwoAttributes `hclext:"nested,block"`
    40  	}
    41  
    42  	type withListofNestedBlocks struct {
    43  		Nested []*withTwoAttributes `hclext:"nested,block"`
    44  	}
    45  
    46  	type withListofNestedBlocksNoPointers struct {
    47  		Nested []withTwoAttributes `hclext:"nested,block"`
    48  	}
    49  
    50  	tests := []struct {
    51  		Name      string
    52  		Body      *BodyContent
    53  		Target    func() interface{}
    54  		Check     func(v interface{}) bool
    55  		DiagCount int
    56  	}{
    57  		{
    58  			Name:      "nil body",
    59  			Body:      nil,
    60  			Target:    makeInstantiateType(struct{}{}),
    61  			Check:     equals(struct{}{}),
    62  			DiagCount: 0,
    63  		},
    64  		{
    65  			Name:      "empty body",
    66  			Body:      &BodyContent{},
    67  			Target:    makeInstantiateType(struct{}{}),
    68  			Check:     equals(struct{}{}),
    69  			DiagCount: 0,
    70  		},
    71  		{
    72  			Name: "empty body with optional attr schema (pointer)",
    73  			Body: &BodyContent{},
    74  			Target: makeInstantiateType(struct {
    75  				Name *string `hclext:"name"`
    76  			}{}),
    77  			Check: equals(struct {
    78  				Name *string `hclext:"name"`
    79  			}{}),
    80  			DiagCount: 0,
    81  		},
    82  		{
    83  			Name: "empty body with optional attr schema (label)",
    84  			Body: &BodyContent{},
    85  			Target: makeInstantiateType(struct {
    86  				Name string `hclext:"name,optional"`
    87  			}{}),
    88  			Check: equals(struct {
    89  				Name string `hclext:"name,optional"`
    90  			}{}),
    91  			DiagCount: 0,
    92  		},
    93  		{
    94  			Name: "empty body with required attr schema",
    95  			Body: &BodyContent{},
    96  			Target: makeInstantiateType(struct {
    97  				Name string `hclext:"name"`
    98  			}{}),
    99  			Check: equals(struct {
   100  				Name string `hclext:"name"`
   101  			}{}),
   102  			DiagCount: 1, // attr is required by default
   103  		},
   104  		{
   105  			Name: "required attr",
   106  			Body: &BodyContent{
   107  				Attributes: Attributes{
   108  					"name": &Attribute{Name: "name", Expr: parseExpr(`"Ermintrude"`)},
   109  				},
   110  			},
   111  			Target: makeInstantiateType(struct {
   112  				Name string `hclext:"name"`
   113  			}{}),
   114  			Check: equals(struct {
   115  				Name string `hclext:"name"`
   116  			}{"Ermintrude"}),
   117  			DiagCount: 0,
   118  		},
   119  		{
   120  			Name: "extraneous attr",
   121  			Body: &BodyContent{
   122  				Attributes: Attributes{
   123  					"name": &Attribute{Name: "name", Expr: parseExpr(`"Ermintrude"`)},
   124  					"age":  &Attribute{Name: "age", Expr: parseExpr(`23`)},
   125  				},
   126  			},
   127  			Target: makeInstantiateType(struct {
   128  				Name string `hclext:"name"`
   129  			}{}),
   130  			Check: equals(struct {
   131  				Name string `hclext:"name"`
   132  			}{"Ermintrude"}),
   133  			DiagCount: 0, // extraneous attr is ignored
   134  		},
   135  		{
   136  			Name: "single block with required single block schema",
   137  			Body: &BodyContent{
   138  				Blocks: Blocks{&Block{Type: "noodle"}},
   139  			},
   140  			Target: makeInstantiateType(struct {
   141  				Noodle struct{} `hclext:"noodle,block"`
   142  			}{}),
   143  			Check: equals(struct {
   144  				Noodle struct{} `hclext:"noodle,block"`
   145  			}{}),
   146  			DiagCount: 0,
   147  		},
   148  		{
   149  			Name: "single block with optional single block schema",
   150  			Body: &BodyContent{
   151  				Blocks: Blocks{&Block{Type: "noodle"}},
   152  			},
   153  			Target: makeInstantiateType(struct {
   154  				Noodle *struct{} `hclext:"noodle,block"`
   155  			}{}),
   156  			Check: equals(struct {
   157  				Noodle *struct{} `hclext:"noodle,block"`
   158  			}{Noodle: &struct{}{}}),
   159  			DiagCount: 0,
   160  		},
   161  		{
   162  			Name: "single block with multiple block schema",
   163  			Body: &BodyContent{
   164  				Blocks: Blocks{&Block{Type: "noodle"}},
   165  			},
   166  			Target: makeInstantiateType(struct {
   167  				Noodle []struct{} `hclext:"noodle,block"`
   168  			}{}),
   169  			Check: equals(struct {
   170  				Noodle []struct{} `hclext:"noodle,block"`
   171  			}{Noodle: []struct{}{{}}}),
   172  			DiagCount: 0,
   173  		},
   174  		{
   175  			Name: "multiple blocks with required single block schema",
   176  			Body: &BodyContent{
   177  				Blocks: Blocks{&Block{Type: "noodle"}, &Block{Type: "noodle"}},
   178  			},
   179  			Target: makeInstantiateType(struct {
   180  				Noodle struct{} `hclext:"noodle,block"`
   181  			}{}),
   182  			Check:     noop,
   183  			DiagCount: 1, // duplicate block is not allowed
   184  		},
   185  		{
   186  			Name: "multiple blocks with optional single block schema",
   187  			Body: &BodyContent{
   188  				Blocks: Blocks{&Block{Type: "noodle"}, &Block{Type: "noodle"}},
   189  			},
   190  			Target: makeInstantiateType(struct {
   191  				Noodle *struct{} `hclext:"noodle,block"`
   192  			}{}),
   193  			Check:     noop,
   194  			DiagCount: 1, // duplicate block is not allowed
   195  		},
   196  		{
   197  			Name: "multiple blocks with multiple block schema",
   198  			Body: &BodyContent{
   199  				Blocks: Blocks{&Block{Type: "noodle"}, &Block{Type: "noodle"}},
   200  			},
   201  			Target: makeInstantiateType(struct {
   202  				Noodle []struct{} `hclext:"noodle,block"`
   203  			}{}),
   204  			Check: equals(struct {
   205  				Noodle []struct{} `hclext:"noodle,block"`
   206  			}{Noodle: []struct{}{{}, {}}}),
   207  			DiagCount: 0,
   208  		},
   209  		{
   210  			Name: "empty body with required single block schema",
   211  			Body: &BodyContent{},
   212  			Target: makeInstantiateType(struct {
   213  				Noodle struct{} `hclext:"noodle,block"`
   214  			}{}),
   215  			Check:     noop,
   216  			DiagCount: 1, // block is required by default
   217  		},
   218  		{
   219  			Name: "empty body with optional single block schema",
   220  			Body: &BodyContent{},
   221  			Target: makeInstantiateType(struct {
   222  				Noodle *struct{} `hclext:"noodle,block"`
   223  			}{}),
   224  			Check: equals(struct {
   225  				Noodle *struct{} `hclext:"noodle,block"`
   226  			}{nil}),
   227  			DiagCount: 0,
   228  		},
   229  		{
   230  			Name: "empty body with multiple block schema",
   231  			Body: &BodyContent{},
   232  			Target: makeInstantiateType(struct {
   233  				Noodle []struct{} `hclext:"noodle,block"`
   234  			}{}),
   235  			Check: equals(struct {
   236  				Noodle []struct{} `hclext:"noodle,block"`
   237  			}{}),
   238  			DiagCount: 0,
   239  		},
   240  		{
   241  			Name: "non-labeled block with labeled block schema",
   242  			Body: &BodyContent{
   243  				Blocks: Blocks{&Block{Type: "noodle"}},
   244  			},
   245  			Target: makeInstantiateType(struct {
   246  				Noodle struct {
   247  					Name string `hclext:"name,label"`
   248  				} `hclext:"noodle,block"`
   249  			}{}),
   250  			Check: equals(struct {
   251  				Noodle struct {
   252  					Name string `hclext:"name,label"`
   253  				} `hclext:"noodle,block"`
   254  			}{}),
   255  			DiagCount: 1, // label is required by default
   256  		},
   257  		{
   258  			Name: "labeled block with labeled block schema",
   259  			Body: &BodyContent{
   260  				Blocks: Blocks{&Block{Type: "noodle", Labels: []string{"foo"}}},
   261  			},
   262  			Target: makeInstantiateType(struct {
   263  				Noodle struct {
   264  					Name string `hclext:"name,label"`
   265  				} `hclext:"noodle,block"`
   266  			}{}),
   267  			Check: equals(struct {
   268  				Noodle struct {
   269  					Name string `hclext:"name,label"`
   270  				} `hclext:"noodle,block"`
   271  			}{Noodle: struct {
   272  				Name string `hclext:"name,label"`
   273  			}{Name: "foo"}}),
   274  			DiagCount: 0,
   275  		},
   276  		{
   277  			Name: "multi-labeled blocks with labeled block schema",
   278  			Body: &BodyContent{
   279  				Blocks: Blocks{&Block{Type: "noodle", Labels: []string{"foo", "bar"}}},
   280  			},
   281  			Target: makeInstantiateType(struct {
   282  				Noodle struct {
   283  					Name string `hclext:"name,label"`
   284  				} `hclext:"noodle,block"`
   285  			}{}),
   286  			Check:     noop,
   287  			DiagCount: 1, // extraneous label is not allowed
   288  		},
   289  		{
   290  			Name: "labeled blocks with multi-labeled block schema",
   291  			Body: &BodyContent{
   292  				Blocks: Blocks{&Block{Type: "noodle", Labels: []string{"foo"}}},
   293  			},
   294  			Target: makeInstantiateType(struct {
   295  				Noodle struct {
   296  					Name string `hclext:"name,label"`
   297  					Type string `hclext:"type,label"`
   298  				} `hclext:"noodle,block"`
   299  			}{}),
   300  			Check:     noop,
   301  			DiagCount: 1, // missing label is not allowed
   302  		},
   303  		{
   304  			Name: "multi-labeled blocks with multi-labeled block schema",
   305  			Body: &BodyContent{
   306  				Blocks: Blocks{&Block{Type: "noodle", Labels: []string{"foo", "bar"}}},
   307  			},
   308  			Target: makeInstantiateType(struct {
   309  				Noodle struct {
   310  					Name string `hclext:"name,label"`
   311  					Type string `hclext:"type,label"`
   312  				} `hclext:"noodle,block"`
   313  			}{}),
   314  			Check: equals(struct {
   315  				Noodle struct {
   316  					Name string `hclext:"name,label"`
   317  					Type string `hclext:"type,label"`
   318  				} `hclext:"noodle,block"`
   319  			}{Noodle: struct {
   320  				Name string `hclext:"name,label"`
   321  				Type string `hclext:"type,label"`
   322  			}{Name: "foo", Type: "bar"}}),
   323  			DiagCount: 0,
   324  		},
   325  		{
   326  			Name: "multiple non-labeled blocks with labeled block schema",
   327  			Body: &BodyContent{
   328  				Blocks: Blocks{
   329  					&Block{Type: "noodle"},
   330  					&Block{Type: "noodle"},
   331  				},
   332  			},
   333  			Target: makeInstantiateType(struct {
   334  				Noodle []struct {
   335  					Name string `hclext:"name,label"`
   336  				} `hclext:"noodle,block"`
   337  			}{}),
   338  			Check: equals(struct {
   339  				Noodle []struct {
   340  					Name string `hclext:"name,label"`
   341  				} `hclext:"noodle,block"`
   342  			}{Noodle: []struct {
   343  				Name string `hclext:"name,label"`
   344  			}{{Name: ""}, {Name: ""}}}),
   345  			DiagCount: 2, // label is required by default
   346  		},
   347  		{
   348  			Name: "multiple single labeled blocks with labeled block schema",
   349  			Body: &BodyContent{
   350  				Blocks: Blocks{
   351  					&Block{Type: "noodle", Labels: []string{"foo"}},
   352  					&Block{Type: "noodle", Labels: []string{"bar"}},
   353  				},
   354  			},
   355  			Target: makeInstantiateType(struct {
   356  				Noodle []struct {
   357  					Name string `hclext:"name,label"`
   358  				} `hclext:"noodle,block"`
   359  			}{}),
   360  			Check: equals(struct {
   361  				Noodle []struct {
   362  					Name string `hclext:"name,label"`
   363  				} `hclext:"noodle,block"`
   364  			}{Noodle: []struct {
   365  				Name string `hclext:"name,label"`
   366  			}{{Name: "foo"}, {Name: "bar"}}}),
   367  			DiagCount: 0,
   368  		},
   369  		{
   370  			Name: "labeled block with label/attr schema",
   371  			Body: &BodyContent{
   372  				Blocks: Blocks{
   373  					&Block{
   374  						Type:   "noodle",
   375  						Labels: []string{"foo"},
   376  						Body: &BodyContent{
   377  							Attributes: Attributes{"type": &Attribute{Name: "type", Expr: parseExpr(`"rice"`)}},
   378  						},
   379  					},
   380  				},
   381  			},
   382  			Target: makeInstantiateType(struct {
   383  				Noodle struct {
   384  					Name string `hclext:"name,label"`
   385  					Type string `hclext:"type"`
   386  				} `hclext:"noodle,block"`
   387  			}{}),
   388  			Check: equals(struct {
   389  				Noodle struct {
   390  					Name string `hclext:"name,label"`
   391  					Type string `hclext:"type"`
   392  				} `hclext:"noodle,block"`
   393  			}{Noodle: struct {
   394  				Name string `hclext:"name,label"`
   395  				Type string `hclext:"type"`
   396  			}{Name: "foo", Type: "rice"}}),
   397  			DiagCount: 0,
   398  		},
   399  		{
   400  			Name: "nested block",
   401  			Body: &BodyContent{
   402  				Blocks: Blocks{
   403  					&Block{
   404  						Type:   "noodle",
   405  						Labels: []string{"foo"},
   406  						Body: &BodyContent{
   407  							Attributes: Attributes{"type": &Attribute{Name: "type", Expr: parseExpr(`"rice"`)}},
   408  							Blocks: Blocks{
   409  								&Block{
   410  									Type:   "bread",
   411  									Labels: []string{"bar"},
   412  									Body: &BodyContent{
   413  										Attributes: Attributes{"baked": &Attribute{Name: "baked", Expr: parseExpr(`true`)}},
   414  									},
   415  								},
   416  							},
   417  						},
   418  					},
   419  				},
   420  			},
   421  			Target: makeInstantiateType(struct {
   422  				Noodle struct {
   423  					Name  string `hclext:"name,label"`
   424  					Type  string `hclext:"type"`
   425  					Bread struct {
   426  						Name  string `hclext:"name,label"`
   427  						Baked bool   `hclext:"baked"`
   428  					} `hclext:"bread,block"`
   429  				} `hclext:"noodle,block"`
   430  			}{}),
   431  			Check: equals(struct {
   432  				Noodle struct {
   433  					Name  string `hclext:"name,label"`
   434  					Type  string `hclext:"type"`
   435  					Bread struct {
   436  						Name  string `hclext:"name,label"`
   437  						Baked bool   `hclext:"baked"`
   438  					} `hclext:"bread,block"`
   439  				} `hclext:"noodle,block"`
   440  			}{Noodle: struct {
   441  				Name  string `hclext:"name,label"`
   442  				Type  string `hclext:"type"`
   443  				Bread struct {
   444  					Name  string `hclext:"name,label"`
   445  					Baked bool   `hclext:"baked"`
   446  				} `hclext:"bread,block"`
   447  			}{
   448  				Name: "foo",
   449  				Type: "rice",
   450  				Bread: struct {
   451  					Name  string `hclext:"name,label"`
   452  					Baked bool   `hclext:"baked"`
   453  				}{
   454  					Name:  "bar",
   455  					Baked: true,
   456  				},
   457  			}}),
   458  			DiagCount: 0,
   459  		},
   460  		{
   461  			Name: "retain nested block",
   462  			Body: &BodyContent{
   463  				Attributes: Attributes{"plain": &Attribute{Name: "plain", Expr: parseExpr(`"foo"`)}},
   464  			},
   465  			Target: func() interface{} {
   466  				return &withNestedBlock{
   467  					Plain: "bar",
   468  					Nested: &withTwoAttributes{
   469  						A: "bar",
   470  					},
   471  				}
   472  			},
   473  			Check: equals(withNestedBlock{
   474  				Plain: "foo",
   475  				Nested: &withTwoAttributes{
   476  					A: "bar",
   477  				},
   478  			}),
   479  			DiagCount: 0,
   480  		},
   481  		{
   482  			Name: "retain attrs in nested block",
   483  			Body: &BodyContent{
   484  				Blocks: Blocks{
   485  					&Block{
   486  						Type: "nested",
   487  						Body: &BodyContent{
   488  							Attributes: Attributes{"a": &Attribute{Name: "a", Expr: parseExpr(`"foo"`)}},
   489  						},
   490  					},
   491  				},
   492  			},
   493  			Target: func() interface{} {
   494  				return &withNestedBlock{
   495  					Nested: &withTwoAttributes{
   496  						B: "bar",
   497  					},
   498  				}
   499  			},
   500  			Check: equals(withNestedBlock{
   501  				Nested: &withTwoAttributes{
   502  					A: "foo",
   503  					B: "bar",
   504  				},
   505  			}),
   506  			DiagCount: 0,
   507  		},
   508  		{
   509  			Name: "retain attrs in multiple nested blocks",
   510  			Body: &BodyContent{
   511  				Blocks: Blocks{
   512  					&Block{
   513  						Type: "nested",
   514  						Body: &BodyContent{
   515  							Attributes: Attributes{"a": &Attribute{Name: "a", Expr: parseExpr(`"foo"`)}},
   516  						},
   517  					},
   518  				},
   519  			},
   520  			Target: func() interface{} {
   521  				return &withListofNestedBlocks{
   522  					Nested: []*withTwoAttributes{
   523  						{B: "bar"},
   524  					},
   525  				}
   526  			},
   527  			Check: equals(withListofNestedBlocks{
   528  				Nested: []*withTwoAttributes{
   529  					{A: "foo", B: "bar"},
   530  				},
   531  			}),
   532  			DiagCount: 0,
   533  		},
   534  		{
   535  			Name: "remove additional elements from the list while decoding nested blocks",
   536  			Body: &BodyContent{
   537  				Blocks: Blocks{
   538  					&Block{
   539  						Type: "nested",
   540  						Body: &BodyContent{
   541  							Attributes: Attributes{"a": &Attribute{Name: "a", Expr: parseExpr(`"foo"`)}},
   542  						},
   543  					},
   544  				},
   545  			},
   546  			Target: func() interface{} {
   547  				return &withListofNestedBlocks{
   548  					Nested: []*withTwoAttributes{
   549  						{B: "bar"},
   550  						{B: "bar"},
   551  					},
   552  				}
   553  			},
   554  			Check: equals(withListofNestedBlocks{
   555  				Nested: []*withTwoAttributes{
   556  					{A: "foo", B: "bar"},
   557  				},
   558  			}),
   559  			DiagCount: 0,
   560  		},
   561  		{
   562  			Name: "remove additional elements from the list while decoding nested blocks even if target are not pointer slices",
   563  			Body: &BodyContent{
   564  				Blocks: Blocks{
   565  					&Block{
   566  						Type: "nested",
   567  						Body: &BodyContent{
   568  							Attributes: Attributes{"b": &Attribute{Name: "b", Expr: parseExpr(`"bar"`)}},
   569  						},
   570  					},
   571  					&Block{
   572  						Type: "nested",
   573  						Body: &BodyContent{
   574  							Attributes: Attributes{"b": &Attribute{Name: "b", Expr: parseExpr(`"baz"`)}},
   575  						},
   576  					},
   577  				},
   578  			},
   579  			Target: func() interface{} {
   580  				return &withListofNestedBlocksNoPointers{
   581  					Nested: []withTwoAttributes{
   582  						{B: "foo"},
   583  					},
   584  				}
   585  			},
   586  			Check: equals(withListofNestedBlocksNoPointers{
   587  				Nested: []withTwoAttributes{
   588  					{B: "bar"},
   589  					{B: "baz"},
   590  				},
   591  			}),
   592  			DiagCount: 0,
   593  		},
   594  	}
   595  
   596  	for _, test := range tests {
   597  		t.Run(test.Name, func(t *testing.T) {
   598  			targetVal := reflect.ValueOf(test.Target())
   599  
   600  			diags := DecodeBody(test.Body, nil, targetVal.Interface())
   601  			if len(diags) != test.DiagCount {
   602  				t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
   603  				for _, diag := range diags {
   604  					t.Logf(" - %s", diag.Error())
   605  				}
   606  			}
   607  			got := targetVal.Elem().Interface()
   608  			if !test.Check(got) {
   609  				t.Errorf("wrong result\ndiff:  %#v", got)
   610  			}
   611  		})
   612  	}
   613  }