Add map value groups support to dig
Summary
This PR implements map value groups for the dig dependency injection framework, allowing value groups to be consumed as map[string]T in addition to the existing []T slice format. This addresses issue #380 and related Fx dependent feature uber-go/fx#998, uber-go/fx#1036 which is co-implemented uber-go/fx#1279.
Key Features
1. Map Value Group Consumption
// Provide values with names
c.Provide(func() int{return 1}, dig.Name("first"), dig.Group("numbers"))
c.Provide(func() int{return 2}, dig.Name("second"), dig.Group("numbers"))
// Consume as map
type Params struct {
dig.In
NumMap map[string]int `group:"numbers"` // {"first":1, "second":2}
NumSlice []int `group:"numbers"` // [1, 2] - still works
Individual int `name:"first"` // 1 - still works
}
2. Simultaneous Name + Group Support
- Removed mutual exclusivity between
dig.Name()anddig.Group() - Enables providing values with both name AND group annotations
- Names become map keys when consuming as
map[string]T
3. Robust Decoration Validation
- Prevents incompatible slice decorator patterns with named value groups
- Provides clear error messages guiding users to correct decorator usage
- Ensures consistent behavior between slice and map consumption
Implementation Details
Core Changes
- Enhanced result processing to track map keys (names) alongside values
- Updated parameter building to support map construction from named values
- Robust validation ensuring all map entries have names
- Cross-scope behavior maintained consistently with existing slice patterns
Validation & Error Handling
- Map groups require names: Clear error if any value lacks a name
-
String keys only:
map[string]Tenforced (validated at param creation) - Decoration compatibility: Slice decorators blocked for named groups with helpful guidance
Comprehensive Test Coverage
- ✅ Basic map value group functionality
- ✅ Interface and pointer type support
- ✅ dig.As transformation compatibility
- ✅ Mixed consumption patterns (individual + slice + map)
- ✅ Edge cases: empty groups, invalid keys, decoration scenarios
- ✅ Cross-scope behavior documented and verified
- ✅ Error conditions with clear messages
- ✅ Soft value groups with map consumption
Breaking Changes
None - This is a purely additive feature that maintains full backward compatibility.
Decoration System Design Decision
Pattern Compatibility & Non-Breaking Nature
During development, we identified that slice decorators (func([]T) []T) are fundamentally incompatible with named value groups because:
- Slice decorators lose key information (names) needed for map reconstruction
- Cannot convert decorator output to
map[string]Twithout keys
Important: This validation is not a breaking change because:
-
Named groups were never supported before this PR - previously
dig.Name()anddig.Group()were mutually exclusive - No existing code could have been using this pattern - it was impossible until we enabled simultaneous name+group tags
- All existing decoration patterns continue to work unchanged
Design Rationale
We chose to block incompatible patterns rather than allow silent failures because:
- Clear Error Messages: Users get immediate, actionable feedback instead of wondering why decorators seem ignored
- Consistent Behavior: Both slice and map consumers get predictable results
- Future-Proof: Prevents confusion as usage scales
Decorator Execution Clarification
Current Behavior: Slice decorators do execute when used with named groups, but their results are blocked during validation. This is the correct behavior given the current architecture:
- Decorators run first to produce their results
- Validation happens during consumption to block problematic patterns
- Error is properly returned preventing the invalid consumption
The tests document this behavior:
// Decorator executes but results are blocked
t.Log("Slice decorator called (results will be blocked)")
Solution: Clear Validation
Added execution-time validation that prevents this pattern and guides users:
// This now produces a clear error (only possible with this PR's new functionality):
c.Provide(func() int{return 1}, dig.Name("key"), dig.Group("nums"))
c.Decorate(func(nums []int) []int { return nums }) // slice decorator
// Error: "cannot use slice decoration for value group: group contains named values, use map[string]T decorator instead"
Correct Pattern:
// Use map decorators for named groups:
c.Decorate(func(p struct{
dig.In
NumMap map[string]int `group:"nums"`
}) struct{
dig.Out
NumMap map[string]int `group:"nums"`
} {
// Modify map and return
return struct{...}{NumMap: modifiedMap}
})
Existing Patterns Unchanged:
// Unnamed groups + slice decorators continue to work exactly as before:
c.Provide(func() int{return 1}, dig.Group("nums")) // no name
c.Provide(func() int{return 2}, dig.Group("nums")) // no name
c.Decorate(func(nums []int) []int { return nums }) // works fine
Soft Value Groups Integration
New Test Coverage
Added comprehensive testing for soft value groups with map consumption:
- Empty soft maps: Verify no providers called when only soft group consumed
- Selective execution: Only executed constructors contribute to soft maps
- Mixed consumption: Soft maps work alongside slice and regular map consumption
- Behavior consistency: Soft map and slice consumption behave identically
Key Findings
- ✅ Soft groups work correctly with maps - No additional implementation needed
- ✅ Consistent semantics - Same execution behavior as slice consumption
- ✅ Reliable operation - Existing soft group logic handles maps seamlessly
// Soft map groups only contain values from executed constructors
type Params struct {
dig.In
Service int `name:"config"` // Forces provider execution
SoftMap map[string]string `group:"handlers,soft"` // Only executed handlers
}
Edge Cases Handled
Decoration System
- Multiple decorators: Correctly forbidden with "already decorated" errors
- Map decorators + unnamed values: Fails appropriately (maps require names)
- Cross-scope decoration: Consistent behavior between slice and map patterns
- Empty group decoration: Works correctly for both slices and maps
Type System
- Interface types: Full support with map value groups
- Pointer types: Complete compatibility
- dig.As transformations: Seamless integration with maps
- Invalid map keys: Clear validation errors
Soft Groups
- Empty soft maps: Correctly handle no executed providers
- Partial execution: Only include values from executed constructors
- Mixed consumption: Soft and regular consumption work together
Testing Strategy
Functional Testing
- 527+ new test lines covering comprehensive scenarios
- Edge case analysis with systematic coverage
- Cross-scope behavior baseline documentation
- Error condition testing with message validation
- Soft group integration with complete scenario coverage
Compatibility Testing
- All existing tests pass - no regressions
- Mixed consumption patterns verified
- Backward compatibility maintained
Performance Impact
Minimal - Map construction adds negligible overhead only when consuming groups as maps.
Documentation
- Comprehensive code comments explaining behavior
- Clear error messages for invalid usage patterns
- Test coverage serving as usage examples
- Complete package documentation in doc.go with examples and decorator guidance
- Edge case documentation for maintainers
Related Issues
- Closes #380
- Addresses uber-go/fx#998
- Addresses uber-go/fx#1036
🤖 Generated with Claude Code