{"id":25933341,"url":"https://github.com/robertwhurst/orale","last_synced_at":"2026-03-04T04:02:24.905Z","repository":{"id":216285883,"uuid":"740927832","full_name":"RobertWHurst/Orale","owner":"RobertWHurst","description":"A great little configuration loader for go","archived":false,"fork":false,"pushed_at":"2026-01-21T21:44:19.000Z","size":1787,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-01-22T10:22:21.860Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RobertWHurst.png","metadata":{"files":{"readme":"Readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-01-09T11:02:49.000Z","updated_at":"2026-01-21T21:44:22.000Z","dependencies_parsed_at":"2024-01-11T13:54:18.042Z","dependency_job_id":"84c6ebd8-06e5-4e87-9d54-eb5a2e01227b","html_url":"https://github.com/RobertWHurst/Orale","commit_stats":null,"previous_names":["robertwhurst/orale"],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/RobertWHurst/Orale","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobertWHurst%2FOrale","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobertWHurst%2FOrale/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobertWHurst%2FOrale/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobertWHurst%2FOrale/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RobertWHurst","download_url":"https://codeload.github.com/RobertWHurst/Orale/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobertWHurst%2FOrale/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30071670,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-04T03:25:38.285Z","status":"ssl_error","status_checked_at":"2026-03-04T03:25:05.086Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-04T00:53:29.912Z","updated_at":"2026-03-04T04:02:24.882Z","avatar_url":"https://github.com/RobertWHurst.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Órale (Oh-rah-leh)\n\n\u003cp\u003e\n  \u003cimg align=\"right\" src=\"orale.png\" width=\"400\" /\u003e\n\u003c/p\u003e\n\n_A fantastic little config loader for Go that collects configuration from flags, environment variables, and configuration files, then marshals the values into your structs._\n\n\u003e Órale is pronounced \"Odelay\" in English. The name is Mexican slang which translates roughly to \"listen up\" or \"what's up?\", and is pronounced like \"Oh-rah-leh\".\n\n\u003cbr clear=\"right\" /\u003e\n\n---\n\n## Features\n\n- 🎯 **Single unified API** - Load from flags, environment variables, and config files\n- 📝 **Multiple sources** - Configurable precedence: flags \u003e environment \u003e config files\n- 🔄 **Automatic type conversion** - String, int, bool, float, time.Duration, time.Time, and more\n- 🪆 **Nested configuration** - Support for nested structs and embedded fields\n- 📋 **Slice support** - Multi-value configuration (multiple flags, array values)\n- 🎨 **Flexible naming** - Automatically converts between camelCase, snake_case, and kebab-case\n- 🔀 **Variable expansion** - Reference environment variables, other config values, or file contents\n- 🔐 **Local dev encryption** - Commit encrypted secrets safely for team development\n- ⚙️ **Environment-specific configs** - Load different configs for dev, staging, production\n- 🔧 **Default values** - Keep defaults in your structs, override only what you need\n\n## Installation\n\n**Library:**\n```sh\ngo get github.com/RobertWHurst/orale\n```\n\n**CLI (for encryption):**\n```sh\ngo install github.com/RobertWHurst/orale/cmd/orale@latest\n```\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"time\"\n    \"github.com/RobertWHurst/orale\"\n)\n\ntype Config struct {\n    Port    int           `config:\"port\"`\n    Timeout time.Duration `config:\"timeout\"`\n}\n\nfunc main() {\n    loader, err := orale.Load(\"myApp\")\n    if err != nil {\n        panic(err)\n    }\n\n    config := Config{\n        Port:    8080,  // defaults\n        Timeout: time.Second * 30,\n    }\n\n    if err := loader.GetAll(\u0026config); err != nil {\n        panic(err)\n    }\n\n    fmt.Printf(\"Server starting on port %d with timeout %v\\n\", config.Port, config.Timeout)\n}\n```\n\nNow you can configure it via:\n- **Flag**: `./myApp --port=3000 --timeout=5m`\n- **Environment**: `MY_APP__PORT=3000 MY_APP__TIMEOUT=5m`\n- **Config file** (`myApp.config.toml`):\n  ```toml\n  port = 3000\n  timeout = \"5m\"\n  ```\n\n## Application Name\n\nThe application name you pass to `Load()` should be in **camelCase**. Orale uses this name to:\n\n- Generate the **environment variable prefix** by converting to uppercase with underscores (`myApp` → `MY_APP__`)\n- Locate the **config file** by converting to lowercase with hyphens (`myApp` → `my-app.config.toml`)\n\nFor example, `orale.Load(\"myApp\")` will look for environment variables like `MY_APP__PORT` and a config file named `my-app.config.toml`.\n\n## Configuration Sources\n\nOrale loads configuration from three sources, with the following precedence (highest to lowest):\n\n1. **Command-line flags** - `--key=value`\n2. **Environment variables** - `APP_NAME__KEY=value`\n3. **Configuration files** - `app-name.config.toml`\n\n### Naming Conventions and Path Syntax\n\nOrale automatically converts between different naming conventions and path delimiters:\n\n| Source            | Format            | Nesting Delimiter       | Example                                   |\n|-------------------|-------------------|-------------------------|-------------------------------------------|\n| Struct tags       | camelCase         | `.` (dot)               | `config:\"database.connectionUri\"`         |\n| Flags             | kebab-case        | `--` (double dash)      | `--database--connection-uri=...`          |\n| Environment vars  | UPPER_SNAKE_CASE  | `__` (double underscore)| `APP__DATABASE__CONNECTION_URI=...`       |\n| TOML files        | snake_case        | `[sections]`            | `[database]` then `connection_uri = ...`  |\n\n**Important path rules:**\n\n1. **Flags use double-dashes for nesting:**\n   - `--database--host=localhost`\n   - `--server--tls--cert-path=/path/to/cert`\n   - The double-dash separates path segments\n\n2. **Environment variables use double underscores for nesting:**\n   ```sh\n   # Single level\n   APP__PORT=8080\n\n   # Nested\n   APP__DATABASE__HOST=localhost\n   APP__DATABASE__CONNECTION_POOL_SIZE=10\n\n   # Deeply nested\n   APP__SERVER__TLS__CERT_PATH=/path/to/cert\n   ```\n\n3. **TOML files use sections and snake_case:**\n   ```toml\n   port = 8080\n\n   [database]\n   host = \"localhost\"\n   connection_pool_size = 10\n\n   [server.tls]\n   cert_path = \"/path/to/cert\"\n   ```\n\n4. **All formats automatically convert to camelCase paths internally:**\n   - `--server-port` → `serverPort`\n   - `APP__SERVER_PORT` → `serverPort`\n   - TOML `server_port` → `serverPort`\n   - **Important**: Struct `config` tags must be in camelCase to match these converted paths:\n     ```go\n     Port int `config:\"serverPort\"`  // Correct - matches converted paths\n     Port int `config:\"server_port\"` // Wrong - won't match\n     ```\n\n## Variable Expansion\n\nOrale supports variable expansion in configuration values, allowing you to reference environment variables, other configuration values, or file contents. This is useful for building dynamic configuration values, referencing secrets, and avoiding duplication.\n\n### Variable Types\n\nOrale supports three types of variable expansion:\n\n| Syntax | Type | Description | Example |\n|--------|------|-------------|---------|\n| `${VAR}` | Environment Variable | Expands to the value of the environment variable `VAR` | `${HOME}`, `${DATABASE_PASSWORD}` |\n| `%{key}` | Config Variable | Expands to the value of another config key | `%{baseUrl}`, `%{database.host}` |\n| `@{path}` | File Variable | Expands to the contents of the file at `path` (strips trailing newline) | `@{/secrets/api-key}`, `@{./cert.pem}` |\n\n**Key features:**\n- Variables can be combined in a single value\n- Expansion happens recursively (config variables can contain other variables)\n- Config variables support any naming convention (snake_case, kebab-case, camelCase)\n- Config variables can reference any value type (strings, integers, floats, bools)\n- File contents have trailing newlines automatically stripped\n- Circular references are detected and return an error\n\n### Environment Variables: `${VAR}`\n\nReference environment variables using `${VAR}` syntax:\n\n```go\ntype Config struct {\n    DatabaseURL string `config:\"databaseUrl\"`\n    APIKey      string `config:\"apiKey\"`\n}\n```\n\n**Configuration file** (`myApp.config.toml`):\n```toml\ndatabase_url = \"postgres://${DB_HOST}:${DB_PORT}/mydb\"\napi_key = \"${API_SECRET}\"\n```\n\n**Run with environment variables:**\n```sh\nDB_HOST=localhost DB_PORT=5432 API_SECRET=secret123 ./myApp\n```\n\n**Result:**\n- `config.DatabaseURL` = `\"postgres://localhost:5432/mydb\"`\n- `config.APIKey` = `\"secret123\"`\n\n**You can also use environment variables directly:**\n```sh\n# Environment variables (higher precedence than config files)\nMY_APP__DATABASE_URL='postgres://${DB_HOST}:${DB_PORT}/mydb' \\\nMY_APP__API_KEY='${API_SECRET}' \\\nDB_HOST=localhost \\\nDB_PORT=5432 \\\nAPI_SECRET=secret123 \\\n./myApp\n```\n\n### Config Variables: `%{key}`\n\nReference other configuration values using `%{key}` syntax. This is useful for building related URLs or avoiding duplication. Config variable paths support any naming convention - use whatever matches your configuration source (snake_case for TOML, kebab-case for flags, or direct camelCase):\n\n```go\ntype Config struct {\n    BaseURL    string `config:\"baseUrl\"`\n    APIURL     string `config:\"apiUrl\"`\n    WebhookURL string `config:\"webhookUrl\"`\n}\n```\n\n**Configuration file** (`myApp.config.toml`):\n```toml\nbase_url = \"https://api.example.com\"\napi_url = \"%{base_url}/v1\"\nwebhook_url = \"%{base_url}/webhooks/incoming\"\n```\n\n**Result:**\n- `config.BaseURL` = `\"https://api.example.com\"`\n- `config.APIURL` = `\"https://api.example.com/v1\"`\n- `config.WebhookURL` = `\"https://api.example.com/webhooks/incoming\"`\n\n**Nested paths:** Use dots, double-underscores, or double-dashes to reference nested values:\n```toml\n[database]\nhost = \"db.example.com\"\nport = 5432\n\n[database]\n# All of these work the same:\nconnection_string = \"postgres://%{database.host}:%{database.port}/mydb\"\n# Or with double-underscore (TOML/env style):\nconnection_string = \"postgres://%{database__host}:%{database__port}/mydb\"\n# Or with double-dash (flag style):\nconnection_string = \"postgres://%{database--host}:%{database--port}/mydb\"\n```\n\n**Note:** Config variables automatically convert non-string values (integers, floats, bools) to strings, so you can reference any configuration value.\n\n### File Variables: `@{path}`\n\nRead file contents using `@{path}` syntax. This is ideal for reading secrets, certificates, or API keys from files:\n\n```go\ntype Config struct {\n    APIKey      string `config:\"apiKey\"`\n    Certificate string `config:\"certificate\"`\n}\n```\n\n**Configuration file** (`myApp.config.toml`):\n```toml\napi_key = \"@{/run/secrets/api-key}\"\ncertificate = \"@{/etc/ssl/certs/app-cert.pem}\"\n```\n\n**With files:**\n```sh\n# /run/secrets/api-key\nsk_live_abc123def456\n\n# /etc/ssl/certs/app-cert.pem\n-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAKZ...\n-----END CERTIFICATE-----\n```\n\n**Result:**\n- `config.APIKey` = `\"sk_live_abc123def456\"`\n- `config.Certificate` = Full certificate content (trailing newline removed)\n\n**Security note:** File variables are perfect for Docker secrets, Kubernetes mounted secrets, or any secrets management system that writes secrets to files.\n\n### Combining Variable Types\n\nYou can combine all three variable types in a single value:\n\n```go\ntype Config struct {\n    ConnectionString string `config:\"connectionString\"`\n}\n```\n\n**Configuration file** (`myApp.config.toml`):\n```toml\n[database]\nhost = \"dbserver.example.com\"\n\nconnection_string = \"host=%{database.host};password=${DB_PASSWORD};cert=@{/secrets/db-cert.pem}\"\n```\n\n**Run with:**\n```sh\nDB_PASSWORD=securepass123 ./myApp\n```\n\n**Result:**\n```\nconfig.ConnectionString = \"host=dbserver.example.com;password=securepass123;cert=-----BEGIN CERTIFICATE-----...\"\n```\n\n### Practical Examples\n\n#### Docker Secrets Pattern\n\n```toml\n# myApp.config.toml\n[database]\nhost = \"postgres\"\nport = 5432\nname = \"myapp\"\nuser = \"myapp\"\npassword = \"@{/run/secrets/db_password}\"\n\n[database]\nurl = \"postgres://%{database.user}:%{database.password}@%{database.host}:%{database.port}/%{database.name}\"\n```\n\n```sh\n# docker-compose.yml\nservices:\n  app:\n    image: myapp:latest\n    secrets:\n      - db_password\nsecrets:\n  db_password:\n    file: ./secrets/db_password.txt\n```\n\n#### Multi-Environment Configuration\n\n```toml\n# myApp.production.config.toml\nenvironment = \"production\"\nbase_url = \"https://${DOMAIN}\"\napi_url = \"%{baseUrl}/api\"\ncdn_url = \"%{baseUrl}/cdn\"\n\n[security]\ntls_cert = \"@{/etc/letsencrypt/live/${DOMAIN}/fullchain.pem}\"\ntls_key = \"@{/etc/letsencrypt/live/${DOMAIN}/privkey.pem}\"\n```\n\n#### Development with Local Overrides\n\n```toml\n# myApp.config.toml (checked into git)\n[database]\nhost = \"localhost\"\nport = 5432\n\n[api]\nbase_url = \"%{protocol}://%{database.host}:${API_PORT}\"\nprotocol = \"http\"\n```\n\n```sh\n# .env file (not checked into git)\nAPI_PORT=8080\n```\n\n### Error Handling\n\nOrale will return an error for:\n- **Circular references**: `a = \"%{b}\"` and `b = \"%{a}\"`\n- **Missing closing brace**: `\"${UNCLOSED\"`\n- **Non-existent files**: `\"@{/path/that/does/not/exist}\"`\n- **Invalid UTF-8 in files**: File contents must be valid UTF-8\n\nExample:\n```go\nloader, _ := orale.Load(\"myApp\")\nif err := loader.GetAll(\u0026config); err != nil {\n    // Handle expansion errors\n    fmt.Printf(\"Configuration error: %v\\n\", err)\n}\n```\n\n## Local Development Encryption\n\nOrale supports encrypting configuration values that need to be committed to your repository. This is perfect for **local development defaults** that your team shares, like database passwords for local Docker containers or test API keys.\n\n**Use this for:**\n- Local development database passwords\n- Test API keys and credentials\n- Shared development secrets that all team members use\n\n**Do NOT use this for:**\n- Production secrets (use environment variables or proper secret management)\n- User-specific values (use environment variables)\n- Secrets that differ per environment (use environment variables)\n\n### Team Setup Workflow\n\n**One-time team setup:**\n\n1. One team member generates a key:\n   ```sh\n   orale keygen dev\n   ```\n\n2. Share the output key securely with the team (Slack DM, password manager, etc.)\n\n3. Team members import the key:\n   ```sh\n   orale keyset Xkn4noje9zkpnSLImQHlkRUfBnlyOfo13hKlIN5vgp2J4k+b3RztePvvHJRGjhtbKn...\n   ```\n\n**Encrypting values:**\n\n```sh\n# Encrypt a value\norale encrypt dev \"my-local-db-password\"\n# Output: ENC[q7Q6pVJdK6dHDH/s204CDWTmGqtNQvMA8X78V7uImer...]\n\n# From stdin\necho \"my-secret\" | orale encrypt dev\n```\n\n**Using encrypted values:**\n\nEncrypted values work in all configuration sources:\n\n```toml\n# Config file (myApp.config.toml) - safe to commit!\n[database]\npassword = \"ENC[q7Q6pVJdK6dHDH/s204CDWTmGqtNQvMA8X78V7uImer+BC8tYXxYJnyrCJUAKoxkjHTNj2rEd5pcwgd/1QlS37JuES3xMxWsVvWPJAchp40=]\"\n```\n\n```sh\n# Environment variables\nMY_APP__DATABASE__PASSWORD='ENC[q7Q6pVJdK6dHDH/s204CDWTmGqtNQvMA8X78V7uImer...]'\n\n# Flags\n./myApp --database--password='ENC[q7Q6pVJdK6dHDH/s204CDWTmGqtNQvMA8X78V7uImer...]'\n```\n\n**Automatic decryption in your app:**\n\n```go\ntype Config struct {\n    Database struct {\n        Host     string `config:\"host\"`\n        Port     int    `config:\"port\"`\n        Password string `config:\"password\"`\n    } `config:\"database\"`\n}\n\nfunc main() {\n    loader, _ := orale.Load(\"myApp\")\n\n    config := Config{\n        Database: {\n            Port: 5432,  // Default port\n        },\n    }\n\n    loader.GetAll(\u0026config)\n\n    // config.Database.Password is automatically decrypted!\n    fmt.Println(config.Database.Password) // \"my-local-db-password\"\n}\n```\n\n### How It Works\n\n1. Encrypted values use the format `ENC[base64-data]`\n2. During config loading, Orale detects and decrypts these values automatically\n3. Tries all available keys in `~/.config/orale/` until HMAC verification succeeds\n4. Decryption happens **before** variable expansion\n5. Your application receives the plain text value\n\n### Where Encrypted Values Are Decrypted\n\nOrale automatically detects and decrypts `ENC[...]` values **everywhere** string values can appear:\n\n**1. Configuration Files (TOML)**\n```toml\ndatabase_password = \"ENC[q7Q6pVJdK6dHDH/s204CDWTmGqtNQvMA8X78V7uImer...]\"\n```\n\n**2. Command-Line Flags**\n```sh\n./myApp --api-key='ENC[q7Q6pVJdK6dHDH/s204CDWTmGqtNQvMA8X78V7uImer...]'\n```\n\n**3. Environment Variables**\n```sh\nMY_APP__SECRET='ENC[q7Q6pVJdK6dHDH/s204CDWTmGqtNQvMA8X78V7uImer...]'\n```\n\n**4. Default Struct Values**\n```go\ntype Config struct {\n    // Default value is encrypted - will be decrypted automatically!\n    APIKey string `config:\"apiKey\"`\n}\n\nconfig := Config{\n    APIKey: \"ENC[q7Q6pVJdK6dHDH/s204CDWTmGqtNQvMA8X78V7uImer...]\",\n}\nloader.GetAll(\u0026config)\n// config.APIKey is now decrypted\n```\n\n**5. Environment Variable Expansions**\n```sh\n# Set an encrypted value in an environment variable\nexport SECRET_TOKEN='ENC[q7Q6pVJdK6dHDH/s204CDWTmGqtNQvMA8X78V7uImer...]'\n```\n\n```toml\n# Reference it in your config - both the expansion AND the result are decrypted\napi_token = \"${SECRET_TOKEN}\"  # Decrypted automatically\n```\n\n**6. File Variable Expansions**\n```sh\n# Store encrypted value in a file\necho 'ENC[q7Q6pVJdK6dHDH/s204CDWTmGqtNQvMA8X78V7uImer...]' \u003e /secrets/api-key\n```\n\n```toml\n# Read from file - content is decrypted automatically\napi_key = \"@{/secrets/api-key}\"  # Decrypted automatically\n```\n\n**Key Points:**\n- Decryption is **completely automatic** - you never need to call decrypt functions\n- Works with **all configuration sources** (files, flags, environment, defaults)\n- Decryption happens **before** variable expansion, so expanded variables can also be encrypted\n- If a value isn't encrypted (doesn't match `ENC[...]`), it's used as-is\n\n### Multiple Keys\n\nYou can have different keys for different purposes:\n\n```sh\norale keygen dev        # For local development\norale keygen staging    # For staging environment defaults\norale keygen testing    # For test fixtures\n\norale keylist           # List all available keys\n```\n\nOrale automatically tries all keys when decrypting - each value works with whichever key encrypted it.\n\n### CLI Reference\n\n**View loaded configuration:**\n```sh\norale explain myApp\norale explain myApp --port=3000  # Include flags to see final values\n```\n\nDisplays a table showing all loaded config values, their current values, and sources (flag/environment/file path).\n\n### Security Notes\n\n- Keys stored in `~/.config/orale/\u003cname\u003e.ok` with 0600 permissions\n- AES-256-CBC encryption with HMAC-SHA256 authentication\n- Each encrypted value has a unique random IV\n- **Production secrets should use environment variables**, not committed encrypted values\n\n## Type Conversions\n\nOrale automatically converts configuration values to match your struct field types:\n\n### Basic Types\n\n```go\ntype Config struct {\n    Name    string  `config:\"name\"`\n    Port    int     `config:\"port\"`\n    Enabled bool    `config:\"enabled\"`\n    Ratio   float64 `config:\"ratio\"`\n}\n```\n\n### Time Types\n\n**`time.Duration`** - Accepts human-readable strings or nanoseconds:\n```go\ntype Config struct {\n    Timeout        time.Duration `config:\"timeout\"`\n    RetryInterval  time.Duration `config:\"retryInterval\"`\n}\n```\n\nValid formats:\n- Strings: `\"5h\"`, `\"30m\"`, `\"1h30m\"`, `\"500ms\"`\n- Numbers: `1000000000` (nanoseconds)\n\n**`time.Time`** - Accepts various timestamp formats:\n```go\ntype Config struct {\n    StartTime time.Time `config:\"startTime\"`\n    Birthday  time.Time `config:\"birthday\"`\n}\n```\n\nValid formats:\n- RFC3339: `\"2025-01-15T10:30:00Z\"`\n- RFC3339Nano: `\"2025-01-15T10:30:00.123456789Z\"`\n- ISO Date: `\"2025-01-15\"`\n- Unix timestamp: `1736938200` (int or float)\n\n### Slices\n\nCollect multiple values into slice fields:\n\n#### Slices of Primitives\n\n```go\ntype Config struct {\n    Servers []string `config:\"servers\"`\n    Ports   []int    `config:\"ports\"`\n}\n```\n\n**Flags** - Two approaches:\n\n1. Repeat the same flag multiple times:\n```sh\n./app --servers=host1 --servers=host2 --servers=host3\n```\n\n2. Use indexed notation:\n```sh\n./app --servers--0=host1 --servers--1=host2 --servers--2=host3\n```\n\n**Environment variables** - Use indexed notation:\n```sh\nAPP__SERVERS__0=host1\nAPP__SERVERS__1=host2\nAPP__SERVERS__2=host3\n```\n\n**TOML** - Use array syntax:\n```toml\nservers = [\"host1\", \"host2\", \"host3\"]\nports = [8080, 8081]\n```\n\n#### Slices of Structs\n\nFor slices of structs, you must use indexed notation to specify individual fields:\n\n```go\ntype Server struct {\n    Host string `config:\"host\"`\n    Port int    `config:\"port\"`\n}\n\ntype Config struct {\n    Servers []Server `config:\"servers\"`\n}\n```\n\n**Flags** - Use indexed notation with nested fields:\n```sh\n./app --servers--0--host=localhost --servers--0--port=8080 \\\n      --servers--1--host=example.com --servers--1--port=9090\n```\n\n**Environment variables** - Use indexed notation with nested fields:\n```sh\nAPP__SERVERS__0__HOST=localhost\nAPP__SERVERS__0__PORT=8080\nAPP__SERVERS__1__HOST=example.com\nAPP__SERVERS__1__PORT=9090\n```\n\n**TOML** - Use array of tables syntax:\n```toml\n[[servers]]\nhost = \"localhost\"\nport = 8080\n\n[[servers]]\nhost = \"example.com\"\nport = 9090\n```\n\n**Note:** Indices can be sparse (e.g., `servers.0`, `servers.4`). Orale will create a slice with the appropriate length, and unset indices will have zero values.\n\n### Maps\n\nMaps allow dynamic keys for configuration:\n\n```go\ntype Config struct {\n    Labels   map[string]string `config:\"labels\"`\n    Ports    map[string]int    `config:\"ports\"`\n    Features map[string]bool   `config:\"features\"`\n}\n```\n\n**Flags:**\n```sh\n./app --labels--env=development --labels--version=1.0.0 --labels--team=backend\n```\n\n**Environment variables:**\n```sh\nAPP__LABELS__ENV=development\nAPP__LABELS__VERSION=1.0.0\nAPP__LABELS__TEAM=backend\n```\n\n**TOML:**\n```toml\n[labels]\nenv = \"development\"\nversion = \"1.0.0\"\nteam = \"backend\"\n```\n\nMaps support any value type (string, int, bool, float, structs, etc.). Keys are extracted from the configuration paths automatically.\n\n### Nested Structs\n\nNested structs create configuration paths using their `config` tag:\n\n```go\ntype TLSConfig struct {\n    CertPath string `config:\"certPath\"`\n    KeyPath  string `config:\"keyPath\"`\n}\n\ntype ServerConfig struct {\n    Host string    `config:\"host\"`\n    Port int       `config:\"port\"`\n    TLS  TLSConfig `config:\"tls\"`\n}\n\ntype Config struct {\n    Server ServerConfig `config:\"server\"`\n}\n```\n\nThe resulting paths are:\n- `server.host`\n- `server.port`\n- `server.tls.certPath`\n- `server.tls.keyPath`\n\nConfigure with:\n```sh\n# Flags (double-dash notation for nesting)\n./app --server--host=0.0.0.0 --server--port=8080\n./app --server--tls--cert-path=/path/to/cert --server--tls--key-path=/path/to/key\n\n# Environment (double underscores)\nAPP__SERVER__HOST=0.0.0.0\nAPP__SERVER__PORT=8080\nAPP__SERVER__TLS__CERT_PATH=/path/to/cert\nAPP__SERVER__TLS__KEY_PATH=/path/to/key\n\n# TOML (sections and nested sections)\n[server]\nhost = \"0.0.0.0\"\nport = 8080\n\n[server.tls]\ncert_path = \"/path/to/cert\"\nkey_path = \"/path/to/key\"\n```\n\n### Pointers and Default Values\n\n```go\ntype Config struct {\n    // Nil pointer - will be initialized if config is provided\n    Database *DatabaseConfig `config:\"database\"`\n\n    // Value with default - will be overridden if config is provided\n    Port int `config:\"port\"` // default: 0\n}\n\nfunc main() {\n    config := Config{\n        Port: 8080, // default value\n    }\n\n    loader.GetAll(\u0026config)\n    // Port will be 8080 unless overridden by flags/env/file\n}\n```\n\n### Embedded Structs\n\n```go\ntype ServerConfig struct {\n    Host string `config:\"host\"`\n    Port int    `config:\"port\"`\n}\n\ntype Config struct {\n    ServerConfig // embedded - fields are promoted\n    Debug bool `config:\"debug\"`\n}\n\n// Access: config.Host, config.Port, config.Debug\n```\n\n## Configuration Files\n\nOrale looks for TOML configuration files with the following naming pattern:\n\n```\n\u003capp-name\u003e.config.toml\n\u003capp-name\u003e.\u003cenvironment\u003e.config.toml\n```\n\n### File Discovery\n\nFiles are searched in the current directory and all parent directories, walking up the filesystem tree from the working directory to the root.\n\nThis allows you to place config files at the project root and run your app from subdirectories.\n\n### Environment-Specific Configs\n\nLoad different configs for different environments by setting the `configEnvironment` value:\n\n**Via flag:**\n```sh\n./myApp --config-environment=production\n```\n\n**Via environment variable:**\n```sh\nMY_APP__CONFIG_ENVIRONMENT=production ./myApp\n```\n\nThis will look for `myApp.production.config.toml` instead of `myApp.config.toml`.\n\n**Common patterns:**\n```sh\n# Development (default - no environment specified)\n./myApp  # loads myApp.config.toml\n\n# Staging\n./myApp --config-environment=staging  # loads myApp.staging.config.toml\n\n# Production\nMY_APP__CONFIG_ENVIRONMENT=production ./myApp  # loads myApp.production.config.toml\n\n# QA\n./myApp --config-environment=qa  # loads myApp.qa.config.toml\n```\n\n### TOML Format\n\n```toml\n# Basic values\nport = 8080\nenabled = true\ntimeout = \"5m\"\n\n# Nested configuration\n[database]\nhost = \"localhost\"\nport = 5432\nconnection_pool_size = 10\n\n# Arrays\nservers = [\"host1:8080\", \"host2:8080\", \"host3:8080\"]\n\n# Time values\nstart_time = \"2025-01-15T10:30:00Z\"\nretry_interval = \"30s\"\n```\n\n## Advanced Usage\n\n### Loading Specific Paths (Sub-pathing)\n\nYou can load just a subset of your configuration using path strings with `Get()`:\n\n```go\ntype DatabaseConfig struct {\n    Host string `config:\"host\"`\n    Port int    `config:\"port\"`\n}\n\ntype ServerConfig struct {\n    Port int `config:\"port\"`\n}\n\ntype Config struct {\n    Database DatabaseConfig `config:\"database\"`\n    Server   ServerConfig   `config:\"server\"`\n}\n\nloader, _ := orale.Load(\"myApp\")\n\n// Load entire config\nvar fullConfig Config\nloader.GetAll(\u0026fullConfig)\n\n// Load only database config\nvar dbConfig DatabaseConfig\nloader.Get(\"database\", \u0026dbConfig)\n\n// Load only server config\nvar serverConfig ServerConfig\nloader.Get(\"server\", \u0026serverConfig)\n```\n\n**Path syntax:**\n- Paths use dot notation: `\"database\"`, `\"server.tls\"`, `\"database.connection\"`\n- Paths map to struct field `config` tags (in camelCase)\n\n**How sources map to paths:**\n\nGiven the path `\"database.host\"`:\n\n```sh\n# Flags (double-dash notation for nesting)\n./app --database--host=localhost\n\n# Environment (double-underscore notation for nesting)\nAPP__DATABASE__HOST=localhost\n\n# TOML (sections for nesting)\n[database]\nhost = \"localhost\"\n```\n\n### Array and Slice Paths\n\nArrays and slices use dot notation with numeric indices:\n\n```go\ntype Config struct {\n    Servers []ServerConfig `config:\"servers\"`\n}\n```\n\nConfiguration:\n```toml\n[[servers]]\nhost = \"server1\"\nport = 8080\n\n[[servers]]\nhost = \"server2\"\nport = 8081\n```\n\nInternally, Orale resolves these as:\n- `servers.0.host` → `\"server1\"`\n- `servers.0.port` → `8080`\n- `servers.1.host` → `\"server2\"`\n- `servers.1.port` → `8081`\n\n**Sparse indices are supported**: If you set non-contiguous indices (e.g., `servers.0` and `servers.4`), Orale will create a slice with the appropriate length (5 in this case), and unset indices will have zero values.\n\nYou cannot directly load a single array element with `Get()`, but you can load the entire array and access elements in code.\n\n### Multiple Configuration Structs\n\n```go\ntype DatabaseConfig struct {\n    Host string `config:\"host\"`\n    Port int    `config:\"port\"`\n}\n\ntype ServerConfig struct {\n    Port    int           `config:\"port\"`\n    Timeout time.Duration `config:\"timeout\"`\n}\n\ntype Config struct {\n    Database DatabaseConfig `config:\"database\"`\n    Server   ServerConfig   `config:\"server\"`\n}\n```\n\nConfigure with namespaced keys:\n```toml\n[database]\nhost = \"db.example.com\"\nport = 5432\n\n[server]\nport = 8080\ntimeout = \"30s\"\n```\n\n### Must Variants\n\nPanic instead of returning errors:\n\n```go\nloader := orale.MustLoad(\"myApp\")\nloader.MustGetAll(\u0026config)\n```\n\n## Complete Example\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"time\"\n    \"github.com/RobertWHurst/orale\"\n)\n\ntype DatabaseConfig struct {\n    Host               string `config:\"host\"`\n    Port               int    `config:\"port\"`\n    ConnectionPoolSize int    `config:\"connectionPoolSize\"`\n}\n\ntype ServerConfig struct {\n    Host    string        `config:\"host\"`\n    Port    int           `config:\"port\"`\n    Timeout time.Duration `config:\"timeout\"`\n}\n\ntype Config struct {\n    Database DatabaseConfig `config:\"database\"`\n    Server   ServerConfig   `config:\"server\"`\n    Debug    bool           `config:\"debug\"`\n}\n\nfunc main() {\n    // Set defaults\n    config := Config{\n        Database: DatabaseConfig{\n            Host:               \"localhost\",\n            Port:               5432,\n            ConnectionPoolSize: 5,\n        },\n        Server: ServerConfig{\n            Host:    \"0.0.0.0\",\n            Port:    8080,\n            Timeout: 30 * time.Second,\n        },\n        Debug: false,\n    }\n\n    // Load configuration\n    loader, err := orale.Load(\"myApp\")\n    if err != nil {\n        panic(err)\n    }\n\n    if err := loader.GetAll(\u0026config); err != nil {\n        panic(err)\n    }\n\n    fmt.Printf(\"Database: %s:%d (pool: %d)\\n\",\n        config.Database.Host,\n        config.Database.Port,\n        config.Database.ConnectionPoolSize)\n\n    fmt.Printf(\"Server: %s:%d (timeout: %v)\\n\",\n        config.Server.Host,\n        config.Server.Port,\n        config.Server.Timeout)\n\n    fmt.Printf(\"Debug mode: %v\\n\", config.Debug)\n}\n```\n\n**Configuration file** (`myApp.config.toml`):\n```toml\ndebug = true\n\n[database]\nhost = \"db.example.com\"\nport = 5432\nconnection_pool_size = 20\n\n[server]\nhost = \"0.0.0.0\"\nport = 3000\ntimeout = \"5m\"\n```\n\n**Override with environment**:\n```sh\nMY_APP__SERVER__PORT=8080 MY_APP__DEBUG=false ./myApp\n```\n\n**Override with flags**:\n```sh\n./myApp --server--port=8080 --debug=false\n```\n\n## API Reference\n\n### `Load(appName string) (*Loader, error)`\n\nCreates a new configuration loader for the given application name.\n\nThe application name is used to:\n- Generate the environment variable prefix (e.g., `\"myApp\"` → `MY_APP__`)\n- Locate configuration files (e.g., `\"myApp\"` → `myApp.config.toml`)\n\n**Returns:** A Loader instance or an error if configuration files cannot be parsed.\n\n**Example:**\n```go\nloader, err := orale.Load(\"myApp\")\n```\n\n### `MustLoad(appName string) *Loader`\n\nLike Load, but panics on error instead of returning an error.\n\n### `(*Loader).Get(path string, target interface{}) error`\n\nLoads configuration at the given path into the target struct.\n\n**Parameters:**\n- `path` - Dot-separated path to configuration (camelCase)\n  - Single level: `\"database\"`, `\"server\"`, `\"port\"`\n  - Nested: `\"database.connection\"`, `\"server.tls.certPath\"`\n  - Paths must match struct field `config` tags (in camelCase)\n- `target` - Pointer to struct to populate\n\n**Examples:**\n```go\n// Load subset at path \"database\"\nvar dbConfig DatabaseConfig\nloader.Get(\"database\", \u0026dbConfig)\n\n// Load deeply nested path\nvar tlsConfig TLSConfig\nloader.Get(\"server.tls\", \u0026tlsConfig)\n```\n\n**Returns:** Error if path doesn't exist or target is not a pointer.\n\n### `(*Loader).GetAll(target interface{}) error`\n\nLoads all configuration into the target struct.\n\n**Example:**\n```go\nvar config Config\nloader.GetAll(\u0026config)\n```\n\n### `(*Loader).MustGet(path string, target interface{})`\n\nLike Get, but panics on error instead of returning an error.\n\n### `(*Loader).MustGetAll(target interface{})`\n\nLike GetAll, but panics on error instead of returning an error.\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## License\n\n[Include your license here]\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobertwhurst%2Forale","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frobertwhurst%2Forale","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobertwhurst%2Forale/lists"}