viper

package module
v0.0.0-...-fde3b76 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 10, 2026 License: MIT Imports: 10 Imported by: 0

README

Viper Configuration Adapter for Hyperion

Production-ready Viper adapter providing configuration management with hot-reload capabilities for Hyperion framework.

Features

  • Multiple Formats: YAML, JSON, TOML, HCL, INI, ENV support
  • Environment Variables: Automatic environment variable binding
  • Hot Reload: Watch configuration files for changes
  • Nested Configuration: Access nested keys with dot notation
  • Type Safety: Type-safe configuration access methods
  • Default Values: Graceful fallback to defaults
  • Zero Dependencies: Clean integration with Hyperion interfaces

Installation

go get github.com/mapoio/hyperion/adapter/viper

Quick Start

1. Create Configuration File

Create a config.yaml file:

app:
  name: myapp
  version: 1.0.0
  debug: true

server:
  host: 0.0.0.0
  port: 8080
  timeout: 30s

log:
  level: info
  encoding: json
  output: stdout

database:
  driver: postgres
  host: localhost
  port: 5432
  database: mydb
2. Initialize Application
package main

import (
    "go.uber.org/fx"
    viperadapter "github.com/mapoio/hyperion/adapter/viper"
    "github.com/mapoio/hyperion"
)

func main() {
    app := fx.New(
        hyperion.CoreModule,  // Core infrastructure (required)
        viperadapter.Module,  // Config provider
        fx.Invoke(run),
    )
    app.Run()
}

func run(cfg hyperion.Config) {
    // Configuration is ready to use
    appName := cfg.GetString("app.name")
    port := cfg.GetInt("server.port")
}
3. Use Configuration
Basic Access
type AppService struct {
    cfg hyperion.Config
}

func (s *AppService) GetSettings() {
    // String values
    name := s.cfg.GetString("app.name")
    host := s.cfg.GetString("server.host")

    // Integer values
    port := s.cfg.GetInt("server.port")

    // Boolean values
    debug := s.cfg.GetBool("app.debug")

    // Check if key exists
    if s.cfg.IsSet("database.driver") {
        driver := s.cfg.GetString("database.driver")
    }
}
Unmarshal to Struct
type ServerConfig struct {
    Host    string        `mapstructure:"host"`
    Port    int           `mapstructure:"port"`
    Timeout time.Duration `mapstructure:"timeout"`
}

type AppConfig struct {
    Name    string `mapstructure:"name"`
    Version string `mapstructure:"version"`
    Debug   bool   `mapstructure:"debug"`
}

func (s *AppService) LoadConfig() error {
    var serverCfg ServerConfig
    if err := s.cfg.Unmarshal("server", &serverCfg); err != nil {
        return fmt.Errorf("failed to unmarshal server config: %w", err)
    }

    var appCfg AppConfig
    if err := s.cfg.Unmarshal("app", &appCfg); err != nil {
        return fmt.Errorf("failed to unmarshal app config: %w", err)
    }

    return nil
}
Hot Reload
func (s *AppService) WatchConfig() {
    s.cfg.Watch(func(cfg hyperion.Config) {
        log.Println("Configuration changed, reloading...")

        // Reload settings
        newPort := cfg.GetInt("server.port")
        if newPort != s.currentPort {
            s.restartServer(newPort)
        }
    })
}

Configuration Structure

# Application settings
app:
  name: string
  version: string
  debug: bool
  environment: string  # development, staging, production

# Server settings
server:
  host: string
  port: int
  timeout: duration
  read_timeout: duration
  write_timeout: duration

# Logging
log:
  level: string       # debug, info, warn, error
  encoding: string    # json, console
  output: string      # stdout, stderr, file path

# Database
database:
  driver: string      # postgres, mysql, sqlite
  host: string
  port: int
  username: string
  password: string
  database: string

# Cache
cache:
  type: string        # memory, redis
  max_size: int
  ttl: duration

# Tracing
tracing:
  enabled: bool
  endpoint: string
  sample_rate: float

Environment Variables

Automatic Binding

Environment variables are automatically bound with HYPERION_ prefix:

export HYPERION_APP_NAME="myapp"
export HYPERION_SERVER_PORT=8080
export HYPERION_LOG_LEVEL="debug"

Access in code:

name := cfg.GetString("app.name")      // Gets HYPERION_APP_NAME
port := cfg.GetInt("server.port")      // Gets HYPERION_SERVER_PORT
level := cfg.GetString("log.level")    // Gets HYPERION_LOG_LEVEL
Override Precedence

Configuration values are resolved in the following order (highest to lowest):

  1. Explicit Set - Values set programmatically
  2. Environment Variables - HYPERION_* variables
  3. Configuration File - Values from config.yaml
  4. Default Values - Fallback defaults

Advanced Usage

Multiple Configuration Files
package main

import (
    "go.uber.org/fx"
    viperadapter "github.com/mapoio/hyperion/adapter/viper"
)

func main() {
    app := fx.New(
        fx.Provide(
            fx.Annotate(
                func() (hyperion.Config, error) {
                    return viperadapter.NewProvider(
                        viperadapter.WithConfigFiles("config.yaml", "local.yaml"),
                        viperadapter.WithEnvPrefix("MYAPP"),
                    )
                },
                fx.As(new(hyperion.Config)),
            ),
        ),
        fx.Invoke(run),
    )
    app.Run()
}
Configuration Validation
type DatabaseConfig struct {
    Driver   string `mapstructure:"driver" validate:"required,oneof=postgres mysql sqlite"`
    Host     string `mapstructure:"host" validate:"required"`
    Port     int    `mapstructure:"port" validate:"required,min=1,max=65535"`
    Database string `mapstructure:"database" validate:"required"`
}

func ValidateConfig(cfg hyperion.Config) error {
    var dbCfg DatabaseConfig
    if err := cfg.Unmarshal("database", &dbCfg); err != nil {
        return err
    }

    validate := validator.New()
    return validate.Struct(dbCfg)
}

Best Practices

1. Use Struct Unmarshaling
// ✅ Good - Type-safe, documented
type Config struct {
    Database DatabaseConfig `mapstructure:"database"`
    Server   ServerConfig   `mapstructure:"server"`
}

var cfg Config
if err := config.Unmarshal("", &cfg); err != nil {
    log.Fatal(err)
}

// ❌ Avoid - Error-prone, no type safety
host := config.GetString("database.host")
port := config.GetInt("database.port")
2. Validate Early
func NewService(cfg hyperion.Config) (*Service, error) {
    var config ServiceConfig
    if err := cfg.Unmarshal("service", &config); err != nil {
        return nil, fmt.Errorf("invalid configuration: %w", err)
    }

    if err := config.Validate(); err != nil {
        return nil, fmt.Errorf("configuration validation failed: %w", err)
    }

    return &Service{config: config}, nil
}
3. Use Sensible Defaults
type Config struct {
    Host    string        `mapstructure:"host"`
    Port    int           `mapstructure:"port"`
    Timeout time.Duration `mapstructure:"timeout"`
}

func (c *Config) SetDefaults() {
    if c.Host == "" {
        c.Host = "localhost"
    }
    if c.Port == 0 {
        c.Port = 8080
    }
    if c.Timeout == 0 {
        c.Timeout = 30 * time.Second
    }
}
4. Document Configuration
// DatabaseConfig holds database connection settings.
type DatabaseConfig struct {
    // Driver specifies the database driver (postgres, mysql, sqlite).
    Driver string `mapstructure:"driver"`

    // Host is the database server hostname or IP address.
    Host string `mapstructure:"host"`

    // Port is the database server port (default: 5432 for postgres).
    Port int `mapstructure:"port"`
}

Testing

Mock Configuration
type mockConfig struct {
    data map[string]any
}

func (m *mockConfig) GetString(key string) string {
    if v, ok := m.data[key].(string); ok {
        return v
    }
    return ""
}

func (m *mockConfig) GetInt(key string) int {
    if v, ok := m.data[key].(int); ok {
        return v
    }
    return 0
}

// Use in tests
func TestService(t *testing.T) {
    cfg := &mockConfig{
        data: map[string]any{
            "app.name": "test",
            "server.port": 8080,
        },
    }

    service := NewService(cfg)
    // Test service
}

Troubleshooting

Configuration Not Loading

Problem: Configuration values are empty or default

Solutions:

  1. Check file path is correct (relative to working directory)
  2. Verify YAML/JSON syntax is valid
  3. Check environment variable names (use HYPERION_ prefix)
  4. Enable debug logging to see what Viper is loading
Environment Variables Not Working

Problem: Environment variables are not overriding config file

Solutions:

  1. Ensure variable names use HYPERION_ prefix
  2. Use uppercase with underscores: HYPERION_APP_NAME
  3. Nested keys use underscores: HYPERION_DATABASE_HOST
Hot Reload Not Triggering

Problem: Configuration changes not detected

Solutions:

  1. Check file permissions (Viper needs read access)
  2. Verify Watch() is called after file is loaded
  3. On some systems, atomic writes (mv) may not trigger fsnotify
  4. Try using symlinks or direct file edits

Performance

  • Memory: ~1-2 MB overhead for typical configuration
  • Startup: < 10ms to load and parse configuration
  • Watch: Minimal CPU usage (<1%) when watching files
  • Access: O(1) for key lookups using internal map

Integration Examples

With Zap Logger
app := fx.New(
    hyperion.CoreModule,  // Core infrastructure (required)
    viperadapter.Module,
    zapadapter.Module,  // Reads log.* from config
    fx.Invoke(func(logger hyperion.Logger, cfg hyperion.Config) {
        logger.Info("application started",
            "name", cfg.GetString("app.name"),
            "version", cfg.GetString("app.version"),
        )
    }),
)
With GORM Database
app := fx.New(
    hyperion.CoreModule,  // Core infrastructure (required)
    viperadapter.Module,
    gormadapter.Module,  // Reads database.* from config
    fx.Invoke(func(db hyperion.Database, cfg hyperion.Config) {
        // Database configured automatically
    }),
)

License

Same as Hyperion framework.

Contributing

See main Hyperion repository for contribution guidelines.

Support

Documentation

Index

Constants

View Source
const (
	// DefaultConfigPath is the default path to configuration file
	DefaultConfigPath = "configs/config.yaml"
)

Configuration-related constants

Variables

View Source
var (
	// ErrNoViperProvided is returned when FromViper is called with nil *viper.Viper.
	ErrNoViperProvided = errors.New("viper: no *viper.Viper provided")

	// ErrNoConfigPathProvided is returned when FromViperWithPath is called with empty config path.
	ErrNoConfigPathProvided = errors.New("viper: no config path provided")
)

Error variables for Viper adapter.

View Source
var AutoModule = hyperion.WrapAdapter(
	fx.Module("hyperion.adapter.viper.auto",
		fx.Provide(
			fx.Annotate(
				NewAutoConfig,
				fx.As(new(hyperion.Config)),
				fx.As(new(hyperion.ConfigWatcher)),
			),
		),
	),
	"config",
)

AutoModule provides Level 1 (Zero-Config) API with capability declaration. It auto-configures Viper Config from a predefined config file path.

Usage:

hyperion.NewApp(
    viper.AutoModule,    // Auto-configured from configs/config.yaml
    zap.Module,
    gorm.Module,
    myapp.Module,
)

Default configuration file: configs/config.yaml

View Source
var Module = AutoModule

Module provides Viper-based Config implementation with capability declaration. This is the default module using Level 1 (Zero-Config) API.

Usage:

hyperion.NewApp(
    viper.Module,  // Provides Config - auto-detected capability
    myapp.Module,
).Run()

Default configuration file: configs/config.yaml

For Level 2 (Configuration-Driven) API, use WithOptions() instead. For Level 3 (Custom Provider) API, use FromViper() or FromViperWithPath() instead.

Functions

func FromViper

func FromViper(v *viper.Viper) fx.Option

FromViper provides Level 3 (Custom Provider) API. It accepts a user-provided *viper.Viper instance.

This allows applications to fully control Viper initialization and version, while still using Hyperion's Config and ConfigWatcher abstractions.

IMPORTANT: When using this function, the caller is responsible for managing the Viper instance lifecycle.

Usage:

import "github.com/spf13/viper"

v := viper.New()
v.SetConfigFile("my-config.yaml")
v.ReadInConfig()

// Add custom settings
v.Set("custom.key", "value")
v.SetDefault("app.name", "my-app")

hyperion.NewApp(
    viper.FromViper(v),
    myapp.Module,
)

func FromViperWithPath

func FromViperWithPath(v *viper.Viper, configPath string) fx.Option

FromViperWithPath provides Level 3 API with explicit config file path. This variant enables hot reload support by tracking the config file.

Usage:

v := viper.New()
v.SetConfigFile("configs/app.yaml")
v.ReadInConfig()

hyperion.NewApp(
    viper.FromViperWithPath(v, "configs/app.yaml"),
    myapp.Module,
)

func NewAutoConfig

func NewAutoConfig() (hyperion.ConfigWatcher, error)

NewAutoConfig creates a Config from auto-configuration. It uses the default config file path: configs/config.yaml

func NewConfigFromOptions

func NewConfigFromOptions(opts Options) (hyperion.ConfigWatcher, error)

NewConfigFromOptions creates a Config from Options.

func NewConfigFromViper

func NewConfigFromViper(v *viper.Viper) hyperion.ConfigWatcher

NewConfigFromViper creates a Config from an existing *viper.Viper. Note: This variant does NOT support hot reload (Watch) since we don't know the config file path. Use FromViperWithPath if you need hot reload.

func NewProvider

func NewProvider(configPath string) (hyperion.ConfigWatcher, error)

NewProvider creates a new viper-based config provider from the given configuration file path. It automatically detects the file format based on the file extension.

Supported formats: .yaml, .yml, .json, .toml

Environment variables are automatically mapped to configuration keys. For example, APP_DATABASE_HOST maps to the "database.host" config key.

Returns an error if the configuration file cannot be read or parsed.

func NewViperProvider deprecated

func NewViperProvider() (hyperion.ConfigWatcher, error)

Deprecated: Use AutoModule, WithOptions(), or FromViper() instead. This function is kept for backward compatibility.

func WithOptions

func WithOptions(opts Options) fx.Option

WithOptions provides Level 2 (Configuration-Driven) API. It creates Config from programmatic Options.

Usage:

opts, err := viper.NewOptionsBuilder().
    WithConfigFile("configs/app.yaml").
    WithEnvPrefix("MYAPP").
    ForProduction().
    Build()
if err != nil {
    panic(err)
}

hyperion.NewApp(
    viper.WithOptions(opts),
    myapp.Module,
)

Types

type Options

type Options struct {
	// Config file configuration
	ConfigFile  string   // Path to config file (e.g., "configs/config.yaml")
	ConfigName  string   // Config file name without extension (e.g., "config")
	ConfigType  string   // Config file type (yaml, json, toml, etc.)
	ConfigPaths []string // Directories to search for config file

	// Environment variable configuration
	EnvPrefix    string // Prefix for environment variables (e.g., "APP")
	AutomaticEnv *bool  // Enable automatic environment variable override (use pointer to allow explicit false)

	// Advanced: Remote configuration
	RemoteProvider string            // Remote config provider (etcd, consul)
	RemoteEndpoint string            // Remote provider endpoint
	RemotePath     string            // Path in remote provider
	RemoteConfig   map[string]string // Additional remote config options
}

Options provides programmatic configuration for the Viper adapter. This is used for Level 2 (Configuration-Driven) API with Builder pattern.

Example usage:

opts := viper.NewOptionsBuilder().
    WithConfigFile("configs/config.yaml").
    WithEnvPrefix("APP").
    WithAutomaticEnv(true).
    Build()

hyperion.NewApp(
    viper.WithOptions(opts),
    myapp.Module,
)

func DefaultOptions

func DefaultOptions() Options

DefaultOptions returns sensible default options for local development.

func (Options) Defaults

func (o Options) Defaults() hyperion.AdapterOptions

Defaults returns the default options for Viper adapter.

func (Options) Merge

Merge combines user-provided options with default options. User options take precedence over defaults.

func (Options) Validate

func (o Options) Validate() error

Validate checks if the options are valid.

type OptionsBuilder

type OptionsBuilder struct {
	// contains filtered or unexported fields
}

OptionsBuilder provides a fluent API for building Viper Options. It implements the builder pattern for better ergonomics.

Example usage:

opts, err := viper.NewOptionsBuilder().
    WithConfigFile("configs/app.yaml").
    WithEnvPrefix("MYAPP").
    WithAutomaticEnv(true).
    Build()

func NewOptionsBuilder

func NewOptionsBuilder() *OptionsBuilder

NewOptionsBuilder creates a new OptionsBuilder with default options.

func (*OptionsBuilder) AddConfigPath

func (b *OptionsBuilder) AddConfigPath(path string) *OptionsBuilder

AddConfigPath adds a directory to search for config files. This appends to the existing paths instead of replacing them.

func (*OptionsBuilder) Build

func (b *OptionsBuilder) Build() (Options, error)

Build finalizes the builder and returns the Options instance. It validates the options before returning.

func (*OptionsBuilder) ForConsul

func (b *OptionsBuilder) ForConsul(endpoint, path string) *OptionsBuilder

ForConsul configures Viper to use Consul as the remote config source.

func (*OptionsBuilder) ForDevelopment

func (b *OptionsBuilder) ForDevelopment() *OptionsBuilder

ForDevelopment applies recommended settings for development environment.

func (*OptionsBuilder) ForEtcd

func (b *OptionsBuilder) ForEtcd(endpoint, path string) *OptionsBuilder

ForEtcd configures Viper to use etcd as the remote config source.

func (*OptionsBuilder) ForProduction

func (b *OptionsBuilder) ForProduction() *OptionsBuilder

ForProduction applies recommended settings for production environment.

func (*OptionsBuilder) ForTesting

func (b *OptionsBuilder) ForTesting() *OptionsBuilder

ForTesting applies recommended settings for testing environment.

func (*OptionsBuilder) WithAutomaticEnv

func (b *OptionsBuilder) WithAutomaticEnv(enable bool) *OptionsBuilder

WithAutomaticEnv enables or disables automatic environment variable override. When enabled, environment variables will automatically override config values.

func (*OptionsBuilder) WithConfigFile

func (b *OptionsBuilder) WithConfigFile(path string) *OptionsBuilder

WithConfigFile sets the complete path to the config file. This is the most straightforward way to specify configuration. Example: "configs/config.yaml", "/etc/myapp/config.json"

func (*OptionsBuilder) WithConfigName

func (b *OptionsBuilder) WithConfigName(name string) *OptionsBuilder

WithConfigName sets the config file name (without extension). Viper will search for this file in the configured paths. Requires ConfigType to be set.

func (*OptionsBuilder) WithConfigPaths

func (b *OptionsBuilder) WithConfigPaths(paths ...string) *OptionsBuilder

WithConfigPaths sets the directories to search for config files. Only used when ConfigName is set (not ConfigFile).

func (*OptionsBuilder) WithConfigType

func (b *OptionsBuilder) WithConfigType(configType string) *OptionsBuilder

WithConfigType sets the config file type/format. Valid values: yaml, json, toml, hcl, env, ini, properties.

func (*OptionsBuilder) WithEnvPrefix

func (b *OptionsBuilder) WithEnvPrefix(prefix string) *OptionsBuilder

WithEnvPrefix sets the prefix for environment variables. For example, with prefix "APP", the env var "APP_DATABASE_HOST" will map to the config key "database.host".

func (*OptionsBuilder) WithJSON

func (b *OptionsBuilder) WithJSON(name string) *OptionsBuilder

WithJSON is a shortcut for setting JSON configuration.

func (*OptionsBuilder) WithRemoteConfig

func (b *OptionsBuilder) WithRemoteConfig(config map[string]string) *OptionsBuilder

WithRemoteConfig sets additional remote configuration options.

func (*OptionsBuilder) WithRemoteProvider

func (b *OptionsBuilder) WithRemoteProvider(provider, endpoint, path string) *OptionsBuilder

WithRemoteProvider configures a remote configuration provider. Supported providers: etcd, etcd3, consul, firestore.

func (*OptionsBuilder) WithTOML

func (b *OptionsBuilder) WithTOML(name string) *OptionsBuilder

WithTOML is a shortcut for setting TOML configuration.

func (*OptionsBuilder) WithYAML

func (b *OptionsBuilder) WithYAML(name string) *OptionsBuilder

WithYAML is a shortcut for setting YAML configuration.

type Provider

type Provider struct {
	// contains filtered or unexported fields
}

Provider is a hyperion.ConfigWatcher implementation based on spf13/viper. It supports multiple configuration formats (YAML, JSON, TOML) and automatic environment variable override with hot reload capabilities.

func (*Provider) AllKeys

func (p *Provider) AllKeys() []string

AllKeys returns all keys in the configuration.

func (*Provider) Get

func (p *Provider) Get(key string) any

Get returns the value for the given key as an interface{}.

func (*Provider) GetBool

func (p *Provider) GetBool(key string) bool

GetBool returns the value for the given key as a bool.

func (*Provider) GetFloat64

func (p *Provider) GetFloat64(key string) float64

GetFloat64 returns the value for the given key as a float64.

func (*Provider) GetInt

func (p *Provider) GetInt(key string) int

GetInt returns the value for the given key as an int.

func (*Provider) GetInt64

func (p *Provider) GetInt64(key string) int64

GetInt64 returns the value for the given key as an int64.

func (*Provider) GetString

func (p *Provider) GetString(key string) string

GetString returns the value for the given key as a string.

func (*Provider) GetStringSlice

func (p *Provider) GetStringSlice(key string) []string

GetStringSlice returns the value for the given key as a string slice.

func (*Provider) IsSet

func (p *Provider) IsSet(key string) bool

IsSet checks if the key is set in the configuration.

func (*Provider) Unmarshal

func (p *Provider) Unmarshal(key string, rawVal any) error

Unmarshal unmarshals the configuration at the given key into the provided struct.

func (*Provider) Watch

func (p *Provider) Watch(callback func(event hyperion.ChangeEvent)) (stop func(), err error)

Watch starts watching for configuration file changes and triggers callbacks. It uses fsnotify to watch the configuration file directly.

The callback is invoked whenever the configuration file is modified. Note: The ChangeEvent.Key will contain the filename and Value will be nil for file-based watches. Use the Provider methods to read the updated configuration values.

Multiple callbacks can be registered by calling Watch multiple times. Each callback is assigned a unique ID to ensure safe removal even in concurrent scenarios.

Returns a stop function to cancel watching and an error if watching cannot be started.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL