Gamend.Settings (gamend_core v1.0.1192)

Copy Markdown View Source

The declared configuration surface: every setting core, the host and its plugins expose, with its type, default, group, env var name and required level.

Settings are declared with Gamend.Settings.Provider and read with get/2, which checks Application.get_env(app, module) and falls back to the compiled default. That means a host configures the ordinary Elixir way:

config :gamend_core, Gamend.Retention, chat_messages_days: 90

Environment variables are one input method into that, not a second source. A host that wants them writes one line in config/runtime.exs:

for {app, module, opts} <- Gamend.Settings.from_env() do
  config app, module, opts
end

A host that prefers a JSON file, or plain Elixir, writes its own equivalent. Every route ends at Application config, so no two sources compete.

Discovery

Providers are found by scanning the modules of apps/0 for __settings__/0 and cached in :persistent_term. Plugins load after config is resolved, so Gamend.Hooks.PluginManager registers theirs on load via add_app/1.

Summary

Functions

Registers another app's providers — the host application, or a plugin loaded after boot. Clears the cache so the next read picks them up.

Registers one provider module directly, for code that is not in a scanned app's module list — a plugin compiled at runtime, or a test.

Every declared setting, across every registered app.

Apps scanned for providers.

Casts a raw string to a declared type. Returns :error when it does not parse, so the caller decides whether that is fatal.

A setting's declaration, with its effective value and where that came from: :config when the host set one, :default otherwise.

The env var name a group/key derives to. Exposed for docs and the .env.example generator.

Reads every declared setting from the environment, as {app, module, opts} ready to splat into config/2.

The current value of a setting: the host's Application config if it set one, otherwise the compiled default.

Declared settings for one group.

Every group, as {group, label}, in display order.

Provider modules, discovered once and cached.

Drops the cached provider list. Call after loading code that declares settings.

Every setting's resolved value, keyed by {module, key}: the environment when it is set, the compiled default otherwise.

Checks every declared requirement against the resolved configuration.

Runs validate/1, logging warnings and raising on failures.

Types

definition()

@type definition() :: %{
  key: atom(),
  module: module(),
  app: atom(),
  group: atom(),
  label: String.t(),
  type: atom(),
  default: term(),
  env: String.t(),
  doc: String.t(),
  secret: boolean(),
  external: boolean(),
  required: :prod | :warn | nil,
  when: {[atom()], term()} | nil,
  with: [atom()]
}

Functions

add_app(app)

@spec add_app(atom()) :: :ok

Registers another app's providers — the host application, or a plugin loaded after boot. Clears the cache so the next read picks them up.

add_provider(module)

@spec add_provider(module()) :: :ok

Registers one provider module directly, for code that is not in a scanned app's module list — a plugin compiled at runtime, or a test.

all()

@spec all() :: [definition()]

Every declared setting, across every registered app.

apps()

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

Apps scanned for providers.

cast(raw, atom)

@spec cast(String.t(), atom()) :: {:ok, term()} | :error

Casts a raw string to a declared type. Returns :error when it does not parse, so the caller decides whether that is fatal.

describe(definition)

@spec describe(definition()) :: map()

A setting's declaration, with its effective value and where that came from: :config when the host set one, :default otherwise.

The admin viewer renders this; :env never appears as a source because env vars are resolved into Application config at boot rather than read live.

env_name(root, group, key)

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

The env var name a group/key derives to. Exposed for docs and the .env.example generator.

from_env()

@spec from_env() :: [{atom(), module(), keyword()}]

Reads every declared setting from the environment, as {app, module, opts} ready to splat into config/2.

Only variables that are actually set contribute; the rest fall through to the compiled default. A value that does not parse as its declared type is skipped with a warning rather than taking the boot down.

get(module, key)

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

The current value of a setting: the host's Application config if it set one, otherwise the compiled default.

Raises for a key the module does not declare — an undeclared read is a bug, not a runtime condition.

group(name)

@spec group(atom()) :: [definition()]

Declared settings for one group.

groups()

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

Every group, as {group, label}, in display order.

providers()

@spec providers() :: [module()]

Provider modules, discovered once and cached.

reload()

@spec reload() :: :ok

Drops the cached provider list. Call after loading code that declares settings.

remove_provider(module)

@spec remove_provider(module()) :: :ok

Undoes add_provider/1.

resolve()

@spec resolve() :: %{required({module(), atom()}) => term()}

Every setting's resolved value, keyed by {module, key}: the environment when it is set, the compiled default otherwise.

For config/runtime.exs, which needs values while it is still building the configuration. config/2 only applies after the whole file is evaluated, so get/2 cannot see what from_env/0 just contributed — this can.

validate(env)

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

Checks every declared requirement against the resolved configuration.

Returns {failures, warnings}, each a list of human-readable lines. Nothing is raised here — validate!/1 decides what is fatal, so a caller that wants to render the state instead (the admin viewer) can.

Severity by environment:

Level:prod:dev:test
required: :prodfailurewarningsilent
required: :warnwarningsilentsilent

The dev warning on a prod requirement is deliberate: it says the deployment will not boot in production while the developer is still at the keyboard. Test is silent because the suite boots hundreds of times.

validate!(env)

@spec validate!(atom()) :: :ok

Runs validate/1, logging warnings and raising on failures.

Called once at boot. Returns :ok when nothing is fatal.