Documentation
¶
Overview ¶
Package mapper provides a reflection-based type conversion framework.
The mapper eliminates the need for explicit conversion functions between types by using runtime registration and automatic type resolution. This is particularly useful for converting between domain types and protocol buffer types without creating circular dependencies or requiring global awareness of conversion functions.
Key Benefits:
- Eliminates circular dependencies between packages
- Single, consistent API: mapper.Convert(src, &dst)
- Context propagation for environment variable resolution
- Bidirectional conversions with automatic type matching
- Thread-safe registration and conversion
Basic Usage:
// 1. Register converters (typically in init functions)
func init() {
mapper.MustRegister(func(ctx context.Context, src *User) (*UserProto, error) {
return &UserProto{Id: src.ID, Name: src.Name}, nil
})
}
// 2. Perform conversions
var userProto *UserProto
err := mapper.Convert(user, &userProto)
Context-Aware Conversions:
// Register converter that uses environment resolution
mapper.MustRegister(func(ctx context.Context, src *Config) (*ConfigProto, error) {
resolver := mapper.GetResolver(ctx)
expanded, err := src.Template.Envsubst(resolver) // ${VAR} → actual value
return &ConfigProto{Value: expanded}, err
})
// Convert with environment resolver
envResolver := func(key string) string { return os.Getenv(key) }
var configProto *ConfigProto
err := mapper.WithResolver(envResolver).Convert(config, &configProto)
The Pointer Pattern:
Conversions require passing the address of the destination variable:
var target *TargetType // Initially nil mapper.Convert(src, &target) // Pass address (&target) // target now points to converted value
This allows the mapper to modify what the pointer points to, changing it from nil to the actual converted object via reflection.
Index ¶
- Variables
- func Clear()
- func Convert(src any, dst any) error
- func IsConversionError(err error) bool
- func IsNoMapperError(err error) bool
- func MustRegister[S, T any](fn func(context.Context, S) (T, error))
- func Register[S, T any](fn func(context.Context, S) (T, error)) error
- type ConversionError
- type Mapper
- type MapperFunc
- type NoMapperError
- type Resolver
Constants ¶
This section is empty.
Variables ¶
var ErrConversionFailure = &ConversionError{}
ErrConversionFailure is a sentinel error for use with errors.Is() to check for any conversion failure
var ErrDuplicateRegistration = errors.New("mapper already registered for these types")
ErrDuplicateRegistration is returned when trying to register a mapper for types that already have a mapper
var ErrInvalidRegistration = errors.New("cannot register nil mapper function")
ErrInvalidRegistration is returned when trying to register a nil function
var ErrNoMapper = &NoMapperError{}
ErrNoMapper is a sentinel error for use with errors.Is()
Functions ¶
func Clear ¶
func Clear()
Clear removes all registered mappers from the global registry.
This function is primarily intended for testing to ensure test isolation. Each test can start with a clean mapper registry without interference from other tests or global registrations.
WARNING: This affects the global mapper registry. Use with caution in production code as it will remove ALL registered converters.
Example test usage:
func TestMapper(t *testing.T) {
// Ensure clean state
mapper.Clear()
defer mapper.Clear() // Clean up after test
// Register test-specific mappers
mapper.MustRegister(func(ctx context.Context, src TestType) (TargetType, error) {
return TargetType{Value: src.Value}, nil
})
// Run test...
}
func Convert ¶
Convert performs type conversion using the default mapper with background context.
This is a convenience function for simple conversions that don't require context propagation (e.g., environment variable resolution).
The destination must be a pointer to the target type. The mapper uses reflection to set the pointed-to value with the conversion result.
Returns:
- NoMapperError if no converter is registered for src→dst types
- ConversionError if the converter function returns an error
- nil on successful conversion
Example:
var target *UserProto
if err := mapper.Convert(user, &target); err != nil {
var noMapper *NoMapperError
if noMapper, ok := errors.AsType[*NoMapperError](err); ok {
log.Printf("No converter from %v to %v", noMapper.SrcType, noMapper.DstType)
}
return err
}
// target now points to converted UserProto
For conversions that need context (e.g., environment resolution), use:
mapper.WithResolver(envResolver).Convert(src, &dst)
func IsConversionError ¶
IsConversionError returns true if the error is a ConversionError
func IsNoMapperError ¶
IsNoMapperError returns true if the error is a NoMapperError
func MustRegister ¶
MustRegister a type converter function, panicking on any registration error.
This is the preferred method for registering converters in init() functions where registration failures should halt application startup rather than being handled at runtime.
Panics if:
- fn is nil (ErrInvalidRegistration)
- A mapper is already registered for S→T (ErrDuplicateRegistration)
Example:
func init() {
// Register bidirectional conversions
mapper.MustRegister(func(ctx context.Context, src *Artifact) (*azdext.Artifact, error) {
return &azdext.Artifact{
Kind: convertKind(src.Kind),
Location: src.Location,
}, nil
})
mapper.MustRegister(func(ctx context.Context, src *azdext.Artifact) (Artifact, error) {
return Artifact{
Kind: convertKindFromProto(src.Kind),
Location: src.Location,
}, nil
})
}
func Register ¶
Register a type converter function that transforms type S to type T.
The mapper framework uses reflection to automatically route conversion requests to the appropriate registered function based on source and target types.
Registration is type-safe at compile time and thread-safe at runtime. Each S→T type pair can only have one registered converter.
Parameters:
- fn: Conversion function that takes (context, source) and returns (target, error)
Returns an error if:
- fn is nil (ErrInvalidRegistration)
- A mapper is already registered for S→T (ErrDuplicateRegistration)
Example:
// Register a converter from *User to *UserProto
err := mapper.Register(func(ctx context.Context, src *User) (*UserProto, error) {
return &UserProto{
Id: src.ID,
Name: src.Name,
}, nil
})
if err != nil {
// Handle registration error
}
Use this when you need to handle registration errors programmatically. For init() functions where errors should halt startup, use MustRegister instead.
Types ¶
type ConversionError ¶
ConversionError is returned when a mapper function fails during conversion. It wraps the original error and provides context about which types were being converted. Callers can check for this error in multiple ways:
1. Using errors.AsType() for detailed inspection:
var convErr *ConversionError
if convErr, ok := errors.AsType[*ConversionError](err); ok {
log.Printf("Conversion failed from %v to %v: %v", convErr.SrcType, convErr.DstType, convErr.Err)
}
2. Using the provided helper:
if IsConversionError(err) {
// Handle conversion failure case
}
3. Using errors.Is() for type checking or wrapped error matching:
if errors.Is(err, ErrConversionFailure) {
// Handle any ConversionError using sentinel
}
if errors.Is(err, &ConversionError{}) {
// Handle any ConversionError using type
}
if errors.Is(err, specificErr) {
// Handle when ConversionError wraps specificErr
}
4. Using errors.Unwrap() to access the original error:
if innerErr := errors.Unwrap(err); innerErr != nil {
// Work with the original error
}
func (*ConversionError) Error ¶
func (e *ConversionError) Error() string
func (*ConversionError) Is ¶
func (e *ConversionError) Is(target error) bool
Is implements error equality for errors.Is() support. It returns true if the target error is also a ConversionError, or if the wrapped error matches.
func (*ConversionError) Unwrap ¶
func (e *ConversionError) Unwrap() error
Unwrap returns the wrapped error for errors.Unwrap() support
type Mapper ¶
type Mapper struct {
// contains filtered or unexported fields
}
Mapper provides a context-aware interface for type conversion.
Mapper instances are created with specific contexts that can carry environment variable resolvers and other conversion-time data.
The zero value is not useful; create instances using WithResolver():
mapper := mapper.WithResolver(envResolver) err := mapper.Convert(src, &dst)
Or use the package-level Convert function for simple conversions:
err := mapper.Convert(src, &dst) // Uses background context
func WithResolver ¶
WithResolver returns a mapper configured with an environment variable resolver.
The resolver enables conversion functions to expand environment variables and other template strings during the conversion process. This is essential for conversions involving configuration with ${VAR} placeholders.
Parameters:
- resolver: Function that takes a variable name and returns its value. If nil, returns a mapper with default background context.
Example:
// Create resolver that reads from environment
envResolver := func(key string) string {
return os.Getenv(key)
}
// Convert with environment resolution
var serviceConfig *azdext.ServiceConfig
err := mapper.WithResolver(envResolver).Convert(src, &serviceConfig)
// Inside conversion functions, retrieve resolver:
func convertService(ctx context.Context, src *Service) (*azdext.Service, error) {
resolver := mapper.GetResolver(ctx)
expandedImage := src.Image.Envsubst(resolver) // ${REGISTRY}/app:${TAG}
return &azdext.Service{Image: expandedImage}, nil
}
func (*Mapper) Convert ¶
Convert performs type conversion using this mapper's context.
This method is called on mapper instances created with WithResolver() to perform conversions with context propagation (e.g., environment resolution).
The conversion process:
- Determines source and destination types via reflection
- Looks up registered converter function for src→dst pair
- Invokes converter with this mapper's context
- Uses reflection to set the destination pointer
The destination must be a pointer (&target). The mapper will modify what the pointer points to, changing it from nil to the converted value.
Example conversion flow:
var proto *azdext.Artifact // Initially nil mapper.Convert(artifact, &proto) // Pass address of pointer // proto now points to converted azdext.Artifact
Error handling:
if err := m.Convert(src, &dst); err != nil {
if mapper.IsNoMapperError(err) {
// No converter registered for this type pair
} else if mapper.IsConversionError(err) {
// Converter function returned an error
}
}
type MapperFunc ¶
MapperFunc is the internal function signature for registered conversion functions.
This type is used internally by the mapper framework after conversion functions are registered via Register or MustRegister. The framework wraps user-provided conversion functions in this signature to handle reflection and error propagation.
Users typically don't interact with this type directly, but instead provide functions matching: func(context.Context, SourceType) (TargetType, error)
type NoMapperError ¶
NoMapperError is returned when no mapper is registered for the given types. Callers can check for this error in multiple ways:
1. Using errors.Is() with the sentinel:
if errors.Is(err, ErrNoMapper) {
// Handle missing mapper case
}
2. Using errors.AsType() for detailed inspection:
var noMapperErr *NoMapperError
if noMapperErr, ok := errors.AsType[*NoMapperError](err); ok {
log.Printf("Missing mapper from %v to %v", noMapperErr.SrcType, noMapperErr.DstType)
}
3. Using the provided helper:
if IsNoMapperError(err) {
// Handle missing mapper case
}
func (*NoMapperError) Error ¶
func (e *NoMapperError) Error() string
func (*NoMapperError) Is ¶
func (e *NoMapperError) Is(target error) bool
Is implements error equality for errors.Is() support. It returns true if the target error is also a NoMapperError.
type Resolver ¶
Resolver is a function that resolves environment variable names to their values.
This function type is used by conversion functions to expand template strings containing variable references like ${REGISTRY_URL} or ${BUILD_VERSION}.
Example implementations:
// Simple environment variable resolver
envResolver := func(key string) string {
return os.Getenv(key)
}
// Custom resolver with defaults
customResolver := func(key string) string {
if value := os.Getenv(key); value != "" {
return value
}
// Return defaults for known keys
switch key {
case "REGISTRY":
return "registry.azurecr.io"
default:
return ""
}
}
func GetResolver ¶
GetResolver retrieves the environment variable resolver from the conversion context.
This function is called within conversion functions to access the resolver that was attached via WithResolver(). If no resolver was attached, returns nil.
Usage pattern in conversion functions:
func convertService(ctx context.Context, src *ServiceConfig) (*azdext.ServiceConfig, error) {
resolver := mapper.GetResolver(ctx)
if resolver != nil {
// Expand environment variables in configuration
expandedImage, err := src.Image.Envsubst(func(key string) string {
return resolver(key)
})
if err != nil {
return nil, err
}
return &azdext.ServiceConfig{Image: expandedImage}, nil
}
// Fallback when no resolver available
return &azdext.ServiceConfig{Image: string(src.Image)}, nil
}
Returns nil if no resolver was attached to the mapper context.
func ResolverFromContext ¶
ResolverFromContext retrieves the resolver from context with explicit presence indication.
This is similar to GetResolver but returns a boolean indicating whether a resolver was actually attached to the context. Use this when you need to distinguish between "no resolver attached" and "resolver attached but returns empty strings".
Example:
func convertWithOptionalResolver(ctx context.Context, src *Config) (*Proto, error) {
if resolver, hasResolver := mapper.ResolverFromContext(ctx); hasResolver {
// Resolver was explicitly attached, use it even if it returns ""
expanded := resolver("SOME_VAR")
return &Proto{Value: expanded}, nil
} else {
// No resolver attached, use different strategy
return &Proto{Value: "default"}, nil
}
}
Returns:
- resolver: The resolver function if attached, nil otherwise
- ok: true if a resolver was attached, false otherwise