Macaw.Host.Config (macaw_host v0.1.4)

Copy Markdown View Source

An app's configuration, from the literal config.exs on its data volume.

Each setting resolves as env var > config.exs > default. An app declares its settings once, as {name, type, default} (Macaw.Host.Config.Types), and loads them at boot:

@settings [
  {:public_host, :string, "localhost"},
  {:port, :port, 4000},
  {:smtp_password, :secret, nil}
]

Macaw.Host.Config.load!(:my_app, @settings,
  path: "/data/config.exs", env_prefix: "MY_APP")

Macaw.Host.Config.get(:my_app, :port)

MY_APP_PORT wins over port in the file, which wins over 4000. Every invalid value and every unknown key in the file fails the load at once, each named with where it came from, so a broken boot says everything that is wrong in one message. The resolved values, with each one's source, are kept in :persistent_term and read without copying.

put!/4 and delete!/3 edit the file for the <app>-ctl config command: the value is parsed as a literal, checked against the setting's type, and the file is written atomically; nothing is evaluated.

Summary

Functions

Removes name from the file, so it falls back to its env var or default.

The env var a setting is read from: PREFIX_NAME.

A loaded setting's value. Raises for a name the app didn't declare.

The settings' names, in declared order.

Reads the file and the environment, resolves, and stores the result under app. Options: :path (the file), :env_prefix, :env (default System.get_env/0). Raises ArgumentError listing every fault.

A value as shown; anything but a secret is inspected. A secret is unset when nil or empty, set (…last4) when it is at least 16 characters long (so at most a quarter of it shows), else set. The generated script's Ctl.Secret.mask/1 follows the same rule.

Sets name in the file at path to the literal text, after checking it against the setting's type. {:ok, value} or {:error, why}; the file is untouched on error.

The file at path as a map, never raising: {:ok, map} (%{} when it doesn't exist) or {:error, message}, every fault on one line, for the ctl, whose failure is one line.

Resolves settings from an env map, a file map and the env var prefix. Pure: {:ok, resolved} or {:error, faults}.

Resolves one setting as resolve/4 does, env var > file > default, the file's key an atom or a string: {:ok, %{value, source}}, or {:error, %{raw, source, fault}} with the value as found (the env var's string, the file's literal) and the fault as resolve/4 words it. resolve/4 is built on it, and so is <app>-ctl config list, so the two never disagree.

Every loaded setting as {name, shown value, source}, secrets masked.

Where a loaded setting's value came from: :env, :file or :default.

Types

resolved()

@type resolved() :: %{required(atom()) => %{value: term(), source: source()}}

source()

@type source() :: :env | :file | :default

spec()

@type spec() :: {atom(), Macaw.Host.Config.Types.t(), term()}

Functions

delete!(path, specs, name)

@spec delete!(Path.t(), [spec()], String.t()) :: :ok | {:error, String.t()}

Removes name from the file, so it falls back to its env var or default.

env_var(prefix, name)

@spec env_var(String.t(), atom()) :: String.t()

The env var a setting is read from: PREFIX_NAME.

get(app, name)

@spec get(atom(), atom()) :: term()

A loaded setting's value. Raises for a name the app didn't declare.

keys(specs)

@spec keys([spec()]) :: [atom()]

The settings' names, in declared order.

load!(app, specs, opts)

@spec load!(atom(), [spec()], keyword()) :: resolved()

Reads the file and the environment, resolves, and stores the result under app. Options: :path (the file), :env_prefix, :env (default System.get_env/0). Raises ArgumentError listing every fault.

mask(arg1, v)

@spec mask(Macaw.Host.Config.Types.t(), term()) :: String.t()

A value as shown; anything but a secret is inspected. A secret is unset when nil or empty, set (…last4) when it is at least 16 characters long (so at most a quarter of it shows), else set. The generated script's Ctl.Secret.mask/1 follows the same rule.

put!(path, specs, name, text)

@spec put!(Path.t(), [spec()], String.t(), String.t()) ::
  {:ok, term()} | {:error, String.t()}

Sets name in the file at path to the literal text, after checking it against the setting's type. {:ok, value} or {:error, why}; the file is untouched on error.

read_file(path)

@spec read_file(Path.t()) :: {:ok, map()} | {:error, String.t()}

The file at path as a map, never raising: {:ok, map} (%{} when it doesn't exist) or {:error, message}, every fault on one line, for the ctl, whose failure is one line.

resolve(specs, env, file, prefix)

@spec resolve([spec()], %{required(String.t()) => String.t()}, map(), String.t()) ::
  {:ok, resolved()} | {:error, [String.t()]}

Resolves settings from an env map, a file map and the env var prefix. Pure: {:ok, resolved} or {:error, faults}.

resolve_setting(arg, env, file, prefix)

@spec resolve_setting(
  spec(),
  %{required(String.t()) => String.t()},
  map(),
  String.t()
) ::
  {:ok, %{value: term(), source: source()}}
  | {:error, %{raw: term(), source: source(), fault: String.t()}}

Resolves one setting as resolve/4 does, env var > file > default, the file's key an atom or a string: {:ok, %{value, source}}, or {:error, %{raw, source, fault}} with the value as found (the env var's string, the file's literal) and the fault as resolve/4 words it. resolve/4 is built on it, and so is <app>-ctl config list, so the two never disagree.

show(app)

@spec show(atom()) :: [{atom(), String.t(), source()}]

Every loaded setting as {name, shown value, source}, secrets masked.

source(app, name)

@spec source(atom(), atom()) :: source()

Where a loaded setting's value came from: :env, :file or :default.