Documentation
¶
Overview ¶
Package eddy is a type-safe, rule-driven client resolution package
Example (BuilderInRules) ¶
Example: Rules provide builders directly
package main
import (
"context"
"fmt"
"time"
"github.com/theopenlane/eddy"
"github.com/theopenlane/eddy/helpers"
"github.com/theopenlane/utils/contextx"
)
// Mock types for examples
type StorageClient interface {
Upload(ctx context.Context, data []byte) error
Download(ctx context.Context, key string) ([]byte, error)
}
type S3Client struct {
endpoint string
bucket string
}
func (c *S3Client) Upload(ctx context.Context, data []byte) error {
return nil
}
func (c *S3Client) Download(ctx context.Context, key string) ([]byte, error) {
return nil, nil
}
type StorageCredentials struct {
AccessKey string
SecretKey string
Region string
}
type StorageConfig struct {
Bucket string
Endpoint string
Timeout time.Duration
}
type StorageCacheKey struct {
TenantID string
ProviderID string
}
func (k StorageCacheKey) String() string {
return fmt.Sprintf("%s:%s", k.TenantID, k.ProviderID)
}
// S3Builder implements Builder interface
type S3Builder struct{}
func (b *S3Builder) Build(ctx context.Context, creds StorageCredentials, config StorageConfig) (StorageClient, error) {
return &S3Client{
endpoint: config.Endpoint,
bucket: config.Bucket,
}, nil
}
func (b *S3Builder) ProviderType() string {
return "s3"
}
// Context hint types
type ProviderHint string
func main() {
// Create a resolver with rules that provide builders
resolver := eddy.NewResolver[StorageClient, StorageCredentials, StorageConfig]()
// Define builders once (these are stateless factories, reused across rules)
s3Builder := &S3Builder{}
// Rule 1: S3 provider
resolver.AddRule(&helpers.MatchHintRule[StorageClient, StorageCredentials, StorageConfig, ProviderHint]{
Value: "s3",
Resolver: func(ctx context.Context) (*eddy.ResolvedProvider[StorageClient, StorageCredentials, StorageConfig], error) {
// Resolve credentials from environment, vault, etc.
creds := StorageCredentials{
AccessKey: "access-key",
SecretKey: "secret-key",
Region: "us-east-1",
}
config := StorageConfig{
Bucket: "my-bucket",
Endpoint: "s3.amazonaws.com",
Timeout: 30 * time.Second,
}
return &eddy.ResolvedProvider[StorageClient, StorageCredentials, StorageConfig]{
Builder: s3Builder, // Rule provides the builder
Output: creds,
Config: config,
}, nil
},
})
// Setup context with hint
ctx := context.Background()
ctx = contextx.WithString(ctx, ProviderHint("s3"))
// Resolve returns Result with Builder included
result := resolver.Resolve(ctx)
if !result.IsPresent() {
fmt.Println("no provider resolved")
return
}
res := result.MustGet()
// Create client service and pool
pool := eddy.NewClientPool[StorageClient](1 * time.Hour)
service := eddy.NewClientService[StorageClient, StorageCredentials, StorageConfig](pool)
// Build cache key
cacheKey := StorageCacheKey{
TenantID: "tenant-1",
ProviderID: res.Builder.ProviderType(), // Get type from builder
}
// GetClient now takes the builder directly from the result
client := service.GetClient(ctx, cacheKey, res.Builder, res.Output, res.Config)
if !client.IsPresent() {
fmt.Println("failed to get client")
return
}
fmt.Println("client created successfully")
}
Output: client created successfully
Example (FallbackChain) ¶
Example: Fallback chain with different builders
package main
import (
"context"
"fmt"
"time"
"github.com/theopenlane/eddy"
"github.com/theopenlane/eddy/helpers"
)
// Mock types for examples
type StorageClient interface {
Upload(ctx context.Context, data []byte) error
Download(ctx context.Context, key string) ([]byte, error)
}
type S3Client struct {
endpoint string
bucket string
}
func (c *S3Client) Upload(ctx context.Context, data []byte) error {
return nil
}
func (c *S3Client) Download(ctx context.Context, key string) ([]byte, error) {
return nil, nil
}
type StorageCredentials struct {
AccessKey string
SecretKey string
Region string
}
type StorageConfig struct {
Bucket string
Endpoint string
Timeout time.Duration
}
// S3Builder implements Builder interface
type S3Builder struct{}
func (b *S3Builder) Build(ctx context.Context, creds StorageCredentials, config StorageConfig) (StorageClient, error) {
return &S3Client{
endpoint: config.Endpoint,
bucket: config.Bucket,
}, nil
}
func (b *S3Builder) ProviderType() string {
return "s3"
}
func main() {
s3Builder := &S3Builder{}
// Could have other builders like &R2Builder{}, &GCSBuilder{}, etc.
resolver := eddy.NewResolver[StorageClient, StorageCredentials, StorageConfig]()
// Try multiple credential sources in order
resolver.AddRule(&helpers.FallbackChainRule[StorageClient, StorageCredentials, StorageConfig]{
Resolvers: []func(context.Context) (*eddy.ResolvedProvider[StorageClient, StorageCredentials, StorageConfig], error){
// Try database credentials first
func(ctx context.Context) (*eddy.ResolvedProvider[StorageClient, StorageCredentials, StorageConfig], error) {
// Would fetch from database...
return nil, fmt.Errorf("no database credentials")
},
// Fall back to environment variables
func(ctx context.Context) (*eddy.ResolvedProvider[StorageClient, StorageCredentials, StorageConfig], error) {
return &eddy.ResolvedProvider[StorageClient, StorageCredentials, StorageConfig]{
Builder: s3Builder,
Output: StorageCredentials{
AccessKey: "env-key",
SecretKey: "env-secret",
Region: "us-east-1",
},
Config: StorageConfig{
Bucket: "env-bucket",
Timeout: 30 * time.Second,
},
}, nil
},
},
})
ctx := context.Background()
result := resolver.Resolve(ctx)
if result.IsPresent() {
res := result.MustGet()
fmt.Printf("resolved with builder: %s\n", res.Builder.ProviderType())
}
}
Output: resolved with builder: s3
Example (Multitenancy) ¶
Example: Multitenancy with builder-in-rules
package main
import (
"context"
"fmt"
"time"
"github.com/theopenlane/eddy"
"github.com/theopenlane/eddy/helpers"
"github.com/theopenlane/utils/contextx"
)
// Mock types for examples
type StorageClient interface {
Upload(ctx context.Context, data []byte) error
Download(ctx context.Context, key string) ([]byte, error)
}
type S3Client struct {
endpoint string
bucket string
}
func (c *S3Client) Upload(ctx context.Context, data []byte) error {
return nil
}
func (c *S3Client) Download(ctx context.Context, key string) ([]byte, error) {
return nil, nil
}
type StorageCredentials struct {
AccessKey string
SecretKey string
Region string
}
type StorageConfig struct {
Bucket string
Endpoint string
Timeout time.Duration
}
type StorageCacheKey struct {
TenantID string
ProviderID string
}
func (k StorageCacheKey) String() string {
return fmt.Sprintf("%s:%s", k.TenantID, k.ProviderID)
}
// S3Builder implements Builder interface
type S3Builder struct{}
func (b *S3Builder) Build(ctx context.Context, creds StorageCredentials, config StorageConfig) (StorageClient, error) {
return &S3Client{
endpoint: config.Endpoint,
bucket: config.Bucket,
}, nil
}
func (b *S3Builder) ProviderType() string {
return "s3"
}
type TenantHint string
func main() {
s3Builder := &S3Builder{}
resolver := eddy.NewResolver[StorageClient, StorageCredentials, StorageConfig]()
// Each tenant can have different credentials, same builder
resolver.AddRule(&helpers.MatchHintRule[StorageClient, StorageCredentials, StorageConfig, TenantHint]{
Value: "tenant-A",
Resolver: func(ctx context.Context) (*eddy.ResolvedProvider[StorageClient, StorageCredentials, StorageConfig], error) {
return &eddy.ResolvedProvider[StorageClient, StorageCredentials, StorageConfig]{
Builder: s3Builder,
Output: StorageCredentials{
AccessKey: "tenant-a-key",
SecretKey: "tenant-a-secret",
Region: "us-east-1",
},
Config: StorageConfig{
Bucket: "tenant-a-bucket",
Timeout: 30 * time.Second,
},
}, nil
},
})
resolver.AddRule(&helpers.MatchHintRule[StorageClient, StorageCredentials, StorageConfig, TenantHint]{
Value: "tenant-B",
Resolver: func(ctx context.Context) (*eddy.ResolvedProvider[StorageClient, StorageCredentials, StorageConfig], error) {
return &eddy.ResolvedProvider[StorageClient, StorageCredentials, StorageConfig]{
Builder: s3Builder, // Same builder
Output: StorageCredentials{
AccessKey: "tenant-b-key",
SecretKey: "tenant-b-secret",
Region: "us-west-2",
},
Config: StorageConfig{
Bucket: "tenant-b-bucket",
Timeout: 30 * time.Second,
},
}, nil
},
})
pool := eddy.NewClientPool[StorageClient](1 * time.Hour)
service := eddy.NewClientService[StorageClient, StorageCredentials, StorageConfig](pool)
// Tenant A request
ctxA := context.Background()
ctxA = contextx.WithString(ctxA, TenantHint("tenant-A"))
resultA := resolver.Resolve(ctxA)
if resultA.IsPresent() {
resA := resultA.MustGet()
cacheKeyA := StorageCacheKey{
TenantID: "tenant-A",
ProviderID: resA.Builder.ProviderType(),
}
clientA := service.GetClient(ctxA, cacheKeyA, resA.Builder, resA.Output, resA.Config)
if clientA.IsPresent() {
fmt.Println("tenant-A client created")
}
}
// Tenant B request
ctxB := context.Background()
ctxB = contextx.WithString(ctxB, TenantHint("tenant-B"))
resultB := resolver.Resolve(ctxB)
if resultB.IsPresent() {
resB := resultB.MustGet()
cacheKeyB := StorageCacheKey{
TenantID: "tenant-B",
ProviderID: resB.Builder.ProviderType(),
}
clientB := service.GetClient(ctxB, cacheKeyB, resB.Builder, resB.Output, resB.Config)
if clientB.IsPresent() {
fmt.Println("tenant-B client created")
}
}
}
Output: tenant-A client created tenant-B client created
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Builder ¶
type Builder[T any, Output any, Config any] interface { // Build constructs a client instance using the provided output and config Build(ctx context.Context, output Output, config Config) (T, error) // ProviderType returns the provider type identifier for cache key construction ProviderType() string }
Builder builds client instances with output and configuration
type BuilderFunc ¶
type BuilderFunc[T any, Output any, Config any] struct { // Type is the provider type identifier Type string // Func is the function that builds the client Func func(context.Context, Output, Config) (T, error) }
BuilderFunc is a function adapter for Builder interface Use this when you want to create a Builder from a function without defining a new type
Example:
builder := &BuilderFunc[*s3.Client, S3Credentials, S3Config]{
Type: "s3",
Func: func(ctx context.Context, output S3Credentials, config S3Config) (*s3.Client, error) {
return buildS3Client(ctx, output, config)
},
}
func (*BuilderFunc[T, Output, Config]) Build ¶
func (b *BuilderFunc[T, Output, Config]) Build(ctx context.Context, output Output, config Config) (T, error)
Build implements Builder.Build
func (*BuilderFunc[T, Output, Config]) ProviderType ¶
func (b *BuilderFunc[T, Output, Config]) ProviderType() string
ProviderType implements Builder.ProviderType
type CacheKey ¶
type CacheKey interface {
String() string
}
CacheKey is the interface that cache keys must implement Implementations must be comparable to work as map keys
type ClientEntry ¶
type ClientEntry[T any] struct { // Client is the cached client instance Client T // Expiration is when this cache entry expires Expiration time.Time }
ClientEntry wraps a client instance with expiration metadata
type ClientPool ¶
type ClientPool[T any] struct { // contains filtered or unexported fields }
ClientPool holds cached client instances with TTL expiration
func NewClientPool ¶
func NewClientPool[T any](ttl time.Duration) *ClientPool[T]
NewClientPool creates a new client pool with the specified TTL
func (*ClientPool[T]) CleanExpired ¶
func (p *ClientPool[T]) CleanExpired() int
CleanExpired removes expired clients from the pool and returns the count of removed clients
func (*ClientPool[T]) GetClient ¶
func (p *ClientPool[T]) GetClient(key CacheKey) mo.Option[T]
GetClient retrieves a client from the pool if it exists and hasn't expired
func (*ClientPool[T]) RemoveClient ¶
func (p *ClientPool[T]) RemoveClient(key CacheKey)
RemoveClient removes a client from the pool
func (*ClientPool[T]) SetClient ¶
func (p *ClientPool[T]) SetClient(key CacheKey, client T)
SetClient stores a client in the pool with TTL expiration
type ClientService ¶
type ClientService[T any, Output any, Config any] struct { // contains filtered or unexported fields }
ClientService manages client pooling and provides cached client instances The builder is provided directly by the rule evaluation result rather than via a registry
func NewClientService ¶
func NewClientService[T any, Output any, Config any](pool *ClientPool[T], opts ...ServiceOption[T, Output, Config]) *ClientService[T, Output, Config]
NewClientService creates a new client service with the specified pool
func (*ClientService[T, Output, Config]) GetClient ¶
func (s *ClientService[T, Output, Config]) GetClient(ctx context.Context, key CacheKey, builder Builder[T, Output, Config], output Output, config Config) mo.Option[T]
GetClient retrieves a client from cache or builds a new one using the provided builder The builder is provided directly from the rule evaluation result
func (*ClientService[T, Output, Config]) Pool ¶
func (s *ClientService[T, Output, Config]) Pool() *ClientPool[T]
Pool returns the underlying client pool
type ResolvedProvider ¶
type ResolvedProvider[T any, Output any, Config any] struct { // Builder is the client builder to use Builder Builder[T, Output, Config] // Output contains the credentials or output data needed to build the client Output Output // Config contains the configuration for the client Config Config }
ResolvedProvider represents a resolved provider configuration This is returned by resolver functions
type Resolver ¶
Resolver is a generic struct that handles rule-based resolution
func NewResolver ¶
NewResolver creates a new resolver instance
type Result ¶
type Result[T any, Output any, Config any] struct { // Builder is the client builder to use Builder Builder[T, Output, Config] // Output contains the credentials or output data needed to build the client Output Output // Config contains the configuration for the client Config Config // CacheKey is the key used to cache the built client CacheKey CacheKey }
Result represents the output of rule evaluation
type Rule ¶
type Rule[T any, Output any, Config any] interface { Evaluate(ctx context.Context) mo.Option[Result[T, Output, Config]] }
Rule is a generic interface that evaluates context and returns a result
type RuleBuilder ¶
type RuleBuilder[T any, Output any, Config any] struct { // contains filtered or unexported fields }
RuleBuilder provides a fluent interface for creating resolution rules
func NewRule ¶
func NewRule[T any, Output any, Config any]() *RuleBuilder[T, Output, Config]
NewRule creates a rule builder for static resolution
func (*RuleBuilder[T, Output, Config]) Resolve ¶
func (b *RuleBuilder[T, Output, Config]) Resolve(resolver func(context.Context) (*ResolvedProvider[T, Output, Config], error)) Rule[T, Output, Config]
Resolve creates a rule that uses a function to resolve the provider
func (*RuleBuilder[T, Output, Config]) WhenFunc ¶
func (b *RuleBuilder[T, Output, Config]) WhenFunc(condition func(context.Context) bool) *RuleBuilder[T, Output, Config]
WhenFunc adds a custom condition function
type RuleFunc ¶
type RuleFunc[T any, Output any, Config any] struct { // EvaluateFunc is the function that evaluates the rule EvaluateFunc func(ctx context.Context) mo.Option[Result[T, Output, Config]] }
RuleFunc is a function adapter for Rule interface
type ServiceOption ¶
type ServiceOption[T any, Output any, Config any] func(*ClientService[T, Output, Config])
ServiceOption configures a ClientService
func WithConfigClone ¶
func WithConfigClone[T any, Output any, Config any](cloneFn func(Config) Config) ServiceOption[T, Output, Config]
WithConfigClone sets the config cloning function for defensive copying
func WithOutputClone ¶
func WithOutputClone[T any, Output any, Config any](cloneFn func(Output) Output) ServiceOption[T, Output, Config]
WithOutputClone sets the output cloning function for defensive copying