Skip to content
/ go-envy Public

go-envy is a lean, production‑ready configuration library for golang support JSONand YAML configuration

License

Notifications You must be signed in to change notification settings

vyntro/go-envy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-envy — Enterprise‑grade Go configuration for JSON & YAML (with arrays)

go-envy is a lean, production‑ready configuration library that gives you:

Multiple file loads (JSON/YAML) with deep‑merge (maps) & replace (arrays)
Case‑insensitive dot‑paths + array indexes: db.host, addresses[1].area
• Predictable precedence: Runtime SetConfigEnvironmentFiles
Typed getters (GetString, GetInt, …), thread‑safe reads/writes
• Minimal deps: stdlib + YAML org’s maintained go.yaml.in/yaml/v4

Built for clarity, safety, and performance in real services.


Installation

go get github.com/vyntro/go-envy

Go: 1.25.6


Project Structure

go-envy/
├─ go.mod
├─ LICENSE
├─ README.md
├─ envy.go        # public API, constructor & options, typed getters
├─ loader.go      # JSON/YAML decoding & normalization (map[string]any, arrays allowed)
├─ merge.go       # deep-merge (maps recursive), arrays replaced wholesale
├─ path.go        # dot-path (+ [index]) parsing; case-insensitive map keys
├─ env.go         # env mapping & coercion, deep-copy helpers
├─ types.go       # Options & Option types
└─ envy_test.go   # unit tests including array paths and precedence

Quick Start

config.yaml

db:
  host: localhost
  port: 5432
  ssl: false
addresses:
  - street: 1
    area: michi
  - street: North street
    area: Kichi
  - street: Sout street
    area: Sichi

main.go

package main

import (
  "fmt"
  "github.com/vyntro/go-envy"
)

func main() {
  cfg := envy.New(
    envy.WithEnvEnabled(true),         // enable ENV overlay (default: true)
    envy.WithEnvPrefix("ENVY_"),       // default ENV prefix
    envy.WithEnvCoerceTypes(true),     // coerce ENV values to target type
    envy.WithMissingBehavior(""),      // on missing GetConfig without default, return ""
  )

  must(cfg.Load("config.yaml"))       // load as many files as you need

  // Runtime override (highest precedence)
  cfg.SetConfig("db.port", 6432)

  // Dot paths + array indexes
  host := cfg.GetString("db.host", "localhost")
  first := cfg.GetConfig("addresses[0]").(map[string]any)
  area1 := cfg.GetString("addresses[1].area") // "Kichi"

  fmt.Println(host, first["street"], area1)

  // Effective config (files -> env -> runtime)
  eff := cfg.GetAllConfig()
  fmt.Println(eff)
}

func must(err error) { if err != nil { panic(err) } }

Environment overrides

  • Prefix: ENVY_ (configurable via WithEnvPrefix).
  • Mapping: replace . and - with _ and uppercase the path.
  • Array indexes are digits: addresses[1].areaENVY_ADDRESSES_1_AREA.
  • Unset ENV means no override. ENV keys not in files are allowed and will be created on the fly.

Caching note:

  • The library builds an internal, read-optimized snapshot of environment variables to avoid re-scanning the process environment on every lookup. By default the snapshot contains only variables that match the configured EnvPrefix (case-insensitive). This makes GetAllConfig and other lookups much faster in read-heavy workloads.
  • If your process modifies environment variables at runtime (for example via os.Setenv), you have two options:
    1. Call RefreshEnvCache() on the *Config instance to force a full rescan and rebuild the snapshot (recommended after programmatic sets). Example:

      cfg.RefreshEnvCache()

    2. Rely on the library's per-key fallback: when a requested env key is not present in the snapshot, the library will perform a direct os.LookupEnv and update the snapshot for that key lazily. This is convenient but slightly more expensive for the first lookup.

The snapshot is internal and read-only; the library never calls os.Setenv. Tests and examples may call os.Setenv to simulate overrides, but production code should call RefreshEnvCache() if it programmatically mutates environment variables and needs immediate visibility.

Env cache semantics and RefreshEnvCache()

  • The library takes a lightweight snapshot of the process environment on first use to avoid repeatedly scanning os.Environ() on hot code paths. This makes environment lookups fast for read-heavy workloads.
  • If your process modifies environment variables at runtime (for example via os.Setenv), there are two ways to ensure the library sees those changes:
    • Call RefreshEnvCache() on your *Config instance to force a full rescan of os.Environ() and update the internal snapshot.
    • The library also performs a per-key fallback check (via os.LookupEnv) when an env key is missing from the snapshot, and will update the cache for that key automatically. This keeps behavior intuitive while still avoiding a full rescan on every call.
  • Recommendation: for deterministic behavior after bulk env updates (or when running programmatic tests that change many envs), call cfg.RefreshEnvCache() once after making the changes. For occasional single-key updates, the per-key fallback will pick them up.
  • Tradeoff: snapshotting avoids repeated syscalls and is faster at scale; RefreshEnvCache() is cheap but does perform a full environment scan.

Precedence (highest → lowest)

  1. Runtime SetConfig (in-memory)
  2. Environment variables
  3. Files (reverse load order; later loaded file overrides earlier)

API — Constructors, Options & Behaviors (in depth)

func New(opts ...Option) *Config

Creates a new configuration instance with sane defaults and optional behavior tweaks via Options.

Defaults:

  • EnvEnabled: true — ENV overlay is active
  • EnvPrefix: "ENVY_" — ENV variables must start with this prefix
  • EnvCoerceTypes: true — try to coerce ENV values to target type (bool/int/float)
  • MissingBehavior: ""GetConfig returns empty string when key is missing and no default is passed
  • EnvKeyReplacer: replaces . and - with _ and uppercases the rest (and also treats [n] as .n before replacement)

Options

func WithEnvEnabled(b bool) Option

What it does: Turns environment variable overrides on or off.

  • When true (default), any matching ENVY_* variables are applied as an overlay above files and below runtime.
  • When false, ENV variables are completely ignored by both GetConfig and GetAllConfig.

When to use:

  • Turn off for fully deterministic test environments or tooling that must ignore ENV.
  • Keep on in services so ops can change behavior without code or file edits.

func WithEnvPrefix(prefix string) Option

What it does: Sets the ENV prefix (default ENVY_). Only variables beginning with this prefix are considered.
Example: with WithEnvPrefix("APP_"), db.host maps to APP_DB_HOST.

When to use:

  • Multi-service processes needing separate namespaces per component (ORDER_, PAYMENT_).

func WithEnvKeyReplacer(fn func(string) string) Option

What it does: Sets a custom path→ENV mapping function.
Default logic:

  1. Convert [n].n (so indexes become segments)
  2. Replace . and - with _
  3. Uppercase

When to use:

  • You need a different ENV naming convention (e.g., keep case, other delimiters).

func WithEnvCoerceTypes(b bool) Option

What it does: Controls whether ENV values are type‑coerced to the underlying target type.

  • If true (default), and the file/runtime value is, say, int, ENV values are parsed to int when possible.
  • If false, ENV values are always strings.

When to use:

  • Disable if you want strict string semantics from ENV.

func WithMissingBehavior(s string) Option

What it does: Sets the return of GetConfig(field) when the key is missing and no default is provided.

  • "" (default) → return empty string
  • "field" → return the field name that was requested
  • "<literal>" → return the literal value you provide

Notes:

  • Typed getters ignore MissingBehavior; they return their default/zero.

API — Core Methods

func (c *Config) Load(path string) error

Loads a JSON or YAML file and deep‑merges it into the base configuration.

  • Maps are merged recursively — later file overrides conflicting keys.
  • Arrays are replaced entirely by the later file (no per‑element merge).
  • Returns descriptive errors for decode issues, trailing data, or invalid root types.

func (c *Config) GetAllConfig() map[string]any

Returns a deep copy of the effective configuration after applying layers: files → ENV → runtime.
Useful for diagnostics, /config debug endpoints, or exporting the current view.

func (c *Config) GetConfig(field string, defaultValue ...string) any

Gets a value using a case‑insensitive dot‑path with array indexes.

  • Checks runtime → ENV → files.
  • If found, returns the native type (bool, int, float64, string, map[string]any, []any, etc.).
  • If not found and a default string is provided, returns that default.
  • If not found and no default is provided, returns MissingBehavior (default: ""), or the field name if set to "field".

Examples

cfg.GetConfig("db.host")                // e.g., "localhost"
cfg.GetConfig("addresses")              // []any of maps
cfg.GetConfig("addresses[0]")           // map[string]any
cfg.GetConfig("addresses[1].area")      // e.g., "Kichi"

func (c *Config) SetConfig(field string, value any)

Sets a runtime (in‑memory) value by path. This is the highest precedence layer.

  • Creates missing containers as needed.
  • For arrays, setting arr[5] expands the array to length 6 with nil fillers.

func (c *Config) DeleteConfig(field string)

Deletes a runtime value at path.

  • Map keys are removed.
  • Array elements are set to nil (array length is preserved).

API — Typed Getters

These helpers return a specific type, consulting runtime → ENV → files. They ignore MissingBehavior and return your default (if provided) or the zero value.

func (c *Config) GetString(field string, defaultValue ...string) string
func (c *Config) GetInt(field string, defaultValue ...int) int
func (c *Config) GetBool(field string, defaultValue ...bool) bool
func (c *Config) GetFloat(field string, defaultValue ...float64) float64
// Duration in nanoseconds; accepts numeric ns or time.ParseDuration strings
func (c *Config) GetDuration(field string, defaultValue ...int64) int64

Examples

cfg.GetString("db.host", "localhost")
cfg.GetInt("db.port", 5432)
cfg.GetBool("db.ssl", false)
cfg.GetFloat("service.timeoutSec", 5.0)
cfg.GetDuration("cache.ttl", int64(time.Second))

Path Syntax & Rules

  • Dot segments → map/object keys: server.tls.enabled
  • Brackets → array index: addresses[2].area
  • Numeric dot segment → also array index: addresses.2.area
  • Case‑insensitive for map keys (not for indices): DB.Host == db.host.
  • Invalid paths return missing behavior (or defaults in typed getters).

Merge Semantics

  • Maps: deep‑merge recursively; later file overrides only the keys it provides.
  • Arrays: later file replaces the prior array at that key.

Precedence Revisited

  1. Runtime SetConfig — high‑priority, in‑memory, not persisted
  2. Environment variablesENVY_ prefix (configurable), path → UPPER_SNAKE, indices as digits
  3. Files — order of Load; later files override earlier

Troubleshooting

  • Unsupported extension: Only .json, .yaml, .yml are accepted.
  • Invalid root: Root must be an object (map).
  • Array operations: Setting an out‑of‑range index expands with nil. Deleting an element sets nil.

License

MIT

Open Source & Contributions

This project is open source. Contributions of all sizes are welcome—bug fixes, tests, docs, examples, and new features. Please open an issue to discuss ideas or submit a pull request when you are ready.

About

go-envy is a lean, production‑ready configuration library for golang support JSONand YAML configuration

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages