March Docs

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

typeValueValue = CfgInt(Int) | CfgFloat(Float) | CfgString(String)#

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.

typeCodecCodec(v) = Codec(String, v -> Value, Value -> Option(v))#

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).

typeKeyKey(v) = Key(String, Codec(v))#

A typed configuration key: a path plus the codec fixing its value type.

typeErrorError = KeyMissing(String) | KeyWrongType(String, String, String)#

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

fnatomatom() : Codec(Atom) do Codec("Atom", fn a -> CfgAtom(a), decode_atom) end#

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)
fnboolbool() : Codec(Bool) do Codec("Bool", fn b -> CfgBool(b), decode_bool) end#

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)
fncodeccodec(name : String, encode : v -> Value, decode : Value -> Option(v)) : Codec(v)#

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))
fnendpoint_hostendpoint_host(default_host : String) : String#

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"
fnendpoint_host_keyendpoint_host_key() : Key(String) do key(:bastion, :host, string()) end#

The key put_endpoint stores the host under: (:bastion, :host).

Examples

march> Config.path(Config.endpoint_host_key())
":bastion/:host"
fnendpoint_portendpoint_port(default_port : Int) : Int#

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)
8443
fnendpoint_port_keyendpoint_port_key() : Key(Int) do key(:bastion, :port, int()) end#

The key put_endpoint stores the HTTP port under: (:bastion, :port).

Examples

march> Config.path(Config.endpoint_port_key())
":bastion/:port"
fnenvenv()#

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()
  _     -> ()
end
fnerror_to_stringerror_to_string(e : Error) : String#

Render 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"
fnfetchfetch(k : Key(v)) : Result(v, Error)#

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())))
true
fnfloatfloat() : Codec(Float) do Codec("Float", fn f -> CfgFloat(f), decode_float) end#

Codec 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)
fnfrom_envfrom_env(k : Key(String), env_var : String, default_val : String) : String#

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"
fnfrom_env_boolfrom_env_bool(k : Key(Bool), env_var : String, default_val : Bool) : Bool#

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)
true
fnfrom_env_intfrom_env_int(k : Key(Int), env_var : String, default_val : Int) : Int#

Load 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)
4
fngetget(k : Key(v)) : Option(v)#

Look 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()))
None
fnget_with_defaultget_with_default(k : Key(v), default_val : v) : v#

Get 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)
30
fnintint() : Codec(Int) do Codec("Int", fn n -> CfgInt(n), decode_int) end#

Codec 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)
fnis_devis_dev()#

True unless MARCH_ENV is \

fnis_prodis_prod()#

True if MARCH_ENV is exactly \

fnis_testis_test()#

True if MARCH_ENV is exactly \

fnkeykey(ns : Atom, name : Atom, c : Codec(v)) : Key(v)#

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"
fnkey_inkey_in(ns : Atom, section : Atom, name : Atom, c : Codec(v)) : Key(v)#

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"
fnlistlist(elem : Codec(v)) : Codec(List(v))#

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])
fnnew_storenew_store(name : String) : Vault(Value)#

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)
1
fnpathpath(k : Key(v)) : String#

The 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"
fnputput(k : Key(v), value : v) : Unit#

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)
4000
fnput_endpointput_endpoint(port : Int, host : String, secret : String) : Unit#

Store 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")
fnrequirerequire(k : Key(v)) : v#

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"
fnsecret_key_basesecret_key_base() : String#

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"
fnsecret_key_base_keysecret_key_base_key() : Key(String) do key(:bastion, :secret_key_base, string()) end#

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"
fnstore_fetchstore_fetch(store : Vault(Value), k : Key(v)) : Result(v, Error)#

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"))
fnstore_getstore_get(store : Vault(Value), k : Key(v)) : Option(v)#

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()))
None
fnstore_putstore_put(store : Vault(Value), k : Key(v), value : v) : Unit#

Store 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))
true
fnstringstring() : Codec(String) do Codec("String", fn s -> CfgString(s), decode_string) end#

Codec 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")
fnvalidatevalidate(k : Key(v), predicate : v -> Bool) : Result(v, String)#

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
fnvalue_type_namevalue_type_name(d : Value) : String#

The type name of a stored Value, as shown in KeyWrongType. A list is just "List", whatever its elements.

Examples

march> Config.value_type_name(Config.CfgInt(1))
"Int"
march> Config.value_type_name(Config.CfgList([Config.CfgBool(true)]))
"List"