Svelixir. Config
(svelixir v0.11.0)
Copy Markdown
The whole scaffolder config for one project, as a struct — one child struct
per section of the templates in priv/templates.
Both project types are this same struct. project.type is a field, not a
separate shape, because the two templates differ in exactly four values and
nothing else: project.type, assets.type, and the two assets.ui flags.
Modelling them as two structs would duplicate twenty-five identical fields to
express four.
The two are reached through their presets, which correspond 1:1 to the template files:
Svelixir.Config.Otp.new!() # priv/templates/base_otp_app.exs
Svelixir.Config.Web.new!() # priv/templates/base_web_app.exsnew!/1 here is the unpresetted form, taking every section verbatim. Prefer
a preset unless you genuinely mean "no project type chosen for me".
Canonical names
The names below are canonical. The scaffolder skills reached the same switches under older spellings, and the templates used to carry three "reconcile before anything reads this" markers; this is the reconciliation, and the older spellings are retired rather than aliased. Nothing accepts them as input — a config.toml reader is where the translation belongs, and this is the table it should translate to.
| Canonical | Skills spelling | Notes |
|---|---|---|
project.github_org | github.org | Sectioned in TOML, one flat atom here |
auth.local | options.local_auth | |
auth.oidc | options.oidc_auth | Forces auth.local |
auth.saml | options.saml_auth | Forces auth.local |
container.compose | options.podman, options.docker | Two booleans collapse to one tri-state |
container.compose is the only one that changes shape rather than just
spelling. Two independent booleans admit docker: true, podman: false,
which is not a thing a project can be; [:none, :podman, :docker] is
ordered, so :docker outranks :podman and the impossible state is
unrepresentable.
Two further spellings appear in the templates' old comments —
config.toml's git_org and composer. Neither occurs anywhere in this
repository or in the scaffolder skills, so they are recorded here as
unverified history rather than carried forward as fact.
Why this exists
The templates document five rules and enforce none of them, saying outright that keeping them in step "is on you". Three are implications and two are hard constraints, and nothing that reads a keyword list can tell the difference. Here they are code.
Construction runs in the order the templates specify — implications expanded before validation:
otp.repoforcesotp.supervisor(a repo is a supervised process)auth.oidcorauth.samlforcesauth.local(both issue app tokens through that path)cache.backend: :valkeyforcescontainer.sidecars.valkey, andsecurity.openbaoforcescontainer.sidecars.openbao- any sidecar forces
container.composeto at least:podman
Then, and only then, the two hard constraints raise
Svelixir.Config.InvalidError: any auth method requires project.type: :web, and assets.type of :api or :both requires it too.
Step 3 must precede step 4, or a :valkey backend would leave compose at
:none — a cache with nothing to run it in.
Consequence
A constructed %Svelixir.Config{} is always self-consistent, so no caller
ever re-derives an implication. config.otp.supervisor is simply correct.
iex> config = Svelixir.Config.new!(otp: [repo: true, supervisor: false])
iex> config.otp.supervisor
trueUndeclared keys raise rather than being ignored, which is what makes a mistyped flag a failure instead of a silent default.
Reading svelixir.exs
read!/1 is the other way in. It reads a project's hand-authored
svelixir.exs — the four-key slim shape of SDD 8.1 — validates the file,
then resolves it through the preset its base: names. What comes back is
the same fully-expanded struct every other caller sees, not a transcription
of the file: base: is project.type, and the file supplies four of
project's six values. There is deliberately no second struct for the
literal file, because there would be nothing for it to express.
Validation happens at two layers, and they are not competitors. This layer
validates a file — keys, types, formats — before any struct exists to hang
a Vex declaration off. The sections validate the resolved config. Both
raise Svelixir.Config.InvalidError, so a caller sees one exception type.
Three asymmetries between the file and this struct are deliberate:
schema_versionsits at the top level of the file and atproject.schema_versionhere, which is where both templates already put it.components:must be[]. It is validated and then dropped:[]is the only legal value in schema_version 1, so the key carries no information, and a root field for it would be a field neither template has. Accepting a non-empty list would silently promise something nothing delivers, so the scope boundary is a loud runtime error.- The file's
project:must be a keyword list, whilenew!/1accepts a map as readily. The file is hand-authored and 8.1's only specimen is a keyword list, with ordering semantics a map does not share; the in-memory constructor has no literal to be faithful to.
Every key of the file is required and unknown keys are rejected rather than ignored: a silently-ignored key reads to the author as "applied".
The implication rules above cannot vary with the file. Nothing in
svelixir.exs reaches otp.repo, auth.*, cache.backend or
security.openbao, so with components: [] every field they read is fixed
at its preset value and they resolve identically for every file this reader
accepts. They stay because new!/1 is a public entry point too.
Summary
Functions
Resolves already-read svelixir.exs source. file names it in errors only.
Builds this section, and any nested block, from input.
The absolute path to target's svelixir.exs.
Reads and resolves target's svelixir.exs.
Types
@type t() :: %Svelixir.Config{ assets: Svelixir.Config.Assets.t(), auth: Svelixir.Config.Auth.t(), cache: Svelixir.Config.Cache.t(), container: Svelixir.Config.Container.t(), features: Svelixir.Config.Features.t(), otp: Svelixir.Config.Supervision.t(), project: Svelixir.Config.Project.t(), security: Svelixir.Config.Security.t() }
Functions
Resolves already-read svelixir.exs source. file names it in errors only.
Builds this section, and any nested block, from input.
@spec path(Svelixir.Target.t()) :: Path.t()
The absolute path to target's svelixir.exs.
There is no filename/0 beside it: a public accessor over a module
attribute has no caller that path/1 does not serve better.
@spec read!(Svelixir.Target.t()) :: t()
Reads and resolves target's svelixir.exs.
Raises File.Error if the file is absent, Svelixir.Exs.DecodeError if it
is not plain data, and Svelixir.Config.InvalidError if it is plain data of
the wrong shape. The target's mix.exs is never consulted, so this works
against a project that does not compile.