diff --git a/AGENTS.md b/AGENTS.md index ebbdee9..5d2c700 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -141,7 +141,15 @@ go run cmd/server/main.go & **Configuration File Support:** -A `config.example.yaml` file is provided as a template: +A `config.example.yaml` file is provided as a template. By default, the application looks for `config.yaml` in the current working directory. + +To specify a custom config file path, set the `DLC_CONFIG_FILE` environment variable: + +```bash +DLC_CONFIG_FILE="/path/to/config.yaml" go run ./cmd/server +``` + +Example `config.yaml`: ```yaml server: @@ -153,10 +161,12 @@ shutdown: ``` **Configuration Loading:** -- Environment variables take precedence over defaults +- **File-based configuration** takes highest precedence +- **Environment variables** override defaults but are overridden by config file +- **Default values** are used when no other configuration is provided - All configuration is validated on startup - Invalid configurations cause server startup failure -- Configuration values are logged at startup +- Configuration values and source are logged at startup **Verification:** ```bash diff --git a/README.md b/README.md index be31820..4af5e1e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ # DanceLessonsCoach -A simple Go project demonstrating idiomatic package structure and CLI implementation. +A Go project demonstrating idiomatic package structure, CLI implementation, and JSON API with Chi router. ## Features - Greet function with default behavior - Command-line interface +- JSON API with versioned endpoints +- Chi router integration +- Zerolog for high-performance logging +- Viper for configuration management +- Graceful shutdown with context - Unit tests - Go 1.26.1 compatible @@ -20,8 +25,70 @@ cd DanceLessonsCoach go run ./cmd/greet ``` +## Optional Configuration + +The project supports configuration via YAML file, environment variables, or defaults. +Configuration priority: file > environment variables > defaults + +### Configuration File + +By default, the application looks for `config.yaml` in the current working directory. + +Create a `config.yaml` file: + +```yaml +server: + host: "0.0.0.0" + port: 8080 +shutdown: + timeout: 30s +``` + +Then start the server: + +```bash +go run ./cmd/server +``` + +### Custom Config File Path + +To specify a custom config file path, use the `DLC_CONFIG_FILE` environment variable: + +```bash +# Use a specific config file +export DLC_CONFIG_FILE="/path/to/your/config.yaml" +go run ./cmd/server + +# Or in one command +DLC_CONFIG_FILE="/path/to/your/config.yaml" go run ./cmd/server +``` + +### Environment Variables + +You can also configure via environment variables with `DLC_` prefix: + +```bash +# Set configuration via environment variables +export DLC_SERVER_HOST="0.0.0.0" +export DLC_SERVER_PORT=8080 +export DLC_SHUTDOWN_TIMEOUT=30s + +# Start the server +go run ./cmd/server +``` + +### Configuration Priority + +1. **File-based configuration** (highest priority) +2. **Environment variables** (override defaults) +3. **Default values** (lowest priority) + +This means if you have both a config file and environment variables, the file takes precedence. + ## Usage +### CLI + ```bash # Default greeting go run ./cmd/greet @@ -32,6 +99,23 @@ go run ./cmd/greet John # Output: Hello John! ``` +### Web Server + +```bash +# Start the server +go run ./cmd/server + +# Test API endpoints +curl http://localhost:8080/api/health +# Output: {"status":"healthy"} + +curl http://localhost:8080/api/v1/greet +# Output: {"message":"Hello world!"} + +curl http://localhost:8080/api/v1/greet/John +# Output: {"message":"Hello John!"} +``` + ## Testing ```bash @@ -47,12 +131,20 @@ go test ./pkg/greet/ ``` DanceLessonsCoach/ ├── cmd/ -│ └── greet/ -│ └── main.go # CLI entry point +│ ├── greet/ # CLI entry point +│ │ └── main.go +│ └── server/ # Web server entry point +│ └── main.go ├── pkg/ -│ └── greet/ -│ ├── greet.go # Core library -│ └── greet_test.go # Unit tests +│ ├── config/ # Configuration management +│ │ └── config.go +│ ├── greet/ # Core greet functionality +│ │ ├── api_v1.go # API v1 handlers +│ │ ├── greet.go # Core service +│ │ └── greet_test.go # Unit tests +│ └── server/ # Server implementation +│ └── server.go +├── config.example.yaml # Configuration template ├── go.mod # Go module definition └── README.md # Project documentation ``` diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..2e88d1b --- /dev/null +++ b/config.yaml @@ -0,0 +1,5 @@ +server: + host: "0.0.0.0" + port: 8080 +shutdown: + timeout: 5s \ No newline at end of file diff --git a/pkg/config/config.go b/pkg/config/config.go index 38d2e0f..20e96e2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "os" "time" "github.com/rs/zerolog/log" @@ -19,7 +20,9 @@ type Config struct { } } -// LoadConfig loads configuration from environment variables and defaults +// LoadConfig loads configuration from file, environment variables, and defaults +// Configuration priority: file > environment variables > defaults +// To specify a custom config file path, set DLC_CONFIG_FILE environment variable func LoadConfig() (*Config, error) { v := viper.New() @@ -28,6 +31,28 @@ func LoadConfig() (*Config, error) { v.SetDefault("server.port", 8080) v.SetDefault("shutdown.timeout", 30*time.Second) + // Check for custom config file path via environment variable + if configFile := os.Getenv("DLC_CONFIG_FILE"); configFile != "" { + v.SetConfigFile(configFile) + log.Info().Str("config_file", configFile).Msg("Using custom config file path") + } else { + // Default: look for config.yaml in current directory + v.SetConfigName("config") + v.SetConfigType("yaml") + v.AddConfigPath(".") + } + + // Read config file if it exists + if err := v.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + // Config file was found but there was an error reading it + log.Warn().Err(err).Msg("Error reading config file, using defaults") + } + // Config file not found, continue with environment variables and defaults + } else { + log.Info().Str("config_file", v.ConfigFileUsed()).Msg("Config file loaded") + } + // Bind environment variables v.AutomaticEnv() v.SetEnvPrefix("DLC") // DanceLessonsCoach prefix