|
21 | 21 | package fx_test |
22 | 22 |
|
23 | 23 | import ( |
| 24 | + "context" |
24 | 25 | "errors" |
25 | 26 | "strings" |
26 | 27 | "testing" |
@@ -485,4 +486,371 @@ func TestDecorateFailure(t *testing.T) { |
485 | 486 | require.Error(t, err) |
486 | 487 | assert.Contains(t, err.Error(), "missing dependencies") |
487 | 488 | }) |
| 489 | + |
| 490 | + t.Run("slice decorators are blocked for named value groups", func(t *testing.T) { |
| 491 | + // This test verifies the key design decision from dig PR #381: |
| 492 | + // Slice decorators cannot be used with named value groups because |
| 493 | + // they would break the map functionality |
| 494 | + |
| 495 | + type Service struct { |
| 496 | + Name string |
| 497 | + } |
| 498 | + |
| 499 | + type DecorationInput struct { |
| 500 | + fx.In |
| 501 | + // Try to consume as slice for decoration |
| 502 | + Services []Service `group:"services"` |
| 503 | + } |
| 504 | + |
| 505 | + type DecorationOutput struct { |
| 506 | + fx.Out |
| 507 | + // Output as slice - this should break map consumption |
| 508 | + Services []Service `group:"services"` |
| 509 | + } |
| 510 | + |
| 511 | + sliceDecorator := func(input DecorationInput) DecorationOutput { |
| 512 | + // This slice decorator executes due to current dig limitation |
| 513 | + // but consumption will fail with proper validation error |
| 514 | + enhanced := make([]Service, len(input.Services)) |
| 515 | + for i, service := range input.Services { |
| 516 | + enhanced[i] = Service{Name: "[DECORATED]" + service.Name} |
| 517 | + } |
| 518 | + return DecorationOutput{Services: enhanced} |
| 519 | + } |
| 520 | + |
| 521 | + app := NewForTest(t, |
| 522 | + fx.Provide( |
| 523 | + // Provide with names (making this a named value group) |
| 524 | + fx.Annotate( |
| 525 | + func() Service { return Service{Name: "auth"} }, |
| 526 | + fx.ResultTags(`name:"auth" group:"services"`), |
| 527 | + ), |
| 528 | + fx.Annotate( |
| 529 | + func() Service { return Service{Name: "billing"} }, |
| 530 | + fx.ResultTags(`name:"billing" group:"services"`), |
| 531 | + ), |
| 532 | + ), |
| 533 | + // This slice decorator should be blocked for named value groups |
| 534 | + fx.Decorate(sliceDecorator), |
| 535 | + // Try to consume as slice - this should trigger the dig validation |
| 536 | + fx.Invoke(fx.Annotate( |
| 537 | + func(serviceSlice []Service) { |
| 538 | + t.Logf("ServiceSlice length: %d", len(serviceSlice)) |
| 539 | + }, |
| 540 | + fx.ParamTags(`group:"services"`), |
| 541 | + )), |
| 542 | + ) |
| 543 | + |
| 544 | + // Decoration fails at invoke/start time, not decorate time |
| 545 | + err := app.Start(context.Background()) |
| 546 | + defer app.Stop(context.Background()) |
| 547 | + |
| 548 | + // Should ALWAYS fail with the specific dig validation error |
| 549 | + require.Error(t, err, "Slice consumption after slice decoration of named groups should fail") |
| 550 | + |
| 551 | + // Should get the specific dig error about slice decoration |
| 552 | + assert.Contains(t, err.Error(), "cannot use slice decoration for value group", |
| 553 | + "Expected dig slice decoration error, got: %v", err) |
| 554 | + assert.Contains(t, err.Error(), "group contains named values", |
| 555 | + "Expected error about named values, got: %v", err) |
| 556 | + assert.Contains(t, err.Error(), "use map[string]T decorator instead", |
| 557 | + "Expected suggestion to use map decorator, got: %v", err) |
| 558 | + }) |
| 559 | + |
| 560 | + t.Run("map decorators work fine with named value groups", func(t *testing.T) { |
| 561 | + // This test shows the contrast - map decorators work perfectly |
| 562 | + // with named value groups, unlike slice decorators which break them |
| 563 | + |
| 564 | + type Service struct { |
| 565 | + Name string |
| 566 | + } |
| 567 | + |
| 568 | + type DecorationInput struct { |
| 569 | + fx.In |
| 570 | + // Consume as map for decoration - this works fine |
| 571 | + Services map[string]Service `group:"services"` |
| 572 | + } |
| 573 | + |
| 574 | + type DecorationOutput struct { |
| 575 | + fx.Out |
| 576 | + // Output as map - preserves the name-to-value mapping |
| 577 | + Services map[string]Service `group:"services"` |
| 578 | + } |
| 579 | + |
| 580 | + mapDecorator := func(input DecorationInput) DecorationOutput { |
| 581 | + // This decorator preserves the map structure and names |
| 582 | + enhanced := make(map[string]Service) |
| 583 | + for name, service := range input.Services { |
| 584 | + enhanced[name] = Service{Name: "[MAP_DECORATED]" + service.Name} |
| 585 | + } |
| 586 | + return DecorationOutput{Services: enhanced} |
| 587 | + } |
| 588 | + |
| 589 | + type FinalParams struct { |
| 590 | + fx.In |
| 591 | + // Consume as map after map decoration - this should work perfectly |
| 592 | + ServiceMap map[string]Service `group:"services"` |
| 593 | + } |
| 594 | + |
| 595 | + var params FinalParams |
| 596 | + app := NewForTest(t, |
| 597 | + fx.Provide( |
| 598 | + // Provide with names (making this a named value group) |
| 599 | + fx.Annotate( |
| 600 | + func() Service { return Service{Name: "auth"} }, |
| 601 | + fx.ResultTags(`name:"auth" group:"services"`), |
| 602 | + ), |
| 603 | + fx.Annotate( |
| 604 | + func() Service { return Service{Name: "billing"} }, |
| 605 | + fx.ResultTags(`name:"billing" group:"services"`), |
| 606 | + ), |
| 607 | + ), |
| 608 | + // This map decorator should work fine with named value groups |
| 609 | + fx.Decorate(mapDecorator), |
| 610 | + fx.Populate(¶ms), |
| 611 | + ) |
| 612 | + |
| 613 | + // Should succeed - map decoration preserves map functionality |
| 614 | + err := app.Start(context.Background()) |
| 615 | + defer app.Stop(context.Background()) |
| 616 | + |
| 617 | + require.NoError(t, err, "Map decoration should work fine with named value groups") |
| 618 | + |
| 619 | + // Verify the final populated params also work correctly |
| 620 | + require.Len(t, params.ServiceMap, 2) |
| 621 | + assert.Equal(t, "[MAP_DECORATED]auth", params.ServiceMap["auth"].Name) |
| 622 | + assert.Equal(t, "[MAP_DECORATED]billing", params.ServiceMap["billing"].Name) |
| 623 | + }) |
| 624 | +} |
| 625 | + |
| 626 | +// Test processor types for map decoration tests |
| 627 | +type testProcessor interface { |
| 628 | + Process(input string) string |
| 629 | + Name() string |
| 630 | +} |
| 631 | + |
| 632 | +type testBasicProcessor struct { |
| 633 | + name string |
| 634 | +} |
| 635 | + |
| 636 | +func (b *testBasicProcessor) Process(input string) string { |
| 637 | + return b.name + ": " + input |
| 638 | +} |
| 639 | + |
| 640 | +func (b *testBasicProcessor) Name() string { |
| 641 | + return b.name |
| 642 | +} |
| 643 | + |
| 644 | +type testEnhancedProcessor struct { |
| 645 | + wrapped testProcessor |
| 646 | + prefix string |
| 647 | +} |
| 648 | + |
| 649 | +func (e *testEnhancedProcessor) Process(input string) string { |
| 650 | + return e.prefix + " " + e.wrapped.Process(input) |
| 651 | +} |
| 652 | + |
| 653 | +func (e *testEnhancedProcessor) Name() string { |
| 654 | + return e.wrapped.Name() |
| 655 | +} |
| 656 | + |
| 657 | +// TestMapValueGroupsDecoration tests decoration of map value groups |
| 658 | +func TestMapValueGroupsDecoration(t *testing.T) { |
| 659 | + t.Parallel() |
| 660 | + |
| 661 | + t.Run("decorate map value groups", func(t *testing.T) { |
| 662 | + t.Parallel() |
| 663 | + |
| 664 | + type DecorationInput struct { |
| 665 | + fx.In |
| 666 | + Processors map[string]testProcessor `group:"processors"` |
| 667 | + } |
| 668 | + |
| 669 | + type DecorationOutput struct { |
| 670 | + fx.Out |
| 671 | + Processors map[string]testProcessor `group:"processors"` |
| 672 | + } |
| 673 | + |
| 674 | + decorateProcessors := func(input DecorationInput) DecorationOutput { |
| 675 | + enhanced := make(map[string]testProcessor) |
| 676 | + for name, processor := range input.Processors { |
| 677 | + enhanced[name] = &testEnhancedProcessor{ |
| 678 | + wrapped: processor, |
| 679 | + prefix: "[ENHANCED]", |
| 680 | + } |
| 681 | + } |
| 682 | + return DecorationOutput{Processors: enhanced} |
| 683 | + } |
| 684 | + |
| 685 | + type FinalParams struct { |
| 686 | + fx.In |
| 687 | + Processors map[string]testProcessor `group:"processors"` |
| 688 | + } |
| 689 | + |
| 690 | + var params FinalParams |
| 691 | + app := NewForTest(t, |
| 692 | + fx.Provide( |
| 693 | + fx.Annotate( |
| 694 | + func() testProcessor { return &testBasicProcessor{name: "json"} }, |
| 695 | + fx.ResultTags(`name:"json" group:"processors"`), |
| 696 | + ), |
| 697 | + fx.Annotate( |
| 698 | + func() testProcessor { return &testBasicProcessor{name: "xml"} }, |
| 699 | + fx.ResultTags(`name:"xml" group:"processors"`), |
| 700 | + ), |
| 701 | + ), |
| 702 | + fx.Decorate(decorateProcessors), |
| 703 | + fx.Populate(¶ms), |
| 704 | + ) |
| 705 | + |
| 706 | + err := app.Start(context.Background()) |
| 707 | + defer app.Stop(context.Background()) |
| 708 | + require.NoError(t, err) |
| 709 | + |
| 710 | + require.Len(t, params.Processors, 2) |
| 711 | + |
| 712 | + // Test that processors are decorated |
| 713 | + jsonResult := params.Processors["json"].Process("data") |
| 714 | + assert.Equal(t, "[ENHANCED] json: data", jsonResult) |
| 715 | + |
| 716 | + xmlResult := params.Processors["xml"].Process("data") |
| 717 | + assert.Equal(t, "[ENHANCED] xml: data", xmlResult) |
| 718 | + |
| 719 | + // Names should be preserved |
| 720 | + assert.Equal(t, "json", params.Processors["json"].Name()) |
| 721 | + assert.Equal(t, "xml", params.Processors["xml"].Name()) |
| 722 | + }) |
| 723 | + |
| 724 | + t.Run("single decoration layer", func(t *testing.T) { |
| 725 | + t.Parallel() |
| 726 | + |
| 727 | + type DecorationInput struct { |
| 728 | + fx.In |
| 729 | + Processors map[string]testProcessor `group:"processors"` |
| 730 | + } |
| 731 | + |
| 732 | + type DecorationOutput struct { |
| 733 | + fx.Out |
| 734 | + Processors map[string]testProcessor `group:"processors"` |
| 735 | + } |
| 736 | + |
| 737 | + decoration := func(input DecorationInput) DecorationOutput { |
| 738 | + enhanced := make(map[string]testProcessor) |
| 739 | + for name, processor := range input.Processors { |
| 740 | + enhanced[name] = &testEnhancedProcessor{ |
| 741 | + wrapped: processor, |
| 742 | + prefix: "[DECORATED]", |
| 743 | + } |
| 744 | + } |
| 745 | + return DecorationOutput{Processors: enhanced} |
| 746 | + } |
| 747 | + |
| 748 | + type FinalParams struct { |
| 749 | + fx.In |
| 750 | + Processors map[string]testProcessor `group:"processors"` |
| 751 | + } |
| 752 | + |
| 753 | + var params FinalParams |
| 754 | + app := NewForTest(t, |
| 755 | + fx.Provide( |
| 756 | + fx.Annotate( |
| 757 | + func() testProcessor { return &testBasicProcessor{name: "base"} }, |
| 758 | + fx.ResultTags(`name:"base" group:"processors"`), |
| 759 | + ), |
| 760 | + ), |
| 761 | + fx.Decorate(decoration), |
| 762 | + fx.Populate(¶ms), |
| 763 | + ) |
| 764 | + |
| 765 | + err := app.Start(context.Background()) |
| 766 | + defer app.Stop(context.Background()) |
| 767 | + require.NoError(t, err) |
| 768 | + |
| 769 | + require.Len(t, params.Processors, 1) |
| 770 | + |
| 771 | + // Test decoration |
| 772 | + result := params.Processors["base"].Process("test") |
| 773 | + assert.Equal(t, "[DECORATED] base: test", result) |
| 774 | + }) |
| 775 | + |
| 776 | + t.Run("decoration preserves map keys", func(t *testing.T) { |
| 777 | + t.Parallel() |
| 778 | + |
| 779 | + type DecorationInput struct { |
| 780 | + fx.In |
| 781 | + Processors map[string]testProcessor `group:"processors"` |
| 782 | + } |
| 783 | + |
| 784 | + type DecorationOutput struct { |
| 785 | + fx.Out |
| 786 | + Processors map[string]testProcessor `group:"processors"` |
| 787 | + } |
| 788 | + |
| 789 | + var decorationInputKeys []string |
| 790 | + var decorationOutputKeys []string |
| 791 | + |
| 792 | + decorateWithKeyTracking := func(input DecorationInput) DecorationOutput { |
| 793 | + decorationInputKeys = make([]string, 0, len(input.Processors)) |
| 794 | + for key := range input.Processors { |
| 795 | + decorationInputKeys = append(decorationInputKeys, key) |
| 796 | + } |
| 797 | + |
| 798 | + enhanced := make(map[string]testProcessor) |
| 799 | + for name, processor := range input.Processors { |
| 800 | + enhanced[name] = &testEnhancedProcessor{ |
| 801 | + wrapped: processor, |
| 802 | + prefix: "[TRACKED]", |
| 803 | + } |
| 804 | + } |
| 805 | + |
| 806 | + decorationOutputKeys = make([]string, 0, len(enhanced)) |
| 807 | + for key := range enhanced { |
| 808 | + decorationOutputKeys = append(decorationOutputKeys, key) |
| 809 | + } |
| 810 | + |
| 811 | + return DecorationOutput{Processors: enhanced} |
| 812 | + } |
| 813 | + |
| 814 | + type FinalParams struct { |
| 815 | + fx.In |
| 816 | + Processors map[string]testProcessor `group:"processors"` |
| 817 | + } |
| 818 | + |
| 819 | + var params FinalParams |
| 820 | + app := NewForTest(t, |
| 821 | + fx.Provide( |
| 822 | + fx.Annotate( |
| 823 | + func() testProcessor { return &testBasicProcessor{name: "alpha"} }, |
| 824 | + fx.ResultTags(`name:"alpha" group:"processors"`), |
| 825 | + ), |
| 826 | + fx.Annotate( |
| 827 | + func() testProcessor { return &testBasicProcessor{name: "beta"} }, |
| 828 | + fx.ResultTags(`name:"beta" group:"processors"`), |
| 829 | + ), |
| 830 | + fx.Annotate( |
| 831 | + func() testProcessor { return &testBasicProcessor{name: "gamma"} }, |
| 832 | + fx.ResultTags(`name:"gamma" group:"processors"`), |
| 833 | + ), |
| 834 | + ), |
| 835 | + fx.Decorate(decorateWithKeyTracking), |
| 836 | + fx.Populate(¶ms), |
| 837 | + ) |
| 838 | + |
| 839 | + err := app.Start(context.Background()) |
| 840 | + defer app.Stop(context.Background()) |
| 841 | + require.NoError(t, err) |
| 842 | + |
| 843 | + require.Len(t, params.Processors, 3) |
| 844 | + |
| 845 | + // Verify keys are preserved through decoration |
| 846 | + assert.ElementsMatch(t, []string{"alpha", "beta", "gamma"}, decorationInputKeys) |
| 847 | + assert.ElementsMatch(t, []string{"alpha", "beta", "gamma"}, decorationOutputKeys) |
| 848 | + |
| 849 | + // Verify final map has correct keys |
| 850 | + finalKeys := make([]string, 0, len(params.Processors)) |
| 851 | + for key := range params.Processors { |
| 852 | + finalKeys = append(finalKeys, key) |
| 853 | + } |
| 854 | + assert.ElementsMatch(t, []string{"alpha", "beta", "gamma"}, finalKeys) |
| 855 | + }) |
488 | 856 | } |
0 commit comments