Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ package waitfor

import "errors"

// ErrWait is returned when resource availability testing fails.
// This error indicates that one or more resources did not become
// available within the configured timeout and retry parameters.
var (
ErrWait = errors.New("failed to wait for resource availability")
// ErrInvalidArgument is returned when invalid arguments are passed
// to functions, such as empty resource URLs or invalid configuration.
ErrInvalidArgument = errors.New("invalid argument")
)
24 changes: 24 additions & 0 deletions helpers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
package waitfor

// Module is a function type that returns resource configuration information.
// It provides a way for resource plugins to declare their supported URL schemes
// and factory function. This enables a plugin-like architecture where resource
// types can be developed and distributed independently.
//
// The function should return:
// - A slice of URL schemes the module supports (e.g., []string{"http", "https"})
// - A ResourceFactory function that can create resource instances from URLs
//
// Example:
//
// func httpModule() ([]string, ResourceFactory) {
// return []string{"http", "https"}, httpResourceFactory
// }
type Module func() ([]string, ResourceFactory)

// Use converts a Module function into a ResourceConfig that can be used
// with New() to register resource types. This provides a convenient way
// to integrate resource plugins into a waitfor Runner.
//
// Example:
//
// runner := waitfor.New(
// waitfor.Use(httpModule),
// waitfor.Use(postgresModule),
// )
func Use(mod Module) ResourceConfig {
scheme, factory := mod()

Expand Down
16 changes: 8 additions & 8 deletions helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ func TestUse(t *testing.T) {
}
return schemes, factory
}

// Test Use function
config := Use(mockModule)

assert.Equal(t, []string{"mock", "test"}, config.Scheme)
assert.NotNil(t, config.Factory)

// Test that the factory works
testURL, _ := url.Parse("mock://example")
resource, err := config.Factory(testURL)
Expand All @@ -39,9 +39,9 @@ func TestUse_WithEmptySchemes(t *testing.T) {
}
return schemes, factory
}

config := Use(mockModule)

assert.Empty(t, config.Scheme)
assert.NotNil(t, config.Factory)
}
Expand All @@ -52,9 +52,9 @@ func TestUse_WithNilFactory(t *testing.T) {
schemes := []string{"nil"}
return schemes, nil
}

config := Use(mockModule)

assert.Equal(t, []string{"nil"}, config.Scheme)
assert.Nil(t, config.Factory)
}
}
42 changes: 35 additions & 7 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,26 @@ import (
)

type (
// Options contains configuration parameters for resource testing behavior.
// These options control retry intervals, maximum wait times, and the number
// of attempts made when testing resource availability.
Options struct {
interval time.Duration
maxInterval time.Duration
attempts uint64
interval time.Duration // Initial retry interval between attempts
maxInterval time.Duration // Maximum interval for exponential backoff
attempts uint64 // Maximum number of retry attempts
}

// Option is a function type used to configure Options through the functional
// options pattern. This allows flexible and extensible configuration of
// resource testing behavior.
Option func(opts *Options)
)

// Create new options
// newOptions creates a new Options instance with default values and applies
// the provided option setters. Default values are:
// - interval: 5 seconds
// - maxInterval: 60 seconds
// - attempts: 5.
func newOptions(setters []Option) *Options {
opts := &Options{
interval: time.Duration(5) * time.Second,
Expand All @@ -29,21 +39,39 @@ func newOptions(setters []Option) *Options {
return opts
}

// Set a custom test interval
// WithInterval creates an Option that sets the initial retry interval in seconds.
// This interval is used as the starting point for exponential backoff between
// retry attempts. The actual interval will increase exponentially up to maxInterval.
//
// Example:
//
// runner.Test(ctx, resources, waitfor.WithInterval(2)) // Start with 2 second intervals
func WithInterval(interval uint64) Option {
return func(opts *Options) {
opts.interval = time.Duration(interval) * time.Second
}
}

// Set a custom maximum test interval
// WithMaxInterval creates an Option that sets the maximum retry interval in seconds.
// When using exponential backoff, the retry interval will not exceed this value.
// This prevents excessively long waits between retry attempts.
//
// Example:
//
// runner.Test(ctx, resources, waitfor.WithMaxInterval(30)) // Cap at 30 seconds
func WithMaxInterval(interval uint64) Option {
return func(opts *Options) {
opts.maxInterval = time.Duration(interval) * time.Second
}
}

// Set a custom attempts count
// WithAttempts creates an Option that sets the maximum number of retry attempts.
// If a resource test fails this many times, the resource is considered unavailable.
// Set to 0 for unlimited attempts (not recommended without context timeout).
//
// Example:
//
// runner.Test(ctx, resources, waitfor.WithAttempts(10)) // Try up to 10 times
func WithAttempts(attempts uint64) Option {
return func(opts *Options) {
opts.attempts = attempts
Expand Down
22 changes: 11 additions & 11 deletions options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

func TestNewOptions_Defaults(t *testing.T) {
opts := newOptions([]Option{})

assert.Equal(t, time.Duration(5)*time.Second, opts.interval)
assert.Equal(t, time.Duration(60)*time.Second, opts.maxInterval)
assert.Equal(t, uint64(5), opts.attempts)
Expand All @@ -21,9 +21,9 @@ func TestNewOptions_WithSetters(t *testing.T) {
WithMaxInterval(120),
WithAttempts(15),
}

opts := newOptions(setters)

assert.Equal(t, time.Duration(10)*time.Second, opts.interval)
assert.Equal(t, time.Duration(120)*time.Second, opts.maxInterval)
assert.Equal(t, uint64(15), opts.attempts)
Expand All @@ -32,27 +32,27 @@ func TestNewOptions_WithSetters(t *testing.T) {
func TestWithInterval(t *testing.T) {
option := WithInterval(30)
opts := &Options{}

option(opts)

assert.Equal(t, time.Duration(30)*time.Second, opts.interval)
}

func TestWithMaxInterval(t *testing.T) {
option := WithMaxInterval(90)
opts := &Options{}

option(opts)

assert.Equal(t, time.Duration(90)*time.Second, opts.maxInterval)
}

func TestWithAttempts(t *testing.T) {
option := WithAttempts(20)
opts := &Options{}

option(opts)

assert.Equal(t, uint64(20), opts.attempts)
}

Expand All @@ -62,8 +62,8 @@ func TestCombinedOptions(t *testing.T) {
WithMaxInterval(30),
WithAttempts(8),
})

assert.Equal(t, time.Duration(2)*time.Second, opts.interval)
assert.Equal(t, time.Duration(30)*time.Second, opts.maxInterval)
assert.Equal(t, uint64(8), opts.attempts)
}
}
64 changes: 59 additions & 5 deletions registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,55 @@ import (
)

type (
// ResourceFactory is a function type that creates Resource instances from URLs.
// Each factory is responsible for parsing the URL and creating the appropriate
// resource implementation that can test the specific resource type.
//
// Example:
//
// func httpResourceFactory(u *url.URL) (Resource, error) {
// return &HTTPResource{url: u}, nil
// }
ResourceFactory func(u *url.URL) (Resource, error)

// ResourceConfig defines the configuration for a resource type, mapping
// URL schemes to their corresponding factory functions. This allows
// the system to support multiple resource types through a plugin-like architecture.
//
// Example:
//
// config := ResourceConfig{
// Scheme: []string{"http", "https"},
// Factory: httpResourceFactory,
// }
ResourceConfig struct {
Scheme []string
Factory ResourceFactory
Scheme []string // URL schemes this config handles (e.g., "http", "postgres")
Factory ResourceFactory // Factory function to create resource instances
}

// Resource defines the interface that all resource types must implement.
// The Test method should verify that the resource is available and ready,
// returning an error if the resource is not accessible or not ready.
//
// Implementations should be context-aware and respect cancellation signals.
Resource interface {
// Test verifies that the resource is available and ready for use.
// Should return nil if the resource is ready, or an error describing
// why the resource is not available.
Test(ctx context.Context) error
}

// Registry manages the mapping between URL schemes and their corresponding
// resource factories. It provides methods to register new resource types
// and resolve URLs to resource instances.
Registry struct {
resources map[string]ResourceFactory
}
)

// newRegistry creates a new Registry instance and populates it with the provided
// resource configurations. Each configuration maps one or more URL schemes to
// their corresponding factory functions.
func newRegistry(configs []ResourceConfig) *Registry {
resources := make(map[string]ResourceFactory)

Expand All @@ -36,7 +69,16 @@ func newRegistry(configs []ResourceConfig) *Registry {
return &Registry{resources}
}

// Register adds a resource factory to the registry
// Register adds a resource factory to the registry for the specified URL scheme.
// The scheme is automatically trimmed of whitespace. Returns an error if a
// factory is already registered for the given scheme.
//
// Example:
//
// err := registry.Register("custom", myResourceFactory)
// if err != nil {
// // Handle registration conflict
// }
func (r *Registry) Register(scheme string, factory ResourceFactory) error {
scheme = strings.TrimSpace(scheme)
_, exists := r.resources[scheme]
Expand All @@ -50,7 +92,17 @@ func (r *Registry) Register(scheme string, factory ResourceFactory) error {
return nil
}

// Resolve returns a resource instance by a given url
// Resolve parses the location URL and creates a Resource instance using the
// appropriate factory for the URL's scheme. Returns an error if the URL
// cannot be parsed or if no factory is registered for the scheme.
//
// Example:
//
// resource, err := registry.Resolve("postgres://user:pass@localhost:5432/db")
// if err != nil {
// // Handle resolution error
// }
// err = resource.Test(ctx)
func (r *Registry) Resolve(location string) (Resource, error) {
u, err := url.Parse(location)

Expand All @@ -67,7 +119,9 @@ func (r *Registry) Resolve(location string) (Resource, error) {
return rf(u)
}

// List returns a list of schemes of registered resources
// List returns a slice containing all registered URL schemes.
// The order of schemes in the returned slice is not guaranteed.
// This can be useful for debugging or displaying available resource types.
func (r *Registry) List() []string {
list := make([]string, 0, len(r.resources))

Expand Down
Loading
Loading