This document is the comprehensive reference for schema authors. It covers the schema file format, all mapping options, all datatypes, the translation and validator systems, and the helper functions available inside translation functions.
- Schema Files
- Mappings
- Schema Partials
- Datatypes
- Translations
- Validators
- Helper Functions
- The conf File Format
- The Pipeline
A schema file is a plain Erlang terms file (parsed with erl_scan/erl_parse). Each term is one of three types:
{mapping, "conf.key", "app.erlang_key", [Options]}.
{translation, "app.erlang_key", fun(Conf) -> ... end}.
{validator, "name", "description", fun(Value) -> boolean() end}.Multiple schema files can be loaded together. Later files can override earlier ones using the merge option. Schema files are loaded via cuttlefish_schema:files/1 or cuttlefish_schema:strings/1.
A mapping connects a conf file key to an Erlang app.config key.
{mapping, "ring_size", "riak_core.ring_creation_size", [
{datatype, integer},
{default, 64},
{doc, ["The number of partitions in the ring."]}
]}.The first argument is the conf key (dot-separated string). The second is the Erlang app.config path (also dot-separated; the first segment is the application name). The third is a proplist of options.
| Option | Value | Description |
|---|---|---|
{datatype, T} |
datatype or list of datatypes | Type(s) for parsing and validation. See Datatypes. |
{default, V} |
any Erlang term | Value used when the key is absent from the conf file. |
{commented, V} |
any Erlang term | Like default, but the key is commented out in the generated conf file. |
{level, L} |
basic, intermediate, or advanced |
Controls which keys appear in the generated conf file. Defaults to basic. |
{doc, [String]} |
list of strings | Documentation lines shown in the generated conf file. |
{validators, [Name]} |
list of strings | Named validators to run against the parsed value. |
{hidden, true} |
boolean | Omit this key from the generated conf file entirely. |
{alias, "old.key"} |
string | A single deprecated conf key that maps to this mapping. See Aliases. |
{aliases, ["old.key"]} |
list of strings | Multiple deprecated conf keys. See Aliases. |
{collect, Type} |
collect type | Auto-collect fuzzy variable matches into a list, map, or proplist. See Collections. |
{see, ["other.key"]} |
list of strings | Cross-references to related conf keys, shown in generated conf. |
{include_default, "name"} |
string | Includes a named default value in the generated conf. |
merge |
atom | Merge this mapping with an existing mapping of the same variable rather than replacing it. Used when multiple schema files contribute to the same key. |
A conf key segment prefixed with $ is a fuzzy variable - it acts as a wildcard matching any value in that position.
{mapping, "listener.tcp.$name", "ranch.tcp_listeners", [
{datatype, integer}
]}.This matches listener.tcp.default, listener.tcp.internal, etc. The $name segment captures the concrete value. Fuzzy variables are used with translations (to collect all matching values) or with the {collect, Type} option (to auto-collect without a translation).
Fuzzy variables are not supported in aliases.
Aliases allow a mapping to transparently accept deprecated conf keys. When a user's conf file contains an alias key, cuttlefish rewrites it to the canonical key and logs a deprecation warning.
{mapping, "new.key", "app.setting", [
{datatype, integer},
{default, 42},
{alias, "old.key"}
]}.Multiple aliases:
{mapping, "new.key", "app.setting", [
{datatype, integer},
{alias, "old.key"},
{aliases, ["older.key", "oldest.key"]}
]}.Behavior:
- Alias resolution runs before defaults and substitutions.
- If both the canonical key and an alias key are present in the conf file, the canonical key wins.
- If multiple alias keys are present and the canonical key is absent, the first alias in declaration order wins.
- Using
$(old.key)in an RHS substitution is an error. Use$(new.key)instead. - Aliases on fuzzy variables are not supported and are rejected at schema load time.
- In a merge mapping,
{aliases, []}explicitly clears all inherited aliases.
Validation at schema load time:
- An alias key that shadows a canonical key is rejected.
- An alias key claimed by two different mappings is rejected.
The {collect, Type} option eliminates the boilerplate translation function required for fuzzy-variable mappings that collect values into a list, map, or proplist.
Supported collection types:
| Type | Erlang result | Key conversion |
|---|---|---|
list |
[Value] |
none - values sorted by wildcard segment |
{map, atom} |
#{atom() => Value} |
list_to_atom/1 |
{map, binary} |
#{binary() => Value} |
list_to_binary/1 |
{proplist, atom} |
[{atom(), Value}] |
list_to_atom/1, sorted by wildcard segment |
Example - replacing an explicit translation:
Before:
{mapping, "auth_mechanisms.$name", "rabbit.auth_mechanisms", [
{datatype, atom}
]}.
{translation, "rabbit.auth_mechanisms",
fun(Conf) ->
Settings = cuttlefish_variable:filter_by_prefix("auth_mechanisms", Conf),
Sorted = lists:keysort(1, Settings),
[V || {_, V} <- Sorted]
end}.After:
{mapping, "auth_mechanisms.$name", "rabbit.auth_mechanisms", [
{datatype, atom},
{collect, list}
]}.Sorting: For list and {proplist, atom}, values are sorted lexicographically by the wildcard segment value (e.g. the $name part). For a single-segment fuzzy variable this produces the same order as lists:keysort/1 on the full conf key.
Empty result: When no conf values match the wildcard pattern, the key is absent from the generated app.config (equivalent to calling cuttlefish:unset() in a translation). This differs from a translation that returns [], which writes an empty list to app.config.
Translation takes precedence: If a translation exists for the same Erlang app.config key, the translation always runs and {collect, Type} is ignored. This means existing schemas with translations are unaffected when {collect, Type} is added to a mapping.
Constraints:
{collect, Type}requires exactly one fuzzy ($) segment in the variable name. Zero or more than one wildcard is a schema load-time error.{proplist, binary}is not a supported collection type.
A partial is a reusable, parameterised block of mappings, validators, and translations that a schema can pull in with a single directive. Partials exist to remove the duplication that arises when many plugin schemas declare the same shape (TLS options, TCP listen options, proxy protocol) with only the conf-key and app-key prefixes differing.
{include_partial, {rabbit, "ssl_options"},
[{prefix, "auth_http.ssl_options"},
{app_prefix, "rabbitmq_auth_backend_http.ssl_options"}]}.The first element is an {App, Name} pair. The loader resolves the partial via code:priv_dir(App) — exactly the same mechanism RabbitMQ already uses to discover .schema files — so the lookup works identically in dev trees, eunit, escripts, .ez plugins, and OS packages, without any new search path or CLI flag.
The partial file lives at <App>/priv/schema/<Name>.partial. The distinct extension keeps it out of cuttlefish_schema:list_schemas/1's auto-discovery, so partials are only loaded when a schema explicitly includes them.
Expansion happens at parse time, before the schema merger runs. Partial-produced mappings and translations are indistinguishable from inline ones to every downstream stage (defaults, datatype conversion, validation, translation, conf generation).
| Argument | Required | Meaning |
|---|---|---|
{prefix, S} |
yes | Prepended to every mapping's conf key; bound into partial-translation closures as ConfPrefix. Must be non-empty. |
{app_prefix, S} |
yes | Prepended to every mapping's and translation's app key; bound as AppPrefix. Must be non-empty. |
{exclude, [Name]} |
no | Drops a partial term when Name exactly equals a mapping's bare conf key, equals a translation's bare app key, or equals the first dotted segment of a mapping's bare conf key (section match). |
{overrides, [{Name, Opt}]} |
no | For each {Name, Opt} pair, applies the option Opt (e.g. {validators, [...]}, {default, _}, {datatype, _}) to the partial mapping with that bare conf key, replacing any same-keyed option from the partial. Equivalent in effect to declaring a [merge, Opt] mapping with the full prefixed name after the include, but expressed inline at the include site. |
{disable_with, Atom} |
no | Adds a guard mapping and translation that lets the user write <prefix> = <Atom> (and nothing else) as a one-line shortcut to disable the entire feature. Any other value is rejected with cuttlefish:invalid/1. Removes the per-consumer guard boilerplate (~10 lines per consumer). |
Unknown options are rejected loudly at load time so a typo like {predix, "..."} fails immediately instead of silently defaulting. Names listed in exclude and overrides are also checked against the partial's contents — a misspelling like {exclude, ["versiosns"]} or {overrides, [{"certfle", _}]} produces a partial_exclude_unmatched or partial_overrides_unmatched error naming the unmatched entries rather than silently no-opping.
A .partial file contains three top-level forms:
{mapping, "bare_conf_key", "bare_app_key", [Options]}.
{validator, "name", "description", fun(Value) -> boolean() end}.
{partial_translation, "bare_app_key",
fun(Conf, ConfPrefix, AppPrefix) -> term() end}.Mappings are written with bare names. The loader prepends the include's prefix and app_prefix. Fuzzy $name segments work normally (they live in the suffix). Most mapping options pass through unchanged. Two are handled specially:
{aliases, [...]}is never prefix-rewritten — aliases are typically legacy absolute keys, not relative siblings.{see, [...]}rewrites bare entries (those without a.) by prepending the conf prefix; dotted entries pass through as absolute references. All entries are tokenized so downstream code sees a uniform[variable()]shape.
Validators pass through unchanged. Validator names are global; if the partial references "pem_file" but doesn't ship its own definition, the consuming schema (or another schema in the same merge) must provide it.
Partial translations are funs of arity 3. The loader wraps each one in a normal fun(Conf) -> _ end closure that captures ConfPrefix and AppPrefix. The downstream pipeline sees a standard translation. Arity 2 or anything else is rejected. Plain {translation, ...} is rejected with a message pointing at partial_translation — the partial format uses the distinct head atom so a reader of the file knows immediately that the fun receives extra prefix arguments.
Gotcha — do not hardcode segment counts in partial translations. A pattern like [{[_, _, Key], Val} | _] only matches when the include site happens to use a two-segment conf prefix; deeper or shallower prefixes silently fall through. Derive the trailing segment from the path instead:
{partial_translation, "key",
fun(Conf, ConfPrefix, _AppPrefix) ->
case cuttlefish_variable:filter_by_prefix(ConfPrefix ++ ".key", Conf) of
[{Path, V} | _] when is_list(Path) ->
{list_to_atom(lists:last(Path)), list_to_binary(V)};
_ ->
cuttlefish:unset()
end
end}.%% deps/rabbit/priv/schema/ssl_options.partial
{mapping, "verify", "verify",
[{datatype, {enum, [verify_peer, verify_none]}},
{default, verify_none}]}.
{mapping, "cacertfile", "cacertfile",
[{datatype, file},
{validators, ["pem_file"]},
{see, ["certfile"]}]}.
{mapping, "certfile", "certfile",
[{datatype, file},
{validators, ["pem_file"]}]}.
{mapping, "versions.$version", "versions",
[{datatype, atom}]}.
{partial_translation, "versions",
fun(Conf, ConfPrefix, _AppPrefix) ->
case cuttlefish_variable:filter_by_prefix(
ConfPrefix ++ ".versions", Conf) of
[] -> cuttlefish:unset();
Set -> [V || {_, V} <- Set]
end
end}.Consumer schemas extend or override a partial using the existing merge machinery — no new syntax:
{include_partial, {rabbit, "ssl_options"},
[{prefix, "shovel.ssl"},
{app_prefix, "rabbitmq_shovel.ssl"}]}.
%% Override one mapping's datatype:
{mapping, "shovel.ssl.password", "rabbitmq_shovel.ssl.password",
[merge, {datatype, string}]}.
%% Replace one translation:
{translation, "rabbitmq_shovel.ssl.versions",
fun(_Conf) -> custom_value end}.
%% Add a sibling mapping that isn't in the partial:
{mapping, "shovel.ssl.cacerts.$name", "rabbitmq_shovel.ssl.cacerts",
[{datatype, binary}]}.The merger's last-write-wins rule applies: an inline translation with the same fully-qualified app key as a partial-produced one replaces it.
A schema can include the same partial multiple times with distinct prefixes — for example, rabbit.schema uses this for its four ssl_options contexts (primary listener, definitions HTTPS, AMQP 0-9-1 client, AMQP 1.0 client):
{include_partial, {rabbit, "ssl_options"},
[{prefix, "ssl_options"}, {app_prefix, "rabbit.ssl_options"}]}.
{include_partial, {rabbit, "ssl_options"},
[{prefix, "definitions.tls"}, {app_prefix, "rabbit.definitions.ssl_options"}]}.
{include_partial, {rabbit, "ssl_options"},
[{prefix, "amqp_client.ssl_options"}, {app_prefix, "amqp_client.ssl_options"}]}.Key collisions are impossible because every emitted key carries the include-site prefix.
- Partials cannot include other partials. Nested
include_partialinside a.partialfile is rejected at load time. - Plain
{translation, ...}is rejected inside a partial — usepartial_translationfor the prefix-bound form. - Unknown top-level head atoms are rejected loudly.
The {datatype, T} option specifies how a conf string value is parsed into an Erlang term. Multiple datatypes can be specified as a list; cuttlefish tries each in order and uses the first that succeeds.
Parses a decimal integer string. Erlang type: integer().
ring_size = 64
{integer, [Constraint, ...]} checks constraints in declaration order; the first failure wins. A constraint is one of {min, N}, {max, N}, {gt, N}, {lt, N}, or the shortcut atoms non_negative (= {min, 0}) and positive (= {min, 1}). A bare shortcut may stand in for the list: {integer, non_negative}.
{datatype, {integer, [{min, 1}, {max, 65535}]}}
{datatype, {integer, non_negative}}Three named aliases cover common ranges:
port={integer, [{min, 0}, {max, 65535}]}(accepts the full IANA range, including0for "OS-assigned" semantics)byte={integer, [{min, 0}, {max, 255}]}percent={percent, integer}(0-100)
For settings that accept the atom infinity in addition to a numeric value, add allow_infinity to the constraint list. Both the atom infinity and the string a conf file produces for it parse to the atom infinity; other inputs go through the usual numeric pipeline.
{datatype, {integer, [non_negative, allow_infinity]}}
{datatype, {integer, [{min, 1}, {max, 65535}, allow_infinity]}}The older datatype-list form continues to work and is equivalent for back-compat purposes:
{datatype, [{atom, infinity}, {integer, non_negative}]}A constraint list also accepts validator entries — either by name (looked up in the loaded validator set) or by anonymous function. Constraints fire left-to-right, first failure wins:
{datatype, {integer, [{min, 1}, {max, 65535}, {validator, "power_of_two"}]}}
{datatype, {integer, [{min, 1}, {validator, fun(N) -> N rem 4 =:= 0 end}]}}Out-of-range values surface as {error, {range_violation, {Value, FailedConstraint}}}. Validator failures surface as {error, {constraint_validator_failed, Value}}.
Passes the value through as-is. Erlang type: string() (charlist).
node.name = riak@127.0.0.1
Converts the string to a binary. Erlang type: binary().
secret.key = mysecret
Converts the string to an atom via list_to_atom/1. Erlang type: atom().
storage_backend = bitcask
Parses a floating-point string. Erlang type: float().
threshold = 0.75
Floats accept the same range constraints as integers ({min, N}, {max, N}, {gt, N}, {lt, N}, non_negative, positive). For floats, positive is {gt, 0}. See the integer section above.
Parses true/false. Erlang type: boolean(). Accepts the atoms true or false and the strings "true" or "false" (case-sensitive).
metrics.enabled = true
boolean replaces {enum, [true, false]}, which is heavily duplicated across plugin schemas. Use flag instead when the conf file convention is on/off.
Parses on/off to true/false. Erlang type: boolean().
log.syslog = on
Custom on/off atoms:
{datatype, {flag, enabled, disabled}}Parses enabled/disabled to true/false.
Custom on/off values:
{datatype, {flag, {on, tyk}, {off, torp}}}Parses on/off to tyk/torp.
Parses one of a fixed set of atom values.
{datatype, {enum, [debug, info, warning, error]}}log.level = info
Erlang type: atom().
Parses an IP:Port string. Erlang type: {string(), integer()}.
listener.http.internal = 127.0.0.1:8098
Parses a hostname with optional port. Erlang type: string() or {string(), integer()}.
amqp.hostname = rabbit.example.com:5672
Parses a Unix domain socket path. Accepts either a plain path string or local:PATH:PORT format. Erlang type: string() or {local, string(), integer()}.
listener.unix = /var/run/app.sock
listener.unix = local:/var/run/app.sock:0
Parses a byte size with optional unit suffix (KB, MB, GB). Case-insensitive. Erlang type: integer() (bytes).
object.size.maximum = 50MB
Bounds are written against the parsed byte count:
{datatype, {bytesize, [{min, 1}, {max, 1073741824}]}}
{datatype, {bytesize, non_negative}}
{datatype, {bytesize, [non_negative, allow_infinity]}}Parses a duration string (e.g. 5s, 2h30m). Erlang type: integer() (in the specified unit).
Units: ms (milliseconds), s (seconds), m (minutes), h (hours), d (days), w (weeks), f (fortnights).
{datatype, {duration, ms}}handoff.timeout = 30s
Bounds are expressed in the declared unit:
{datatype, {duration, ms, [{min, 0}, {max, 60000}]}}
{datatype, {duration, s, [non_negative, allow_infinity]}}Parses a percentage string ending in %. {percent, integer} produces an integer() in the range 0-100. {percent, float} produces a float() in the range 0.0-1.0.
cache.fill.ratio = 75%
Parses a tag:value string where the tag is alphanumeric (plus underscores). Erlang type: {atom(), string()}.
credential = plain:mypassword
Like tagged_string but the value part is returned as a binary. Erlang type: {atom(), binary()}.
Parses a comma-separated list of values, each parsed as datatype T. Erlang type: [T]. Lists of lists are not supported.
{datatype, {list, atom}}plugins = plugin_a, plugin_b, plugin_c
Pass the value through as a string. Semantically indicate that the value is a filesystem path. Erlang type: string().
Validates that the value is a non-empty regular expression. The pattern is compiled at config-load time and probed for excessive backtracking. On success the input string is passed through unchanged. Erlang type: string().
{datatype, regex}Backtracking detection uses PCRE's match_limit against short adversarial probes. It catches the common nested-quantifier and overlapping-alternation patterns — (a+)+, (\w+)+, ^([a-z]+)+$, the classic "evil email" regex — that are still pathological in PCRE 8.x. Not covered:
- Nested quantifiers over rare characters (
(z+)+); the probes do not begin with those characters - Patterns that only blow up on inputs unlike the probes
- Patterns PCRE already short-circuits (
(a*)*,(.+)+); these are accepted because they cannot run away on the consumer either
Treat the datatype as a safety net against accidental pathological patterns in copy-pasted regexes, not as a defense against an adversarial author.
Validates that the value parses as a URI with a recognised scheme and a non-empty host. On success the input string is returned (trimmed of surrounding whitespace). Erlang type: string().
%% accepts both HTTP and HTTPS
{datatype, uri}
%% HTTPS only
{datatype, {uri, [https]}}
%% an explicitly provided scheme list
{datatype, {uri, [amqp, amqps]}}Bare uri is equivalent to {uri, [http, https]}. Schemes is a non-empty list of atoms; the scheme in the input is matched case-insensitively. Userinfo (https://user:pass@host), IPv6 literals (https://[::1]:8080), ports, paths, and query strings are all accepted. Reachability and DNS are not checked.
A translation converts one or more conf values into a final Erlang app.config value. Translations run after datatype conversion, so values in Conf are already typed Erlang terms.
{translation, "app.erlang_key",
fun(Conf) ->
Value = cuttlefish:conf_get("some.conf.key", Conf),
transform(Value)
end}.The translation function receives the full typed conf proplist. It must return the Erlang value to write to app.config.
To omit the key from app.config entirely, call cuttlefish:unset/0. To report invalid input, call cuttlefish:invalid/1,2. Both are throws caught by the pipeline.
When a translation exists for an Erlang key, it takes precedence over any direct 1:1 mapping to that key.
A validator is a named predicate applied to a parsed value before translations run.
{validator, "positive_integer", "must be a positive integer",
fun(Value) ->
Value > 0
end}.Reference validators by name in a mapping's validators list:
{mapping, "ring_size", "riak_core.ring_creation_size", [
{datatype, integer},
{validators, ["positive_integer"]}
]}.The validator function receives the typed value (after datatype conversion). Return true to pass, false to fail. On failure, cuttlefish reports the validator description as the error message.
A validator may carry an optional 5th argument: a proplist with {aliases, [Name]} and/or {deprecated, Since, Hint}. Both are independent.
{validator, "new_name", "must be positive",
fun(N) -> N > 0 end,
[{aliases, ["old_name"]},
{deprecated, "3.9.0", "use {integer, positive} datatype"}]}.aliases lets a renamed validator answer to its old name(s) so call sites can migrate at their own pace. The same alias may not appear on two different validators or shadow another validator's canonical name.
deprecated causes cuttlefish to emit a single warn-level log line the first time the validator runs in a given load cycle. Repeat uses are silenced. The hint should point the schema author at the replacement form.
Cuttlefish ships a small set of built-in validators that delegate to the matching datatype:
| Name | Equivalent |
|---|---|
"byte" |
byte datatype (0..255) |
"port" |
port datatype (0..65535) |
"valid_regex" |
regex datatype (rejects empty patterns and patterns prone to catastrophic backtracking) |
"uri" |
no-op marker (accepts any string) |
These are injected on demand only when a mapping actually references them. A user-defined validator with the same name wins silently — the local predicate is intentional, and the deprecation hint on the builtin (which fires only when the builtin is what runs) is the channel for nudging the migration. The intent is to let a schema drop its in-tree definition of one of these names without rewriting call sites; the migration to the native datatype form ({datatype, port} etc.) is then a separate, gradual cleanup.
"uri" is a marker validator by design — it accepts any string, so call sites that historically used a no-op "uri" predicate with template-style values (https://{{node}}/...) keep working when their local definition is deleted. Use the uri datatype when you want structural validation.
These functions are available inside translation functions via the cuttlefish module.
cuttlefish:conf_get(Key, Conf) -> ValueLooks up Key in Conf. Key can be a dot-separated string or a tokenized variable list. Throws {not_found, Key} if the key is absent, which aborts the translation. Use conf_get/3 if you want a default instead.
cuttlefish:conf_get(Key, Conf, Default) -> ValueLike conf_get/2 but returns Default instead of throwing when the key is absent.
cuttlefish:unset() -> no_return()Call inside a translation to omit the Erlang key from the generated app.config entirely.
cuttlefish:invalid(Reason :: string()) -> no_return()
cuttlefish:invalid(Fmt :: io:format(), Args :: [term()]) -> no_return()Call inside a translation to report that the configuration is invalid. Reason is shown to the user.
cuttlefish:warn(Message :: iodata()) -> ok
cuttlefish:warn(Fmt :: io:format(), Args :: [term()]) -> okCall inside a translation to log a warning without aborting.
cuttlefish:otp(MinVersion :: string(), IfGte :: any(), IfLt :: any()) -> any()
cuttlefish:otp(MinVersion :: string(), ActualVersion :: string()) -> boolean()OTP version comparison helper. Useful for generating version-dependent configuration.
{translation, "app.ssl_opts",
fun(Conf) ->
Base = [...],
cuttlefish:otp("26", [{versions, ['tlsv1.3', 'tlsv1.2']} | Base], Base)
end}.Conf files use a sysctl-style key = value syntax.
ring_size = 64
log.error.file = /var/log/error.log
log.syslog = onLines beginning with # are comments.
## This is a comment
# This is also a comment
ring_size = 64Values spanning multiple lines use triple single-quote delimiters (''').
api.config = '''
{
"endpoints": ["/health", "/metrics"],
"timeout": 30
}
'''Leading and trailing newlines inside the delimiters are trimmed. Internal whitespace and newlines are preserved exactly.
One conf file can include another:
include /etc/app/extra.confGlob patterns are supported:
include conf.d/*.confA value can reference another conf key using $(key) syntax:
listener.http.internal = 127.0.0.1:8098
listener.http.external = $(listener.http.internal)Substitutions are resolved after alias rewriting. Using an alias key in a substitution (e.g. $(old.key)) is an error; use the canonical key instead.
Conf files starting with a UTF-8, UTF-16 BE/LE, or UTF-32 BE/LE byte order mark are handled correctly. The BOM is stripped before parsing.
Understanding the pipeline helps when debugging unexpected behavior.
-
Parse conf files -
cuttlefish_conf:file/1parses.confinto a[{variable(), string()}]proplist. Values are raw strings at this stage. -
Load schema files -
cuttlefish_schema:files/1parses.schemafiles, merges them, validates aliases, and returns a{[translation()], [mapping()], [validator()]}tuple. -
Resolve aliases - Alias keys in conf are rewritten to their canonical keys. Deprecation warnings are logged. Canonical key wins if both are present.
-
Add defaults - Schema defaults are injected for any key absent from conf.
-
Expand substitutions -
$(key)references in values are resolved. -
Convert datatypes - String values are converted to typed Erlang terms using each mapping's declared datatype.
-
Run validators - Named validators are applied to typed values.
-
Run translations - Translation functions produce the final Erlang values. Direct 1:1 mappings (and
{collect, Type}mappings) are applied here as well.
The output is a [{AppName, [{Key, Value}]}] proplist suitable for use as app.config.
When migrating a schema, the safest check is "the generated app.config is the same for any valid input". cuttlefish_diff:render_normalised/1,2 takes the output of cuttlefish_generator:map/2 and returns a deterministic, sorted, printable form suitable for line-diffing:
{ok, Before} = cuttlefish_generator:map(OldSchemas, Conf),
{ok, After} = cuttlefish_generator:map(NewSchemas, Conf),
A = cuttlefish_diff:render_normalised(Before, [{skip_funs, true}]),
B = cuttlefish_diff:render_normalised(After, [{skip_funs, true}]),
%% feed A and B to your diff tool of choiceTwo equivalent configs that differ only by internal key order produce the same string; any real difference shows up as a localised line-level change. The second argument is an options proplist:
| Option | Default | Effect |
|---|---|---|
{skip_funs, bool} |
true |
Render every function as the stable placeholder #Fun<>. With false, use Erlang's ~p form, which may differ between runs |
{atom_quoting, strict | loose} |
loose |
strict quotes atoms via ~p (e.g. 'with-dashes'); loose emits the bare name |
The library function is intended to be called from CI scripts and migration verification harnesses. There is no CLI subcommand in 3.9.0.