After two long topics (1 , 2) dedicated to how to best configure your elixir application,
talking with a couple of people at ElixirConf.eu about the current state of configuring applications,
and recognizing some patterns (both good and bad) in the Elixir libraries I and others have written in the last couple of years,
here is my take on the challenge of configuring applications and libraries in Elixir:
Comfortable, Explicit, multi-Layered configuration specifications.
Specify is an Elixir library to standardize the method of doing configuration for your applications and libraries. Rather than trying to force configuration to happen in one way, it enables both library writers and library consumers to specify on multiple different levels how they want to configure something.
The main goals that Specify has in mind are:
- Configuration is based on looking at a stack of (also per-specification and globally configurable!) configuration sources, falling back to defaults written in the configuration specification.
- Overriding the values in this stack of configuration using plain arguments passed to a function is also always possible, circumventing the ‘singleton problem’ that many configurable things suffer from.
- It is very clear what values are supported for a given configuration field, since a validation/parsing function is named when specifying it.
- Documentation is automatically generated from a configuration specification, listing names, descriptions, parser functions and default values.
- Normalization: After a call tto
YourConfig.load
you end up with an Elixir struct containing the configuration, with all fields having passed validation and parsing, so you know exactly what fields are there to use and what kind of values to expect. This greatly reduces the number of weird ambiguous errors when something is misconfigured. - Fail-fast: The library raises errors on missing required configuration, things that cannot be parsed and attempting to override unexistent fields. The library raises compilation warnings on missing documentation. The library logs at ‘error’-log-level when a source cannot be reached during an attempted configuration load.
Example
defmodule Cosette.CastleOnACloud do
require Specify
Specify.defconfig sources: [Specify.Providers.MixEnv, Specify.Providers.Process] do
@doc "there are no floors for me to sweep"
field :floors_to_sweep, :integer, default: 0
@doc "there are a hundred boys and girls"
field :amount_boys_and_girls, :integer, default: 100
@doc "The lady all in white holds me and sings a lullaby"
field :lullaby, :string
@doc "Crying is usually not allowed"
field :crying_allowed, :boolean, default: false
end
end
Trying to load this using Cosette.CatleOnACloud.load()
will fail because a mandatory field is missing.
iex> Cosette.CastleOnACloud.load
** (Specify.MissingRequiredFieldsError) Missing required fields for `Elixir.Cosette.CastleOnACloud`: `:lullaby`.
(specify) lib/specify.ex:179: Specify.prevent_missing_required_fields!/3
(specify) lib/specify.ex:147: Specify.load/2
However, by configuring it using
Application.put_env(Cosette.CastleOnACloud, :lullaby, "I love you very much")
# or, in your config/conf.exs file:
config Cosette.CastleOnACloud, lullaby: "I love you very much"
or instead, using the Process Dictionary source:
Process.put(Cosette.CastleOnACloud, :lullaby, "I love you very much")
or specifying an inline lullaby value as part of the overrides:
argument:
iex> Cosette.CastleOnACloud.load(overrides: [lullaby: "I love you very much", crying_allowed: true])
we end up with:
%Cosette.CastleOnACloud{
crying_allowed: true,
floors_to_sweep: 0,
lullaby: "I love you very much",
amount_boys_and_girls: 100
}
Roadmap
Specify is not yet done. Most importantly, it needs some tests and some more effort to make sure that its interface is understandable not only to me but also to the rest of the community .
Also, at the very least I still want to create an environment variables provider (both for .env
-files and System.get_env
).
I am fairly certain that its documentation and examples can be improved to be more understandable. Besides this, I would love to hear answers to the following practical questions:
Does Specify work with how you are used to doing configuration, or does it clash, and in what way?
I have followed along and read through both of afore-mentioned topics so I think/hope that Specify is able to be useful to most common use-cases. Of course, it is only a tool, and definitely is somewhat opinionated.
To make it as generally useful as possible, I very much would like to hear your suggestions/ideas/questions.
What would be the best way to read from environment variables?
Basically, how to deal with the mismatch of Elixir’s snake_case
typing and environment variables that usually, but not necessarily, are in CONSTANT_CASE
. I am currently thinking of adding additional options to the fields to define an environment variable name (which defaults to the uppercase field name, but is overridable), with an additional option to the defconfig
call that will be prefixed to these names, but I am not yet entirely certain this is the best way.
All with all, it was a ton of fun to write Specify (it might have been my most involved macro- and metaprogramming in Elixir to date). Very eager for your feedback!
note: starting at v. 0.4.0, Confy has been renamed to Specify.