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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ Each bundle provides:
- Graceful cleanup in `Stop()` method
- Production-ready defaults

### Error Handling (`errors/`)
### Error Handling (`forgeerrors/`)
Domain error patterns and classification utilities for consistent error handling across services.

### Observability (`framework/observability.go`, `framework/logging.go`)
Expand Down Expand Up @@ -230,7 +230,7 @@ forge/
├── config/ # Configuration management (BaseConfig, validation, env detection)
├── health/ # Health check system (registry, status, concurrent execution)
├── bundles/ # Pre-built integrations (postgresql, redis, jwt, etc.)
├── errors/ # Error handling utilities
├── forgeerrors/ # Error handling utilities
├── examples/ # Example service implementations
├── docs/ # GitHub Pages documentation site (Jekyll)
├── testutil/ # Testing utilities (assertions, test configs, zerolog test logger)
Expand Down
52 changes: 26 additions & 26 deletions bundles/configloader/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ package configloader
import (
"context"
"encoding/json"
stderrors "errors"
"errors"
"fmt"
"io"
"net/url"
Expand All @@ -99,7 +99,7 @@ import (
"github.com/rs/zerolog"
"gopkg.in/yaml.v3"

"github.com/datariot/forge/errors"
"github.com/datariot/forge/forgeerrors"
"github.com/datariot/forge/framework"
)

Expand Down Expand Up @@ -154,13 +154,13 @@ func DefaultConfig() Config {
// Validate validates the configuration loader settings.
func (c *Config) Validate() error {
if len(c.ConfigPaths) == 0 {
return stderrors.New("at least one config path must be specified")
return errors.New("at least one config path must be specified")
}

// Validate config paths
for _, path := range c.ConfigPaths {
if path == "" {
return stderrors.New("config path cannot be empty")
return errors.New("config path cannot be empty")
}
if !filepath.IsAbs(path) && !strings.HasPrefix(path, "./") {
return fmt.Errorf("config path must be absolute or relative (starting with ./): %s", path)
Expand Down Expand Up @@ -194,7 +194,7 @@ func (b *Bundle) Name() string {
// Initialize sets up the configuration loader.
func (b *Bundle) Initialize(app *framework.App) error {
if err := b.config.Validate(); err != nil {
return errors.ErrInvalidConfiguration.WithMessage("Configuration loader validation failed").WithCause(err)
return forgeerrors.ErrInvalidConfiguration.WithMessage("Configuration loader validation failed").WithCause(err)
}

if app != nil {
Expand Down Expand Up @@ -283,7 +283,7 @@ func (b *Bundle) initializeWatcher() error {
type Loader struct {
config Config
loadedFrom string
changeCallbacks []func(interface{})
changeCallbacks []func(any)
mu sync.RWMutex
}

Expand All @@ -297,14 +297,14 @@ type LoadResult struct {
}

// Load loads configuration into the provided struct from multiple sources.
func (l *Loader) Load(dest interface{}) (*LoadResult, error) {
func (l *Loader) Load(dest any) (*LoadResult, error) {
if dest == nil {
return nil, stderrors.New("destination cannot be nil")
return nil, errors.New("destination cannot be nil")
}

destValue := reflect.ValueOf(dest)
if destValue.Kind() != reflect.Pointer || destValue.Elem().Kind() != reflect.Struct {
return nil, stderrors.New("destination must be a pointer to a struct")
return nil, errors.New("destination must be a pointer to a struct")
}

result := &LoadResult{
Expand Down Expand Up @@ -365,7 +365,7 @@ func (l *Loader) findConfigFile() (string, error) {
}

// loadFromFile loads configuration from a file based on its extension with security validation.
func (l *Loader) loadFromFile(filename string, dest interface{}) error {
func (l *Loader) loadFromFile(filename string, dest any) error {
// Validate file path for security
if err := l.validateFilePath(filename); err != nil {
return fmt.Errorf("invalid config file path: %w", err)
Expand Down Expand Up @@ -418,15 +418,15 @@ func (l *Loader) loadFromFile(filename string, dest interface{}) error {
}

// loadYAML loads configuration from YAML data.
func (l *Loader) loadYAML(data []byte, dest interface{}) error {
func (l *Loader) loadYAML(data []byte, dest any) error {
if err := yaml.Unmarshal(data, dest); err != nil {
return fmt.Errorf("failed to parse YAML: %w", err)
}
return nil
}

// loadJSON loads configuration from JSON data.
func (l *Loader) loadJSON(data []byte, dest interface{}) error {
func (l *Loader) loadJSON(data []byte, dest any) error {
if err := json.Unmarshal(data, dest); err != nil {
return fmt.Errorf("failed to parse JSON: %w", err)
}
Expand Down Expand Up @@ -573,7 +573,7 @@ func (l *Loader) setFieldValue(field reflect.Value, value string) error {
field.Set(reflect.ValueOf(values))
}
} else {
return stderrors.New("unsupported slice type for field")
return errors.New("unsupported slice type for field")
}
default:
return fmt.Errorf("unsupported field type: %s", field.Kind())
Expand Down Expand Up @@ -701,16 +701,16 @@ func toEnvVarName(fieldName string) string {
}

// OnConfigChange registers a callback for configuration changes (hot reload).
func (l *Loader) OnConfigChange(callback func(interface{})) {
func (l *Loader) OnConfigChange(callback func(any)) {
l.mu.Lock()
defer l.mu.Unlock()
l.changeCallbacks = append(l.changeCallbacks, callback)
}

// StartWatching starts watching configuration files for changes.
func (b *Bundle) StartWatching(ctx context.Context, dest interface{}) error {
func (b *Bundle) StartWatching(ctx context.Context, dest any) error {
if b.watcher == nil {
return stderrors.New("file watcher not initialized")
return errors.New("file watcher not initialized")
}

go func() {
Expand All @@ -731,7 +731,7 @@ func (b *Bundle) StartWatching(ctx context.Context, dest interface{}) error {

// Notify callbacks
b.loader.mu.RLock()
callbacks := make([]func(interface{}), len(b.loader.changeCallbacks))
callbacks := make([]func(any), len(b.loader.changeCallbacks))
copy(callbacks, b.loader.changeCallbacks)
b.loader.mu.RUnlock()

Expand All @@ -755,12 +755,12 @@ func (b *Bundle) StartWatching(ctx context.Context, dest interface{}) error {
return nil
}

// GetConfigInfo returns information about the loaded configuration.
func (l *Loader) GetConfigInfo() map[string]interface{} {
// ConfigInfo returns information about the loaded configuration.
func (l *Loader) ConfigInfo() map[string]any {
l.mu.RLock()
defer l.mu.RUnlock()

return map[string]interface{}{
return map[string]any{
"loaded_from": l.loadedFrom,
"config_paths": l.config.ConfigPaths,
"env_prefix": l.config.EnvPrefix,
Expand All @@ -772,7 +772,7 @@ func (l *Loader) GetConfigInfo() map[string]interface{} {
}

// LoadFromString loads configuration from a string (useful for testing).
func (l *Loader) LoadFromString(data, format string, dest interface{}) error {
func (l *Loader) LoadFromString(data, format string, dest any) error {
switch strings.ToLower(format) {
case "yaml", "yml":
return l.loadYAML([]byte(data), dest)
Expand All @@ -784,7 +784,7 @@ func (l *Loader) LoadFromString(data, format string, dest interface{}) error {
}

// MustLoad loads configuration and panics on error (useful for application startup).
func (l *Loader) MustLoad(dest interface{}) *LoadResult {
func (l *Loader) MustLoad(dest any) *LoadResult {
result, err := l.Load(dest)
if err != nil {
panic(fmt.Sprintf("Configuration loading failed: %v", err))
Expand All @@ -793,7 +793,7 @@ func (l *Loader) MustLoad(dest interface{}) *LoadResult {
}

// Reload reloads configuration from all sources.
func (l *Loader) Reload(dest interface{}) (*LoadResult, error) {
func (l *Loader) Reload(dest any) (*LoadResult, error) {
return l.Load(dest) // Load method already handles all sources
}

Expand All @@ -802,14 +802,14 @@ func (l *Loader) Reload(dest interface{}) (*LoadResult, error) {
// Paths containing ".." traversal sequences are rejected regardless of resolution outcome.
func (l *Loader) validateFilePath(filename string) error {
if filename == "" {
return stderrors.New("configuration file path cannot be empty")
return errors.New("configuration file path cannot be empty")
}

// Reject inputs that contain path traversal sequences before any resolution.
// filepath.Clean / filepath.Abs would silently resolve "../../../etc/passwd" to
// a valid absolute path, making it look legitimate. We block such inputs explicitly.
if strings.Contains(filepath.Clean(filename), "..") {
return stderrors.New("path traversal not allowed in configuration file path")
return errors.New("path traversal not allowed in configuration file path")
}

// Resolve relative paths to absolute
Expand Down Expand Up @@ -858,7 +858,7 @@ func LoadConfig[T any](configPaths ...string) (*T, *LoadResult, error) {
return &cfg, result, nil
}

// MustLoadConfig is a convenience function that panics on configuration loading errors.
// MustLoadConfig is a convenience function that panics on configuration loading forgeerrors.
func MustLoadConfig[T any](configPaths ...string) (*T, *LoadResult) {
cfg, result, err := LoadConfig[T](configPaths...)
if err != nil {
Expand Down
12 changes: 6 additions & 6 deletions bundles/configloader/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,10 +409,10 @@ func TestLoaderLoadFromString_UnsupportedFormat(t *testing.T) {
}
}

// TestLoaderGetConfigInfo tests the GetConfigInfo method.
func TestLoaderGetConfigInfo(t *testing.T) {
// TestLoaderConfigInfo tests the ConfigInfo method.
func TestLoaderConfigInfo(t *testing.T) {
l := loaderWithConfig(DefaultConfig())
info := l.GetConfigInfo()
info := l.ConfigInfo()

if info["config_paths"] == nil {
t.Error("expected config_paths in info")
Expand All @@ -426,7 +426,7 @@ func TestLoaderGetConfigInfo(t *testing.T) {
func TestLoaderOnConfigChange(t *testing.T) {
l := loaderWithConfig(DefaultConfig())
called := 0
l.OnConfigChange(func(cfg interface{}) {
l.OnConfigChange(func(cfg any) {
called++
})
if len(l.changeCallbacks) != 1 {
Expand Down Expand Up @@ -721,7 +721,7 @@ func TestLoadFromFile_RelativePath(t *testing.T) {

l := loaderWithConfig(DefaultConfig())

var dest map[string]interface{}
var dest map[string]any
if err := l.loadFromFile(relPath, &dest); err != nil {
t.Errorf("loadFromFile with relative path failed: %v", err)
}
Expand Down Expand Up @@ -790,7 +790,7 @@ func TestMustLoadConfig_Panics(t *testing.T) {
}()
// Force a config with a required file that doesn't exist
// Use a path that definitely doesn't exist
MustLoadConfig[map[string]interface{}]("/nonexistent/path/that/does/not/exist.yaml")
MustLoadConfig[map[string]any]("/nonexistent/path/that/does/not/exist.yaml")
}

func TestLooksLikeSensitiveData_JWTToken(t *testing.T) {
Expand Down
Loading
Loading