GgenIgniter.Frontmatter (ggen_igniter v26.9.8)

Copy Markdown View Source

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):

  • toString (required). Output path template string.
  • sparql%{String.t() => String.t()}, default %{}. Named query sources (Rust: BTreeMap<String, String> via sparql_map).
  • for_eachString.t() | nil. Name of the query whose rows drive multi-row fan-out.

  • constructString.t() | nil. A CONSTRUCT query string.

  • injectboolean(), default false.
  • before / afterGgenIgniter.Frontmatter.MatchSpec.t() | nil.

  • at_linepos_integer() | nil.

  • skip_ifGgenIgniter.Frontmatter.MatchSpec.t() | nil.

  • unless_existsboolean(), default false.
  • unattended_write_eligibleboolean(), default false.
  • forceboolean(), default false.
  • whenString.t() | nil. A SPARQL ASK guard expression.

  • skip_emptyboolean(), default false.
  • fromString.t() | nil.

  • sh_beforeString.t() | nil (Rust field is aliased from sh).

  • sh_afterString.t() | nil.

  • backupboolean(), default false.
  • shape[String.t()], default []. SHACL shape file paths.
  • determinismboolean() | nil.

  • freeze_policyGgenIgniter.Frontmatter.FreezePolicy.t() | nil.

  • freeze_slots_dirString.t() | nil.

  • rdf[String.t()], default [].
  • rdf_inline[String.t()], default [].
  • prefixes%{String.t() => String.t()}, default %{}.
  • baseString.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

t()

@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

from_map(map)

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

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"

from_map(map, opts)

@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(template_string)

@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/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"}