Specify
is a library to create Comfortable, Explicit, Multi-Layered and Well-Documented Specifications for all your configurations, settings and options in Elixir.
Basic features:
- Configuration is converted to a struct, with fields being parsed to their appropriate types.
- Specify a stack of sources to fetch the configuration from.
- Always possible to override local configuration using plain arguments to a function call.
- Fail-fast on missing or malformed values.
- Auto-generated documentation based on your config specification.
Specify can be used both to create normalized configuration structs during runtime and compile-time using both implicit external configuration sources and explicit arguments to a function call.
Installation
You can install Specify by adding specify
to your list of dependencies in mix.exs
:
def deps do
[
{:specify, "~> 0.7.0"}
]
end
Documentation can be found at https://hexdocs.pm/specify.
Examples
Basic usage is as follows, using Specify.defconfig/1
:
defmodule Cosette.CastleOnACloud do
require Specify
Specify.defconfig 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
and later Specify.load/2
, Specify.load_explicit/3
(or YourModule.load/1
, YourModule.load_explicit/2
which are automatically defined).
iex> Cosette.CastleOnACloud.load(explicit_values: [lullaby: "I love you very much", crying_allowed: true])
%Cosette.CastleOnACloud{
crying_allowed: true,
floors_to_sweep: 0,
lullaby: "I love you very much",
amount_boys_and_girls: 100
}
Mandatory Fields
Notice that since the :lullaby
-field is mandatory, if it is not defined in any of the configuration sources, an error will be thrown:
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
Multiple parsers
It is possible to specify several parsers for a unique field, using a list of parsers. They will be tried consecutively in list order. For instance:
field :some_field, [:string, :boolean], default: true
Loading from Sources
Loading from another source is easy:
iex> Application.put_env(Cosette.CastleOnACloud, :lullaby, "sleep little darling")
# or: in a Mix config.ex file
config Cosette.CastleOnACloud, lullaby: "sleep little darling"
iex> Cosette.CastleOnACloud.load(sources: [Specify.Provider.MixEnv])
%Cosette.CastleOnACloud{
crying_allowed: false,
floors_to_sweep: 0,
lullaby: "sleep little darling",
no_boys_and_girls: 100
}
Rather than passing in the sources when loading the configuration, it often makes more sense to specify them when defining the configuration:
defmodule Cosette.CastleOnACloud do
require Specify
Specify.defconfig sources: [Specify.Provider.MixEnv] do
# ...
end
end
Providers
Providers can be specified by passing them to the sources:
option (while loading the configuration structure or while defining it).
They can also be set globally by altering the sources:
key of the Specify
application environment, or per-process using the :sources
subkey of the Specify
key in the current process' dictionary (Process.put_env
).
Be aware that for bootstrapping reasons, it is impossible to override the :sources
field globally in an external source (because Specify would not know where to find it).
Specify
comes with the following built-in providers:
Specify.Provider.MixEnv
, which usesMix.env
/Application.get_env
to read from the application environment.Specify.Provider.SystemEnv
, which usesSystem.get_env
to read from system environment variables.Specify.Provider.Process
, which usesProcess.get
to read from the current process' dictionary.
Often, Providers have sensible default values on how they work, making their usage simpler:
Specify.Provider.Process
will look at the configuredkey
, but will default to the configuration specification module name.Specify.Provider.MixEnv
will look at the configuredapplication_name
andkey
, but will default to the whole environment of an application (Application.get_all_env
) if no key was set, withapplication_name
defaulting to the configuration specification module name.Specify.Provider.SystemEnv
will look at the configuredprefix
but will default to the module name (in all caps), followed by the field name (in all caps, separated by underscores). What names should be used for a field is also configurable.
Writing Providers
Providers implement the Specify.Provider
protocol, which consists of only one function: load/2
.
Its first argument is the implementation's own struct, the second argument being the configuration specification's module name.
If extra information is required about the configuration specification to write a good implementation, the Reflection function module_name.__specify__
can be used to look these up.
Roadmap
- [x] Compound parsers for collections using
{collection_parser, element_parser}
-syntax, with provided:list
parser. - [x] Main functionality documentation.
- [x] Parsers documentation.
[x] Writing basic Tests
- [x] Specify.Parsers
- [x] Main Specify module and functionality.
- [x] Thinking on how to handle environment variable names (capitalization, prefixes).
- [x] Environment Variables (System.get_env) provider
- [x] Specify Provider Tests.
- [ ] Better/more examples
- [ ] Stable release
Possibilities for the future
- (D)ETS provider
- CLI arguments provider, which could be helpful for defining e.g. Mix tasks.
- .env files provider.
- JSON and YML files provider.
- Nested configs?
- Possibility to load without raising on parsing falure (instead returning a success/failure tuple?)
- Watching for updates and call a configurable handler function when configuration has changed.
Changelog
- 0.10.0 - Adds an
option({atom, term})
parser that can be used to parse for instance keyword lists. Thank you, @tanguilp! - 0.9.0 - Allows multi-value parsers by specifying a list of parsers. Thank you, @tanguilp!
- 0.8.0 - Makes string-parsers work on more of Elixir's builtin terms including lists and maps of other types (including lists and maps themselves). Thank you, @tanguilp!
- 0.7.2 - Makes functions clickable in the generated documentation. Thank you, @tanguilp!
- 0.7.1 - Pretty-prints long default values in a multi-line code block in the documentation (#2).
- 0.7 - Adds an
optional
key to the built-in providers. They will only return{error, :not_found}
if they are not set to optional. Also adds two new ways to indicate sources, which are helpful in environments where you do not have access to the structs directly (such asMix.Config
or the newerElixir.Config
files.) - 0.6 - Adds the
mfa
andfunction
builtin parsers. - 0.5 - Adds the
nonnegative_integer
,positive_integer
,nonnegative_float
,positive_float
andtimeout
builtin parsers. - 0.4.5 - Fixes built-in
integer
andfloat
parsers to not crash on input like"10a"
(but instead return{:error, _}
). - 0.4.4 - Fixes references to validation/parsing functions in documentation.
- 0.4.2 - Finishes provider tests; bugfix for the MixEnv provider.
- 0.4.1 - Improves documentation.
- 0.4.0 - Name change: from 'Confy' to 'Specify'. This name has been chosen to be more clear about the intent of the library.
- 0.3.0 - Changed
overrides:
toexplicit_values:
and addedSpecify.load_explicit/3
function. (Also added tests and fixed parser bugs). - 0.2.0 - Initially released version
Attribution
I want to thank Chris Keathley for his interesting library Vapor which helped inspire Specify.
I also want to thank José Valim for the great conversations we've had about the advantages and disadvantages of various approaches to configuring Elixir applications.