Skip to content

Commit 68678a5

Browse files
jquirkeclaude
andcommitted
Implement map value groups support in fx
Add support for consuming named value groups as map[string]T, enabling both individual named access and map-based consumption of the same providers. While the core functionality is implemented in dig PR #381, this commit adds comprehensive fx integration and test coverage. Changes: - Add map_groups_test.go with extensive test coverage for map consumption, interfaces, pointer types, name+group combinations, and edge cases - Enhance decorate_test.go with map decoration tests and validation of slice decorator restrictions for named value groups - Update go.mod to use dig fork with map value groups support - Update CHANGELOG.md to document the new functionality Fixes #1036 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 6fab1b2 commit 68678a5

File tree

9 files changed

+1351
-3
lines changed

9 files changed

+1351
-3
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1111
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1212

1313
## Unreleased
14-
- No changes yet.
14+
15+
### Added
16+
- Support for map value groups consumption via `map[string]T` for named value groups,
17+
enabling both individual named access and map-based access to the same providers.
18+
- Comprehensive test coverage for map value groups functionality including decoration,
19+
edge cases, and validation of slice decorator restrictions.
1520

1621
## [1.24.0](https://github.com/uber-go/fx/compare/v1.23.0...v1.24.0) - 2025-05-13
1722

decorate_test.go

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package fx_test
2222

2323
import (
24+
"context"
2425
"errors"
2526
"strings"
2627
"testing"
@@ -485,4 +486,371 @@ func TestDecorateFailure(t *testing.T) {
485486
require.Error(t, err)
486487
assert.Contains(t, err.Error(), "missing dependencies")
487488
})
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(&params),
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(&params),
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(&params),
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(&params),
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+
})
488856
}

0 commit comments

Comments
 (0)