Elixir shape mirroring the real Rust ggen::Frontmatter struct 1:1
(~/ggen/crates/ggen-engine/src/template.rs:40-175, ggen v26.8.24).
This module exists so a template-frontmatter block parsed on the Elixir side
(ggen_igniter) and the same block parsed on the Rust/WASM side (the
planned WASM bridge for the templating/write-safety engine, handled by
another agent) describe the identical field set with the identical names —
no field renames, no field drops, no Elixir-only additions to this struct.
Fields not yet consumed by any ggen_igniter code path are still declared
here (as nil-defaulted) so the shape stays a faithful mirror; wiring their
actual behavior is separate follow-on work, not part of this alignment
pass.
Field-by-field provenance (all from template.rs:40-175 unless noted):
to—String(required). Output path template string.sparql—%{String.t() => String.t()}, default%{}. Named query sources (Rust:BTreeMap<String, String>viasparql_map).for_each—String.t() | nil. Name of the query whose rows drive multi-row fan-out.construct—String.t() | nil. A CONSTRUCT query string.inject—boolean(), defaultfalse.before/after—GgenIgniter.Frontmatter.MatchSpec.t() | nil.at_line—pos_integer() | nil.skip_if—GgenIgniter.Frontmatter.MatchSpec.t() | nil.unless_exists—boolean(), defaultfalse.unattended_write_eligible—boolean(), defaultfalse.force—boolean(), defaultfalse.when—String.t() | nil. A SPARQL ASK guard expression.skip_empty—boolean(), defaultfalse.from—String.t() | nil.sh_before—String.t() | nil(Rust field is aliased fromsh).sh_after—String.t() | nil.backup—boolean(), defaultfalse.shape—[String.t()], default[]. SHACL shape file paths.determinism—boolean() | nil.freeze_policy—GgenIgniter.Frontmatter.FreezePolicy.t() | nil.freeze_slots_dir—String.t() | nil.rdf—[String.t()], default[].rdf_inline—[String.t()], default[].prefixes—%{String.t() => String.t()}, default%{}.base—String.t() | nil.
inject/before/after/at_line are wired into mix ggen_igniter.sync
When a mode: file template's frontmatter has inject: true,
Mix.Tasks.GgenIgniter.Sync dispatches the rendered body to
GgenIgniter.Actuate.inject_content!/5 against the resolved output path,
instead of GgenIgniter.Actuate.write_file!/3 -- using exactly one of
before:, after:, or at_line: as the anchor (a template with zero, or
more than one, of these three set while inject: true raises a clear,
named ArgumentError -- an ambiguous anchor is a template-authoring error,
never a silent pick). before/after's GgenIgniter.Frontmatter.MatchSpec.t/0
is converted into inject_content!/5's real marker arg
(String.t() | Regex.t() | nil) by Mix.Tasks.GgenIgniter.Sync's private
match_spec_to_marker!/2 -- a {:literal, s} spec maps directly onto a
plain string marker; a {:structured, %MatchRule{}} spec honors matcher
(:contains/:exact/:regex) and case_sensitive, and trim when
paired with matcher: :exact, converting each combination into the
equivalent Regex (or, for the :contains + case-sensitive default, the
same plain string inject_content!/5 already treats as a substring match).
scope: :file, any occurrence other than the default :first, and
trim: true on a non-:exact matcher have no equivalent in
inject_content!/5's real anchor-resolution behavior (it always requires
the marker to match EXACTLY one line, has no whole-file matching mode, and
no "pick the last/nth occurrence" logic) -- setting one of those raises a
clear error naming the exact unsupported combination, rather than being
silently dropped. --dry-run previews an injection the same honest way it
previews a write: the real anchor/idempotency check runs
(inject_content!/5's own :dry_run option), nothing is written, and the
notice line reads "planned: inject ..." / "planned: skip ... (unchanged)".
See Mix.Tasks.GgenIgniter.Sync's moduledoc (## Injection mode (inject: true)) for the full CLI-facing behavior and examples.
Summary
Functions
Builds a t/0 from a plain string-keyed map (e.g. the result of parsing a
YAML frontmatter block), applying the same field-name-for-field-name mapping
as the Rust struct -- no renames. Raises ArgumentError if "to" is
missing (mirrors Rust's non-Option to: String being a required field).
Same as from_map/1, but accepts require_to: false to skip the "to" is
required check -- used only for mode: eval templates (see
split_template/1), which write nothing to disk and so have no output path
to require. Every other field's mapping is identical to from_map/1.
Splits a raw template file's text into its leading YAML frontmatter block
(if any), its execution mode, and the remaining template body, mirroring
the real Rust ggen's own header convention (a --- fence at the very top of
the file, matched by ggen-engine's frontmatter scanner) and hygen's
identical ---\n...\n---\n convention that this module's moduledoc names
as the parity target.
Types
@type t() :: %GgenIgniter.Frontmatter{ after: GgenIgniter.Frontmatter.MatchSpec.t() | nil, at_line: pos_integer() | nil, backup: boolean(), base: String.t() | nil, before: GgenIgniter.Frontmatter.MatchSpec.t() | nil, construct: String.t() | nil, determinism: boolean() | nil, for_each: String.t() | nil, force: boolean(), freeze_policy: GgenIgniter.Frontmatter.FreezePolicy.t() | nil, freeze_slots_dir: String.t() | nil, from: String.t() | nil, inject: boolean(), prefixes: %{optional(String.t()) => String.t()}, rdf: [String.t()], rdf_inline: [String.t()], sh_after: String.t() | nil, sh_before: String.t() | nil, shape: [String.t()], skip_empty: boolean(), skip_if: GgenIgniter.Frontmatter.MatchSpec.t() | nil, sparql: %{optional(String.t()) => String.t()}, to: String.t(), unattended_write_eligible: boolean(), unless_exists: boolean(), when: String.t() | nil }
Functions
Builds a t/0 from a plain string-keyed map (e.g. the result of parsing a
YAML frontmatter block), applying the same field-name-for-field-name mapping
as the Rust struct -- no renames. Raises ArgumentError if "to" is
missing (mirrors Rust's non-Option to: String being a required field).
See from_map/2 for the require_to: false escape hatch used by
split_template/1 for mode: eval templates, which have no to: at all.
Examples
iex> fm = GgenIgniter.Frontmatter.from_map(%{"to" => "lib/foo.ex"})
iex> {fm.to, fm.sparql, fm.skip_empty}
{"lib/foo.ex", %{}, false}
iex> GgenIgniter.Frontmatter.from_map(%{})
** (ArgumentError) frontmatter is missing required field "to"
Same as from_map/1, but accepts require_to: false to skip the "to" is
required check -- used only for mode: eval templates (see
split_template/1), which write nothing to disk and so have no output path
to require. Every other field's mapping is identical to from_map/1.
Splits a raw template file's text into its leading YAML frontmatter block
(if any), its execution mode, and the remaining template body, mirroring
the real Rust ggen's own header convention (a --- fence at the very top of
the file, matched by ggen-engine's frontmatter scanner) and hygen's
identical ---\n...\n---\n convention that this module's moduledoc names
as the parity target.
mode is read from the same YAML block's mode: key (default "file",
the only behavior that existed before this field did), but is deliberately
NOT one of t/0's mirrored fields -- it has no counterpart in the real
Rust ggen::Frontmatter struct this module otherwise mirrors 1:1. It is an
Elixir/Igniter-specific execution concern: :file (default) means the
rendered body is written to disk as before; :eval means the rendered body
is real Elixir source meant to be Code.eval_string'd in-process instead
(see Mix.Tasks.GgenIgniter.Sync's ## Execution mode docs) -- never
written to disk at all. from_map/1 already ignores unrecognized keys, so
mode: sitting in the same block as every mirrored field is harmless to it.
Returns {%__MODULE__{}, mode, body} when the file starts with a ---
fence on its own line (the fence must be the file's first line, matching
both ggen's and hygen's convention -- leading whitespace before it means "no
header", not "malformed header"), or {nil, :file, template_string}
unchanged when it does not -- a template with no header is not an error, it
just has no frontmatter-derived defaults (every routing option must then
come from the CLI, exactly as before this function existed).
Examples
iex> {fm, mode, body} = GgenIgniter.Frontmatter.split_template("---\nto: lib/foo.ex\n---\nhello")
iex> {fm.to, mode, body}
{"lib/foo.ex", :file, "hello"}
iex> GgenIgniter.Frontmatter.split_template("plain body, no header")
{nil, :file, "plain body, no header"}