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 }