Pax.Config (Pax v0.0.1-dev)
View SourcePax.Config
is a module for defining configuration structs and functions for Pax. It has two main concerns:
- Ingest and validate configuration data, checking for common mistakes and typos.
- Resolve configuration values for code that is configurable, calling functions and checking return types.
Ingesting and validation
Configuration data supplied by a developer is validated against a specification of expected keys and their possible types. These types are just basic Elixir types, such as would be checked with a guard clause. This is not intended to be a complete type system, just enough to help the developer if they make a mistake.
Supported Types
Config type | Elixir type | Example |
---|---|---|
nil | nil | nil |
:atom | atom() | :foo |
:string | binary() | "foo" |
:boolean | boolean() | true , false |
:integer | integer() | 42 |
:float | float() | 42.0 |
:tuple | tuple() | {1, 2, 3} |
:list | list() | [1, 2, 3] |
:map | map() | %{foo: "bar"} |
:module | module() | MyProject.Module |
:struct | struct() | %MyStruct{name: "foo"} |
{:struct, Module} | struct() | {:struct, MyStruct} => %MyStruct{name: "foo"} |
:date | Date | ~D[2020-01-01] |
:time | Time | ~T[12:00:00] |
:naive_datetime | NaiveDateTime | ~N[2020-01-01 12:00:00] |
:datetime | DateTime | ~U[2020-01-01 12:00:00Z] |
:uri | URI | URI.parse("https://example.com") |
:function | function() | :function => fn -> "foo" end |
{:function, arity} | function() | {:function, 1} => fn x -> x end |
{:function, type} | function() | {function, :atom} => fn -> :foo end |
{:function, types} | function() | {function, [nil, :atom, :string]} => fn -> "foo" end |
{:function, arity, type} | function() | {:function, 2, :integer} => fn x, y -> x * y end |
{:function, arity, types} | function() | {:function, 2, [:integer, nil]} => fn _, _ -> nil end |
The spec of types can either be a single type, or a list of types. If a list of types is provided, the value must match at least one of the types in the list.
Any extra keys in the config data that are not expected will return an error.
A map of validated configuration is returned that can be used with fetch/3
, fetch!/3
and get/4
to resolve the
configuration values.
Nested Configuration
The spec can be a map of keys to either types, or another map of keys to types. This allows for nested configuration data.
For example, you can have a spec like the following, then the data must conform to the spec with the same nesting.
iex> spec = %{
...> foo: [:integer, {:function, 1, :integer}],
...> bar: %{
...> baz: [:string]
...> }
...> }
...> data = [
...> foo: 42,
...> bar: [
...> baz: "hello"
...> ]
...> ]
...> {:ok, _config} = Pax.Config.validate(spec, data)
{:ok,
%{
foo: {:integer, 42},
bar: %{baz: {:string, "hello"}}
}}
To fetch the configured value of nested keys, you can use the fetch/3
, fetch!/3
and get/4
functions with a
list of keys, similar to the get_in/2
function in Elixir.
iex> spec = %{foo: [:integer, {:function, 1, :integer}], bar: %{:baz => [:string]}}
...> data = [foo: 42, bar: [baz: "hello"]]
...> config = Pax.Config.validate!(spec, data)
...> Pax.Config.fetch(config, :foo)
{:ok, 42}
...> Pax.Config.get(config, [:bar, :baz])
"hello"
Resolving configuration values
Since configuration values can be functions, this module provides a way to call those functions and get the resolved value. This is useful for code that wants to use the configuration data without having to know how to call functions. If the spec allows a function, then when you get the value you must pass the args (if any) that would be required to resolve the function. The args are only used if the user supplied an anonymous function as the value for that config key.
For example, if your spec is like the following, then when you fetch the config value for the :foo
config key, you
must pass the argument expected by the function, even if the user hasn't provided a function, and instead just
provided a value.
iex> spec = %{
...> foo: [:integer, {:function, 1, :integer}]
...> }
...>
...> # The user has provided an integer instead of a function returning an integer, but when
...> # fetching the value, you still must pass the argument expected by the function
...> data = %{foo: 42}
...> config = Pax.Config.validate!(spec, data)
...> Pax.Config.fetch(config, :foo, [:arg])
{:ok, 42}
...>
...> # This is so if the user provided a function, then fetching the value will be able
...> # to pass that arg to the user-supplied function.
...> data = %{foo: fn arg -> if arg == :one, do: 1, else: 0 end}
...> {:ok, config} = Pax.Config.validate(spec, data)
...> Pax.Config.fetch(config, :foo, [:one])
{:ok, 1}
Summary
Functions
Fetch a value for a specific key
from a config
map. Returns {:ok, value}
if the key is found in the config,
otherwise :error
.
Fetch a value for a specific key
from a config
map, raising an error if the key is not found.
Gets the value for a specific key
from a config
map, returning the default
if not found.
Validate user-provided configuration data
against a configuration spec
.
Validate user-provided configuration data
against a configuration spec
, raising an error if the data does not
conform.
Functions
Fetch a value for a specific key
from a config
map. Returns {:ok, value}
if the key is found in the config,
otherwise :error
.
The config
map must be the result of a call to validate/3
.
Either an individual key
can be given, or a list of keys
if the configuration data is nested. See
Nested Configuration for more information.
In the case that a function is allowed in the spec, then the correct args
must be passed to this function to
resolve the value. If no functions are allowed by the spec, the args can be omitted.
An ArgumentError
will be raised if the config is not a map with the correct structure, as returned from
validate/3
.
A Pax.Config.TypeError
will be raised if the function provided by the user does not return the correct type, as
specified by the spec.
A Pax.Config.ArityError
will be raised if the count of args given to this function do not match one of the specs
for the given key.
Fetch a value for a specific key
from a config
map, raising an error if the key is not found.
The config
map must be the result of a call to validate/3
.
Either an individual key
can be given, or a list of keys
if the configuration data is nested. See
Nested Configuration for more information.
In the case that a function is allowed in the spec, then the correct args
must be passed to this function to resolve
the value. If no functions are allowed by the spec, the args can be omitted.
A KeyError
will be raised if the key is not found in the configuration, which means it was not in the
data given to validate/3
.
An ArgumentError
will be raised if the config is not a map with the correct structure, as returned from
validate/3
.
A Pax.Config.TypeError
will be raised if the function provided by the user does not return the correct type, as
specified by the spec.
A Pax.Config.ArityError
will be raised if the count of args given to this function do not match one of the specs
for the given key.
@spec get( config :: map(), key_or_keys :: atom() | [atom(), ...], args :: list(), default :: any() ) :: any()
Gets the value for a specific key
from a config
map, returning the default
if not found.
The config
map must be the result of a call to validate/3
.
Either an individual key
can be given, or a list of keys
if the configuration data is nested. See
Nested Configuration for more information.
In the case that a function is allowed in the spec, then the correct args
must be passed to this function to
resolve the value. If no functions are allowed by the spec, the args
can be omitted.
Important Note
If you are passing a default
that is a list, then you need to use the get/4
function with an empty list
for the args
, otherwise the default
will be treated as the args
, and the default
will actually be nil
iex> Pax.Config.get(config, :key, [], [1, 2, 3])
An ArgumentError
will be raised if the config is not a map with the correct structure, as returned from
validate/3
.
A Pax.Config.TypeError
will be raised if the function provided by the user does not return the correct type, as
specified by the spec.
A Pax.Config.ArityError
will be raised if the count of args given to this function do not match one of the specs
for the given key.
@spec validate(spec :: map(), data :: map() | keyword(), opts :: keyword()) :: {:ok, map()} | {:error, term()}
Validate user-provided configuration data
against a configuration spec
.
The spec
is a map of expected keys and the type (or types) that the data should provide. Please see
Supported Types for the list of allowed types.
If the spec
has has any errors then a Pax.Config.SpecError
will be raised.
If the data
does conform to the spec, then {:ok, config}
will be returned, where config
is a map of
validated configuration that can be used with the fetch/3
, fetch!/3
and get/4
functions to resolve the value.
If the data not not conform to the spec, then {:error, reason}
will be returned, where reason
is a string
describing the error.
Only keys that are given in the data will be returned in the config. If the spec has a key that is not in the data, then it will not be in the config.
Options
:validate_config_spec
- Validate the spec itself. This is useful to turn on when developing an adapter or plugin, but should be turned off in production to avoid unnecessary checks. Defaults tofalse
unless the application env:validate_config_spec
is set totrue
in the:pax
application at compile time. E.g. in "config/dev.exs":config :pax, validate_config_spec: true
If it is changed, you will need to
mix deps.compile --force pax
to recompile pax with the new value.
Validate user-provided configuration data
against a configuration spec
, raising an error if the data does not
conform.
The spec
is a map of expected keys and the type (or types) that the data should provide. Please see
Supported Types for the list of allowed types.
If the data
does conform to the spec, then {:ok, config}
will be returned, where config
is a map of
validated configuration that can be used with the fetch/3
, fetch!/3
and get/4
functions to resolve the value.
Only keys that are given in the data will be returned in the config. If the spec has a key that is not in the data, then it will not be in the config.
If the data does not form to the spec, then a Pax.ConfigError
will be raised.
Options
:validate_config_spec
- Validate the spec itself, raising aPax.Config.SpecError
in the case of errors. This is useful to turn on when developing an adapter or plugin, but should be turned off in production to avoid unnecessary checks. Defaults tofalse
unless the application env:validate_config_spec
is set totrue
in the:pax
application at compile time. E.g. in "config/dev.exs":config :pax, validate_config_spec: true
If it is changed, you will need to
mix deps.compile --force pax
to recompile pax with the new value.