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