# `GgenIgniter.Frontmatter`
[🔗](https://github.com/seanchatmangpt/ggen_igniter/blob/v26.9.8/lib/ggen_igniter/frontmatter.ex#L1)

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>` via `sparql_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()`, default `false`.
- `before` / `after` — `GgenIgniter.Frontmatter.MatchSpec.t() | nil`.
- `at_line` — `pos_integer() | nil`.
- `skip_if` — `GgenIgniter.Frontmatter.MatchSpec.t() | nil`.
- `unless_exists` — `boolean()`, default `false`.
- `unattended_write_eligible` — `boolean()`, default `false`.
- `force` — `boolean()`, default `false`.
- `when` — `String.t() | nil`. A SPARQL ASK guard expression.
- `skip_empty` — `boolean()`, default `false`.
- `from` — `String.t() | nil`.
- `sh_before` — `String.t() | nil` (Rust field is aliased from `sh`).
- `sh_after` — `String.t() | nil`.
- `backup` — `boolean()`, default `false`.
- `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 `t: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.

# `t`

```elixir
@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()) =&gt; 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()) =&gt; String.t()},
  to: String.t(),
  unattended_write_eligible: boolean(),
  unless_exists: boolean(),
  when: String.t() | nil
}
```

# `from_map`

```elixir
@spec from_map(map()) :: t()
```

Builds a `t: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"

# `from_map`

```elixir
@spec from_map(
  map(),
  keyword()
) :: t()
```

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`.

# `split_template`

```elixir
@spec split_template(String.t()) :: {t() | nil, :file | :eval, String.t()}
```

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: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"}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
