Config
Config module: layered application configuration with typed keys.
Every configuration value lives under a typed KEY. A key names a path ((ns, name) or (ns, section, name), all atoms) and carries a codec that fixes the value's type:
let port = Config.key(:myapp, :port, Config.int()) -- Key(Int)
let host = Config.key_in(:myapp, :db, :host, Config.string()) -- Key(String)
Config.put(port, 4000)
Config.get(port) -- => Some(4000)
Config.put(host, "localhost")
Config.get(host) -- => Some("localhost")Storage is heterogeneous (an Int port and a String host share one table), but a read can never hand back a value at the wrong type. Two things guarantee that:
- statically,
put(key : Key(v), value : v)and `get(key : Key(v)) :
Option(v)` tie the value to the key's type, so storing an Int through
`port` and using the result of `get(port)` as a Pid is a type error;
* at runtime, each value is stored as a tagged `Config.Value` and decoded
by the READING key's codec. Two keys minted for the same path with
different codecs (`key(:a, :b, int())` and `key(:a, :b, string())`)
therefore disagree safely: the read is `None` from `get` and
`Err(KeyWrongType(...))` from `fetch`, never an unchecked value.Built-in codecs: int(), float(), string(), bool(), atom(), list(codec). Custom types: Config.codec(name, encode, decode), mapping onto Value.
Load from environment at startup: Config.from_env(Config.key(:myapp, :secret, Config.string()), "SECRET_KEY_BASE", "") Config.from_env_int(port, "PORT", 4000)
Validate values: Config.validate(port, fn p -> p > 0 && p < 65536)
Detect current environment (reads MARCH_ENV): Config.env() -- :dev | :test | :prod
Endpoint config shorthand (for web servers): Config.put_endpoint(4000, "localhost", my_secret) Config.endpoint_port(4000) Config.endpoint_host("localhost") Config.secret_key_base()
Named stores (for isolation in tests): let store = Config.new_store("my_test_store") Config.store_put(store, port, 4000) Config.store_get(store, port)
Types
A stored configuration value. This closed, tagged sum is what the config table actually holds, which is what makes a heterogeneous table safe to read: a key's codec decodes a Value back to its own type and rejects any other tag.
How a key's values are stored and read back: a type name (used in error messages), an encoder into Value, and a decoder that returns None for a Value of any other shape. Build one with int(), float(), string(), bool(), atom(), list(codec), or codec(name, encode, decode).
A typed configuration key: a path plus the codec fixing its value type.
Why a typed read produced no value. KeyMissing(path) means nothing is stored under the path; KeyWrongType(path, expected, found) means a value is stored there, but it was written through a key of another type.
Functions
Codec for Atom values.
Examples
march> let s = Config.new_store("codec_atom_demo")
march> let k = Config.key(:myapp, :mode, Config.atom())
march> Config.store_put(s, k, :fast)
march> let s = Config.new_store("codec_atom_demo")
march> let k = Config.key(:myapp, :mode, Config.atom())
march> Config.store_get(s, k)
Some(:fast)Codec for Bool values.
Examples
march> let s = Config.new_store("codec_bool_demo")
march> let k = Config.key(:myapp, :debug, Config.bool())
march> Config.store_put(s, k, true)
march> let s = Config.new_store("codec_bool_demo")
march> let k = Config.key(:myapp, :debug, Config.bool())
march> Config.store_get(s, k)
Some(true)A custom codec: store a v as a Value and read it back. name appears in KeyWrongType errors. Because Value is a closed sum, any codec that typechecks is safe; a decoder can only produce a v it can build from a Value.
type Mode = Fast | Safe
fn encode_mode(m : Mode) : Config.Value do
match m do
Fast -> CfgAtom(:fast)
Safe -> CfgAtom(:safe)
end
end
fn decode_mode(d : Config.Value) : Option(Mode) do
match d do
CfgAtom(:fast) -> Some(Fast)
CfgAtom(:safe) -> Some(Safe)
_ -> None
end
end
let mode = Config.key(:myapp, :mode, Config.codec("Mode", encode_mode, decode_mode))Get the configured host (endpoint_host_key()). Returns default_host if not set.
Examples
march> Config.put(Config.endpoint_host_key(), "example.test")
march> Config.endpoint_host("localhost")
"example.test"The key put_endpoint stores the host under: (:bastion, :host).
Examples
march> Config.path(Config.endpoint_host_key())
":bastion/:host"Get the configured HTTP port (endpoint_port_key()). Returns default_port if not set.
Examples
march> Config.put(Config.endpoint_port_key(), 8443)
march> Config.endpoint_port(80)
8443The key put_endpoint stores the HTTP port under: (:bastion, :port).
Examples
march> Config.path(Config.endpoint_port_key())
":bastion/:port"Returns the current application environment as an atom. Reads the MARCH_ENV environment variable: "prod" gives :prod, "test" gives :test, and anything else (including unset) gives :dev.
match Config.env() do
:prod -> enable_caching()
_ -> ()
endRender an Error as a one-line message.
Examples
march> Config.error_to_string(Config.KeyMissing(":myapp/:port"))
"Config: key :myapp/:port not set"
march> Config.error_to_string(Config.KeyWrongType(":myapp/:port", "Int", "String"))
"Config: key :myapp/:port holds a value of type String, expected Int"Look up key, returning Ok(value), Err(KeyMissing(path)), or Err(KeyWrongType(path, expected, found)), where expected is the reading codec's type name and found the stored value's (value_type_name). Reading the :test/:port Int stored in the example below through a String key gives Err(KeyWrongType(":test/:port", "String", "Int")).
Examples
march> let port = Config.key(:myapp, :port, Config.int())
march> let k = Config.key(:test, :port, Config.int())
march> Config.fetch(k)
Err(KeyMissing(":test/:port"))
march> let k = Config.key(:test, :port, Config.int())
march> let port = Config.key(:myapp, :port, Config.int())
march> Config.put(k, 8080)
march> let k = Config.key(:test, :port, Config.int())
march> let port = Config.key(:myapp, :port, Config.int())
march> Result.unwrap_or(Config.fetch(k), 0)
8080
march> let port = Config.key(:myapp, :port, Config.int())
march> Result.is_err(Config.fetch(Config.key(:test, :port, Config.string())))
trueCodec for Float values.
Examples
march> let s = Config.new_store("codec_float_demo")
march> let k = Config.key(:myapp, :ratio, Config.float())
march> Config.store_put(s, k, 0.5)
march> let s = Config.new_store("codec_float_demo")
march> let k = Config.key(:myapp, :ratio, Config.float())
march> Config.store_get(s, k)
Some(0.5)Load a String value from an environment variable and store it under key. Uses default_val if the env var is not set (a set-but-empty variable stores ""). Always stores, overwriting any previous value; returns the value stored.
Examples
march> let secret = Config.key(:myapp, :secret, Config.string())
march> Config.from_env(secret, "MARCH_DOCTEST_NEVER_SET", "dev-secret")
"dev-secret"
march> let secret = Config.key(:myapp, :secret, Config.string())
march> Config.get_with_default(secret, "")
"dev-secret"Load a Bool value from an environment variable and store it under key. Exactly "true", "1" and "yes" (case-sensitive) are parsed as true; any other value that is set (including "TRUE" or "") stores false. Uses default_val only when the variable is absent. Returns the value stored.
Examples
march> Config.from_env_bool(Config.key(:myapp, :verbose, Config.bool()), "MARCH_DOCTEST_NEVER_SET", true)
trueLoad an Int value from an environment variable and store it under key. Parses the string as an integer; uses default_val if absent or unparseable. Always stores; returns the value stored.
Examples
march> Config.from_env_int(Config.key(:myapp, :workers, Config.int()), "MARCH_DOCTEST_NEVER_SET", 4)
4Look up key. Returns Some(value), or None when the key is unset OR holds a value written through a key of another type. Use fetch to tell the two apart.
Examples
march> let host = Config.key(:myapp, :host, Config.string())
march> Config.put(host, "localhost")
march> let host = Config.key(:myapp, :host, Config.string())
march> Option.unwrap_or(Config.get(host), "unset")
"localhost"
march> let host = Config.key(:myapp, :host, Config.string())
march> Config.get(Config.key(:myapp, :host, Config.int()))
NoneGet key's value, or default_val when it is unset or of another type.
Examples
march> Config.get_with_default(Config.key(:myapp, :timeout, Config.int()), 30)
30Codec for Int values.
Examples
march> let s = Config.new_store("codec_int_demo")
march> let k = Config.key(:myapp, :workers, Config.int())
march> Config.store_put(s, k, 4)
march> let s = Config.new_store("codec_int_demo")
march> let k = Config.key(:myapp, :workers, Config.int())
march> Config.store_get(s, k)
Some(4)True unless MARCH_ENV is \
True if MARCH_ENV is exactly \
True if MARCH_ENV is exactly \
A typed key for the 2-level path (ns, name). The codec fixes the value type: Config.key(:myapp, :port, Config.int()) : Key(Int). Building a key stores nothing; two keys with the same atoms share one path.
Examples
march> Config.path(Config.key(:test, :port, Config.int()))
":test/:port"A typed key for the 3-level path (ns, section, name): Config.key_in(:myapp, :db, :host, Config.string()) : Key(String).
Examples
march> Config.path(Config.key_in(:test, :dev, :host, Config.string()))
":test/:dev/:host"Codec for a list whose elements use elem. Decoding fails (None) unless every element decodes. Its type name is "List(" ++ elem's name ++ ")".
Examples
march> let s = Config.new_store("codec_list_demo")
march> let k = Config.key(:myapp, :tags, Config.list(Config.atom()))
march> Config.store_put(s, k, [:a, :b])
march> let s = Config.new_store("codec_list_demo")
march> let k = Config.key(:myapp, :tags, Config.list(Config.atom()))
march> Config.store_get(s, k)
Some([:a, :b])Create (or reopen) an isolated named config store. Useful for testing or multi-tenant configurations. Returns a store handle holding tagged Values; use it with store_put, store_get and store_fetch and the same typed keys as the global table. Calling it again with the same name returns the same store, contents included.
Examples
march> let port = Config.key(:myapp, :port, Config.int())
march> let k = Config.key(:myapp, :port, Config.int())
march> let store = Config.new_store("new_store_demo")
march> Config.store_put(store, k, 1)
march> let k = Config.key(:myapp, :port, Config.int())
march> let port = Config.key(:myapp, :port, Config.int())
march> Option.unwrap_or(Config.store_get(Config.new_store("new_store_demo"), k), 0)
1The key's path as stored: the atoms (each with its leading :) joined by /, e.g. ":myapp/:port".
Examples
march> Config.path(Config.endpoint_port_key())
":bastion/:port"Store value under key in the global config table, replacing any previous value at that path (whatever its type). The table is process-wide and lives until the program exits.
Examples
march> let port = Config.key(:myapp, :port, Config.int())
march> Config.put(port, 4000)
march> let port = Config.key(:myapp, :port, Config.int())
march> Config.get_with_default(port, 0)
4000Store web endpoint configuration (port, host and secret key base) in the global table under the endpoint_*_key() keys. Typically called during application startup.
Examples
march> Config.put_endpoint(4000, "localhost", "s3cret")
march> (Config.endpoint_port(80), Config.endpoint_host("0.0.0.0"))
(4000, "localhost")Get a required configuration value. Panics with the error_to_string message when the key is unset or holds a value of another type; use fetch for a recoverable read.
Examples
march> let host = Config.key(:myapp, :host, Config.string())
march> let db = Config.key(:test, :host, Config.string())
march> Config.put(db, "db.local")
march> let host = Config.key(:myapp, :host, Config.string())
march> let db = Config.key(:test, :host, Config.string())
march> Config.require(db)
"db.local"Get the secret key base (secret_key_base_key()). Returns "" if not configured.
Examples
march> Config.put(Config.secret_key_base_key(), "s3cret")
march> Config.secret_key_base()
"s3cret"The key put_endpoint stores the secret under: (:bastion, :secret_key_base).
Examples
march> Config.path(Config.secret_key_base_key())
":bastion/:secret_key_base"Look up key in a named config store, with an Error on failure (see fetch).
Examples
march> let host = Config.key(:myapp, :host, Config.string())
march> let store = Config.new_store("store_fetch_demo")
march> Config.store_fetch(store, Config.key(:test, :host, Config.string()))
Err(KeyMissing(":test/:host"))Look up key in a named config store. None when unset or of another type (see get).
Examples
march> let store = Config.new_store("store_get_demo")
march> Config.store_get(store, Config.key(:myapp, :limit, Config.int()))
NoneStore value under key in a named config store; the global table is not touched.
Examples
march> let k = Config.key(:myapp, :retries, Config.int())
march> let store = Config.new_store("store_put_demo")
march> Config.store_put(store, k, 3)
march> let k = Config.key(:myapp, :retries, Config.int())
march> let store = Config.new_store("store_put_demo")
march> Option.unwrap_or(Config.store_get(store, k), 0)
3
march> let k = Config.key(:myapp, :retries, Config.int())
march> Option.is_none(Config.get(k))
trueCodec for String values.
Examples
march> let s = Config.new_store("codec_string_demo")
march> let k = Config.key(:myapp, :name, Config.string())
march> Config.store_put(s, k, "demo")
march> let s = Config.new_store("codec_string_demo")
march> let k = Config.key(:myapp, :name, Config.string())
march> Config.store_get(s, k)
Some("demo")Validate a config value with a predicate function. Returns Ok(value) if the key is set and the predicate returns true; otherwise Err with the error_to_string message for an unset or wrong-typed key, or "Config: validation failed" when the predicate rejects the value.
Examples
march> let port = Config.key(:myapp, :port, Config.int())
march> let p = Config.key_in(:myapp, :http, :port, Config.int())
march> Config.put(p, 8080)
march> let port = Config.key(:myapp, :port, Config.int())
march> let p = Config.key_in(:myapp, :http, :port, Config.int())
march> Config.validate(p, fn n -> n > 0 && n < 65536)
Ok(8080)
march> let port = Config.key(:myapp, :port, Config.int())
march> let p = Config.key_in(:myapp, :http, :port, Config.int())
march> Result.is_err(Config.validate(p, fn n -> n < 1024))
true