Caravela.Gen.Custom (Caravela v0.12.0)

Copy Markdown View Source

Preserves user-authored code across regenerations.

Generators emit a language-appropriate CUSTOM marker before the closing of every regenerable file. Anything the developer writes below that marker is preserved verbatim when the generator runs again.

Caravela also stamps a checksum header at the top of every generated file:

# caravela-gen: generator=context version=0.8.0 above_sha256=<hex>

On regeneration the checksum is re-computed from the file on disk. If the content above the marker was edited by hand, the hashes no longer match and verify_existing!/2 aborts via Mix.raise/1 — the user's edits would otherwise be silently overwritten. Pass force: true (from the mix task's --force flag) to bypass.

Three comment styles are supported via the :style opt:

  • :elixir (default) — # …
  • :ts// … (TypeScript)
  • :svelte<!-- … --> (HTML/Svelte top-level)

Summary

Types

Comment style for a generated file's CUSTOM markers.

Functions

Extract all named CUSTOM blocks from an Elixir source. Returns %{name => body} where name is a binary and body is the raw text between the CUSTOM :name and END :name markers.

Marker string for the given style (default :elixir).

Reusable marker block appended by Elixir generators. Terminates the module with end once custom content is merged in. Only meaningful for the :elixir style; Svelte and TS templates embed their own trailing marker inline.

Merge custom content from an existing source into a newly-generated source. If the existing source does not contain the marker, the new source is returned unchanged.

Merge named CUSTOM blocks from existing_source into new_source.

Merge custom content from disk, after verifying the on-disk file has not been tampered with above the marker.

Empty named CUSTOM block, rendered at its extension point by generator templates. The indent opt controls leading whitespace.

Parse a header line into {:ok, %{generator, version, hash}} or :error. Exposed for tests and tooling.

Stamp a fresh caravela-gen: header onto source, replacing any existing header line on the first line.

Inspect source contents. Returns :ok, :no_header, or {:mismatch, stored_hash, current_hash}.

Verify path's on-disk content against the checksum stored in its caravela-gen: header. Returns :ok when

Types

style()

@type style() :: :elixir | :ts | :svelte

Comment style for a generated file's CUSTOM markers.

Functions

extract_named_blocks(source)

@spec extract_named_blocks(String.t()) :: %{required(String.t()) => String.t()}

Extract all named CUSTOM blocks from an Elixir source. Returns %{name => body} where name is a binary and body is the raw text between the CUSTOM :name and END :name markers.

Only parses Elixir-style markers (# --- CUSTOM :name ---). The other styles don't support named blocks in this release.

marker(style \\ :elixir)

@spec marker(style()) :: String.t()

Marker string for the given style (default :elixir).

marker_block()

@spec marker_block() :: String.t()

Reusable marker block appended by Elixir generators. Terminates the module with end once custom content is merged in. Only meaningful for the :elixir style; Svelte and TS templates embed their own trailing marker inline.

merge(new_source, existing_source, opts \\ [])

@spec merge(String.t(), String.t(), keyword()) :: String.t()

Merge custom content from an existing source into a newly-generated source. If the existing source does not contain the marker, the new source is returned unchanged.

Splits on the last occurrence of the marker in both sources. This is deliberate: the marker string commonly appears inside @moduledoc / <!-- --> prose where it is being named rather than delimiting user code. The real marker is always the last one (emitted at the tail of the template). Using first-occurrence would cut the file at the docstring and produce garbage.

merge_named(new_source, existing_source, opts \\ [])

@spec merge_named(String.t(), String.t(), keyword()) :: String.t()

Merge named CUSTOM blocks from existing_source into new_source.

For every named block in new_source, if existing_source has a block with the same name, the new source's body is replaced with the existing body. Orphan blocks (present in existing but absent in new) are reported via Mix.shell/0 and discarded.

Only applies to Elixir-style markers. Returns new_source unchanged for any other style.

merge_with_file(new_source, path, opts \\ [])

@spec merge_with_file(String.t(), Path.t(), keyword()) :: String.t()

Merge custom content from disk, after verifying the on-disk file has not been tampered with above the marker.

opts:

  • :style (default :elixir) — :elixir | :ts | :svelte.

  • :force (default false) — skip the verification step.

Raises via Mix.raise/1 when the file's stored checksum does not match its current above-marker body and force: true was not passed. Returns the merged source otherwise.

named_empty(name, opts \\ [])

@spec named_empty(
  atom() | String.t(),
  keyword()
) :: String.t()

Empty named CUSTOM block, rendered at its extension point by generator templates. The indent opt controls leading whitespace.

Example

iex> Caravela.Gen.Custom.named_empty(:list_books, indent: "  ")
"  # --- CUSTOM :list_books ---\n  # --- END :list_books ---"

parse_header_line(line, style \\ :elixir)

@spec parse_header_line(String.t(), style()) ::
  {:ok,
   %{hash: String.t(), generator: String.t() | nil, version: String.t() | nil}}
  | :error

Parse a header line into {:ok, %{generator, version, hash}} or :error. Exposed for tests and tooling.

stamp_header(source, opts)

@spec stamp_header(
  String.t(),
  keyword()
) :: String.t()

Stamp a fresh caravela-gen: header onto source, replacing any existing header line on the first line.

opts:

  • :generator (required) — atom identifying the generator.
  • :style (default :elixir) — comment style for the header.
  • :version — override the default Caravela version string.

The checksum covers every byte above the CUSTOM marker, excluding the header line itself. Call after any formatting step so the hash reflects the bytes actually written to disk.

verify_contents(contents, style \\ :elixir)

@spec verify_contents(String.t(), style()) ::
  :ok | :no_header | {:mismatch, String.t(), String.t()}

Inspect source contents. Returns :ok, :no_header, or {:mismatch, stored_hash, current_hash}.

Pure function. Useful from tests and mix caravela.gen --check.

verify_existing!(path, opts \\ [])

@spec verify_existing!(
  Path.t(),
  keyword()
) :: :ok

Verify path's on-disk content against the checksum stored in its caravela-gen: header. Returns :ok when:

  • the file does not exist;
  • the file has no Caravela header (first-time adoption);
  • the file has a header and the stored hash matches the current above-marker body.

Raises via Mix.raise/1 when the stored hash does not match, unless opts[:force] is true.