dig icon indicating copy to clipboard operation
dig copied to clipboard

Add map value groups support to dig

Open jquirke opened this issue 2 years ago • 3 comments

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() and dig.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]T enforced (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:

  1. Slice decorators lose key information (names) needed for map reconstruction
  2. Cannot convert decorator output to map[string]T without keys

Important: This validation is not a breaking change because:

  • Named groups were never supported before this PR - previously dig.Name() and dig.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:

  1. Clear Error Messages: Users get immediate, actionable feedback instead of wondering why decorators seem ignored
  2. Consistent Behavior: Both slice and map consumers get predictable results
  3. 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

jquirke avatar Mar 06 '23 06:03 jquirke

CLA assistant check
All committers have signed the CLA.

CLAassistant avatar Mar 06 '23 06:03 CLAassistant