SvEx.Config.Shape (SvEx v0.4.2)

The shape of a resolved SvEx.Config, read off the sections themselves.

target.exs records four keys. What those four keys RESOLVE to is a twenty-nine-field struct across eight sections, and until this module the only way to see it was to read lib/sv_ex/config/*.ex. The mix task sv_ex.gen.config writes it into the generated file as a comment block, so a project carries the shape of its own configuration next to the four values that chose it.

Nothing here is a second copy of the schema

Every column is read from the section that defines it, at runtime:

  • the fields and their order — __info__(:struct), which reports them in declaration order
  • the type — the section's own t() typespec, read out of the beam's type chunk
  • the value — the resolved config that was passed in, so it is what this project actually gets rather than what the struct defaults to
  • the domain — the inclusion: option of the section's own validation, which is the same literal SvEx.Config.Enums hands the validator

A field added to a section appears here with no edit. That is the whole design: a hand-maintained table is a second schema, and a second schema is one that can be wrong.

Except the rules, which are data on purpose

rules/0 and constraints/0 ARE written out, because the four implications and two constraints live in SvEx.Config's finalize step as code and no introspection recovers them. They are declarative — each one names the input that triggers it and the output it forces — so SvEx.Config.ShapeTest runs every one of them against the real constructor rather than reading them back. A rule that stops holding fails a test; it does not quietly become a wrong comment.

Summary

Types

A combination the constructor refuses outright.

One leaf field of a resolved config.

An implication: given this input, the constructor forces forces to to.

Functions

The combinations SvEx.Config refuses, raising SvEx.Config.InvalidError.

Every leaf field of config, in declaration order, deepest-last per section.

Renders config's shape as plain lines: a header, the field table, the implications and the constraints.

The implications SvEx.Config applies while resolving a config.

Types

constraint()

@type constraint() :: %{base: atom(), given: keyword(), requires: String.t()}

A combination the constructor refuses outright.

row()

@type row() :: %{
  path: String.t(),
  type: String.t(),
  value: term(),
  domain: [term()] | nil
}

One leaf field of a resolved config.

rule()

@type rule() :: %{base: atom(), given: keyword(), forces: String.t(), to: term()}

An implication: given this input, the constructor forces forces to to.

Functions

constraints()

@spec constraints() :: [constraint()]

The combinations SvEx.Config refuses, raising SvEx.Config.InvalidError.

describe(config)

@spec describe(SvEx.Config.t()) :: [row()]

Every leaf field of config, in declaration order, deepest-last per section.

Nested blocks are walked, so container.sidecars.valkey is one row rather than a container.sidecars row holding a struct.

render(config, base)

@spec render(SvEx.Config.t(), atom()) :: [String.t()]

Renders config's shape as plain lines: a header, the field table, the implications and the constraints.

base names the preset the config came from and appears in the header only.

rules()

@spec rules() :: [rule()]

The implications SvEx.Config applies while resolving a config.