View Source ConfigSmuggler (config_smuggler v0.8.2)

ConfigSmuggler is a library for converting Elixir-style configuration statements to and from string-encoded key/value pairs.

Elixir (and Erlang)'s configuration system is somewhat richer than naïve environment variables, i.e., System.get_env/1-style key/value configs alone can capture.

Configs in Elixir are namespaced by app, can be arbitrarily nested, and contain Elixir-native data types like atoms, keyword lists, etc.

ConfigSmuggler provides a bridge between Elixir applications and key/value configuration stores, especially those available at runtime. It makes it dead-simple to use platform-agnostic configuration systems with Elixir services.

warning

WARNING!

The functions in ConfigSmuggler are not suitable for use on untrusted inputs! Code is evaled, atoms are created, etc.

Configs are considered privileged inputs, so don't worry about using ConfigSmuggler for its intended purpose. But please, never let user input anywhere near this module. You've been warned.

example

Example

iex> encoded_configs = %{
...>   # imagine you fetch this every 60 seconds at runtime
...>   "elixir-logger-level" => ":debug",
...>   "elixir-my_api-MyApi.Endpoint-url-port" => "8888",
...> }
iex> ConfigSmuggler.apply(encoded_configs)
iex> Application.get_env(:logger, :level)
:debug
iex> ConfigSmuggler.encode([my_api: Application.get_all_env(:my_api)])
{:ok, %{"elixir-my_api-MyApi.Endpoint-url-port" => "8888"}}

overview

Overview

  • apply/1 applies encoded or decoded configs to the current environment.

  • decode/1 converts an encoded config map into Elixir-native decoded configs, also returning a list of zero or more encoded key/value pairs that could not be decoded.

  • encode/1 converts Elixir-native decoded configs (i.e., a keyword list with app name as key and keyword list of configs as value) into an encoded config map.

  • encode_file/1 converts an entire config.exs-style file (along with all files included with Mix.Config.import_config/1) into an encoded config map.

  • encode_statement/1 converts a single config statement from a config.exs-style file into an encoded config map.

  • At the command line, mix smuggle encode <filename.exs> encodes a file with encode_file/1 and emits a JSON object as output.

encoding-scheme

Encoding Scheme

The encoded key begins with elixir and is a hyphen-separated "path" of atoms and modules leading to the config value we wish to set.

The value is any valid Elixir term, encoded using normal Elixir syntax.

Encoding is performed by Kernel.inspect/2. Decoding is performed by Code.eval_string/1 and String.to_atom/1.

see-also

See Also

If you build and deploy Erlang releases, and you want to apply encoded configs before any other apps have started, look into Distillery config providers.

This feature allows specified modules to make environment changes with Application.put_env/3, after which these changes are persisted to the release's sys.config file and the release is started normally.

gotchas

Gotchas

Atoms and modules are expected to follow standard Elixir convention, namely that atoms begin with a lowercase letter, modules begin with an uppercase letter, and neither contains any hyphen characters.

If a config file or statement makes reference to Mix.env(), the current Mix env will be substituted. This may be different than what the config file intended.

authorship-and-license

Authorship and License

Copyright 2019, Appcues, Inc.

ConfigSmuggler is released under the MIT License.

Link to this section Summary

Functions

Applies the given config to the current environment (i.e., calls Application.put_env/3 a bunch of times). Accepts Elixir- native decoded configs or encoded config maps.

Decodes a map of string-encoded key/value pairs into a keyword list of Elixir configs, keyed by app. Also returns a list of zero or more invalid key/value pairs along with their errors.

Converts Elixir-native decoded configs (i.e., a keyword list with app name as key and keyword list of configs as value) into an encoded config map.

Reads a config file and returns a map of encoded key/value pairs representing the configuration. Respects Mix.Config.import_config/1.

Encodes a single Mix.Config.config/2 or Mix.Config.config/3 statement into one or more encoded key/value pairs.

Link to this section Types

Specs

decoded_configs() :: [{atom(), Keyword.t()}]

Specs

encoded_config_map() :: %{required(encoded_key()) => encoded_value()}

Specs

encoded_key() :: String.t()

Specs

encoded_value() :: String.t()

Specs

error_reason() :: :bad_input | :bad_key | :bad_value | :load_error

Specs

validation_error() :: {{encoded_key(), encoded_value()}, error_reason()}

Link to this section Functions

Specs

apply(decoded_configs() | encoded_config_map()) ::
  :ok | {:error, error_reason()}

Applies the given config to the current environment (i.e., calls Application.put_env/3 a bunch of times). Accepts Elixir- native decoded configs or encoded config maps.

iex> ConfigSmuggler.apply([my_app: [foo: 22]])
iex> Application.get_env(:my_app, :foo)
22

iex> ConfigSmuggler.apply(%{"elixir-my_app-bar" => "33"})
iex> Application.get_env(:my_app, :bar)
33
Link to this function

decode(encoded_config_map)

View Source

Specs

Decodes a map of string-encoded key/value pairs into a keyword list of Elixir configs, keyed by app. Also returns a list of zero or more invalid key/value pairs along with their errors.

iex> ConfigSmuggler.decode(%{
...>   "elixir-my_app-some_key" => "22",
...>   "elixir-my_app-MyApp.Endpoint-url-host" => "\"localhost\"",
...>   "elixir-logger-level" => ":info",
...>   "elixir-my_app-MyApp.Endpoint-url-port" => "4444",
...>   "bad key" => "22",
...>   "elixir-my_app-foo" => "bogus value",
...> })
{:ok,
  [
    my_app: [
      {:some_key, 22},
      {MyApp.Endpoint, [
        url: [
          port: 4444,
          host: "localhost",
        ]
      ]},
    ],
    logger: [
      level: :info,
    ],
  ],
  [
    {{"elixir-my_app-foo", "bogus value"}, :bad_value},
    {{"bad key", "22"}, :bad_key},
  ]
}

Specs

encode(decoded_configs()) ::
  {:ok, encoded_config_map()} | {:error, error_reason()}

Converts Elixir-native decoded configs (i.e., a keyword list with app name as key and keyword list of configs as value) into an encoded config map.

iex> ConfigSmuggler.encode([logger: [level: :info], my_app: [key: "value"]])
{:ok, %{
    "elixir-logger-level" => ":info",
    "elixir-my_app-key" => "\"value\"",
}}

Specs

encode_file(String.t()) ::
  {:ok, encoded_config_map()} | {:error, error_reason()}

Reads a config file and returns a map of encoded key/value pairs representing the configuration. Respects Mix.Config.import_config/1.

iex> ConfigSmuggler.encode_file("config/config.exs")
{:ok, %{
  "elixir-logger-level" => ":info",
  # ...
}}

Specs

encode_statement(String.t()) ::
  {:ok, encoded_config_map()} | {:error, error_reason()}

Encodes a single Mix.Config.config/2 or Mix.Config.config/3 statement into one or more encoded key/value pairs.

iex> ConfigSmuggler.encode_statement("config :my_app, key1: :value1, key2: \"value2\"")
{:ok, %{
    "elixir-my_app-key1" => ":value1",
    "elixir-my_app-key2" => "\"value2\"",
}}

iex> ConfigSmuggler.encode_statement("config :my_app, MyApp.Endpoint, url: [host: \"localhost\", port: 4444]")
{:ok, %{
  "elixir-my_app-MyApp.Endpoint-url-host" => "\"localhost\"",
  "elixir-my_app-MyApp.Endpoint-url-port" => "4444",
}}