The resolver package provides a flexible and extensible way to resolve configuration values from various sources including environment variables, plain files, JSON, YAML, INI, TOML, and key-value files.
It uses a prefix-based system to determine which resolver to use, and returns either the resolved value or an error.
go get github.com/containeroo/resolver/resolverThe primary entry point is the ResolveVariable function.
It takes a string and attempts to resolve it based on its prefix:
-
env:- Environment variables. Example:env:PATH→ value of
$PATH. -
file:- Simple key-value files. SupportsKEY=VALlines, with optionalexportprefixes and#comments. Example:file:/config/app.txt//USERNAME→ value of
USERNAMEinapp.txt. -
json:- JSON files. Supports dot-notation for nested keys and array indexing. Examples:json:/config/app.json//server.host json:/config/app.json//servers.0.host json:/config/app.json//servers.[name=api].port -
yaml:- YAML files. Same dot/array/filter notation as JSON. Example:yaml:/config/app.yaml//servers.[host=example.org].port -
ini:- INI files. Supports section+key or default section. Examples:ini:/config/app.ini//Database.User ini:/config/app.ini//Key1 -
toml:- TOML files. Dot-notation for nested keys and array indexing. Example:toml:/config/app.toml//server.host -
No prefix - Returns the value unchanged. Example:
just-a-literal→
"just-a-literal".
Interpolate ${...} tokens inside a larger string and resolve each token with the same rules as ResolveVariable.
// Replace ${...} tokens using the default registry (up to 8 passes).
func ResolveString(s string) (string, error)
// Registry method, if you use a custom registry:
func (*Registry) ResolveString(s string) (string, error)Features & rules
-
${scheme:...}tokens are resolved; the${...}wrapper is removed. -
\${emits a literal"${"(escape) and is not expanded. -
A bare
$not followed by{is copied literally. -
Malformed tokens error with
ErrBadPath:- missing closing
}(e.g.,"${env:HOME") - empty token
"${}"
- missing closing
-
Multi-pass expansion: tokens that produce new
${...}are expanded in subsequent passes (depth limit 8). -
Unknown schemes follow your registry policy:
- Default (PassThrough): the token's content is inserted unchanged (e.g.,
"${nosuch:x}" → "nosuch:x"). - ErrorOnUnknown: unknown tokens yield
ErrNotFound.
- Default (PassThrough): the token's content is inserted unchanged (e.g.,
Examples
os.Setenv("USER", "alice")
s, _ := resolver.ResolveString("db://u=${env:USER}@${json:/cfg/app.json//db.host}")
// → "db://u=alice@localhost"
s, _ = resolver.ResolveString(`literal \${env:USER}`)
// → "literal ${env:USER}"
s, _ = resolver.ResolveString("price is $$5 (not a token)")
// → "price is $$5 (not a token)"
// Multi-pass: a token that expands to another token
r := resolver.NewRegistry()
resolver.RegisterResolver("a:", resolver.ResolverFunc(func(_ string) (string, error) { return "${b:x}", nil }))
resolver.RegisterResolver("b:", resolver.ResolverFunc(func(_ string) (string, error) { return "OK", nil }))
s, _ = r.ResolveString("s=${a:any}")
// → "s=OK"When you need to resolve a list of strings (e.g., CLI args, YAML arrays), use the slice helpers. Both preserve order, return a new slice, and leave inputs unchanged. Unknown schemes still pass through unchanged, just like ResolveVariable.
func ResolveSlice(values []string) ([]string, error)Resolves each element using the default registry. If any element fails, the function stops at the first error and returns it. No partial results are returned.
func ResolveSliceBestEffort(values []string) ([]string, []error)Attempts to resolve all elements and never fails fast. Returns:
out: resolved values (same length as input; failed items are"").errs: per-index errors you can inspect or log.
Registry methods are also available:
(*Registry).ResolveSliceand(*Registry).ResolveSliceBestEffort.
package main
import (
"fmt"
"log"
"os"
"github.com/containeroo/resolver"
)
func main() {
os.Setenv("MY_VAR", "HelloWorld")
val, err := resolver.ResolveVariable("env:MY_VAR")
if err != nil {
log.Fatal(err)
}
fmt.Println(val) // Output: HelloWorld
// Example: resolve a JSON key
// File: /config/app.json
// {
// "server": {
// "host": "localhost",
// "port": 8080
// }
// }
host, err := resolver.ResolveVariable("json:/config/app.json//server.host")
if err != nil {
log.Fatal(err)
}
fmt.Println(host) // Output: localhost
// Interpolation example
os.Setenv("USER", "alice")
s, err := resolver.ResolveString("db://${env:USER}@${json:/config/app.json//server.host}")
if err != nil {
log.Fatal(err)
}
fmt.Println(s) // db://alice@localhost
}You can register your own resolver schemes at runtime:
resolver.RegisterResolver("secret:", myCustomResolver)Resolvers must implement:
type Resolver interface {
Resolve(value string) (string, error)
}You can also use the ergonomic adapter:
resolver.RegisterResolver("secret:", resolver.ResolverFunc(func(v string) (string, error) {
return fetchSecret(v), nil
}))This allows you to plug in custom backends (e.g., Vault, Consul, HTTP endpoints).