# `Mutare.Phoenix.Swoosh.RenderBody`
[🔗](https://github.com/foxbenjaminfox/mutare_phoenix_swoosh/blob/v0.2.1/lib/mutare/phoenix/swoosh/render_body.ex#L1)

`:render_body` — mutates calls that control **which email body parts are rendered**:
`Phoenix.Swoosh.render_body/2,3`, the call that renders an email's templates onto its
`html_body`/`text_body` fields, and the format map of `Phoenix.Swoosh.put_new_formats/2`,
which maps template extensions to body fields. Three kinds, variant-labelled:

**Removal** (label `remove`) — the call collapses to the email it received, so the email is
built, addressed, and delivered with no rendered body at all:

    render_body(email, :welcome, %{name: name})  ->  email
    email |> render_body(:welcome)               ->  email

A survivor means no test asserts the rendered body content — the mail pipeline is exercised,
but nothing checks that `html_body`/`text_body` ever got set.

**Atom-template narrowing** (labels `html_only` / `text_only`) — an atom template renders
*both* the `.html` and the `.text` template; the string form renders exactly one. Narrowing
the literal atom to each string form drops the other body part:

    render_body(email, :welcome, assigns)  ->  render_body(email, "welcome.html", assigns)
                                           ->  render_body(email, "welcome.text", assigns)

A survivor means no test asserts the *other* body part. Use
`Swoosh.TestAssertions.assert_email_sent/1` on both bodies, or a direct assertion such as
`email.text_body =~ ...`, to kill it. Narrowing applies only to a literal atom template;
a string or computed template gets the removal mutant alone.

**Format dropping** (label `format`) — a literal `put_new_formats/2` map with more than one
entry loses one entry per mutant, so that extension's template stops rendering:

    put_new_formats(email, %{"html" => :html_body, "amp" => :html_body})
      ->  put_new_formats(email, %{"amp" => :html_body})
      ->  put_new_formats(email, %{"html" => :html_body})

This is the custom-formats form of the same gap the narrowings probe — and the *only* form of
it once a mailer configures `:formats`, since the narrowings' `.html`/`.text` extensions may
not exist there (a narrowed mutant then causes a missing-template crash: an uninformative
kill, never a wrong survivor). A single-entry map produces nothing: dropping the only format
renders no body at all, duplicating the `remove` mutation.

Suppress one kind with `# mutare:ignore[render_body:remove]` / `[render_body:html_only]` /
`[render_body:text_only]` / `[render_body:format]`, or the family with
`# mutare:ignore[render_body]`.

Mutations apply only to the supported arities — `render_body/2,3` (`/2` is the
`use`-injected wrapper's default-assigns form) and `put_new_formats/2` — so a name-matched
call of any other arity is left alone, keeping every metamutant compiling. A piped call is
the same call with the email piped in, and its removal collapses the stage to that email.

## How the three kinds relate

At an atom-template site the removal mutant is the *weakest* of the three: any assertion that
kills a narrowing also kills the removal, so a surviving `remove` where both narrowings also
survive is one gap reported three times, not three gaps. It is kept because it is the only
mutant at a string or computed template site, and the only one that also drops the assigns
merge (a test that reads `email.assigns` rather than the bodies kills it alone).

## The template and format positions are pinned

The template name (argument 1 — atom or string) is a structural identifier, not a
computed value: a perturbed name is a missing-template crash at render time (an
uninformative crash-kill), never a signal. `put_new_formats/2`'s extension→field map is the
same kind of value — a perturbed extension key is a missing template, a perturbed field value
is a crash-free but meaningless field swap. This family registers all three calls in the
call-routing registry with those positions `:raw`, so **no** family — core value literals
included — mutates them or anything inside them, at bare, qualified, and aliased call sites
alike. The registry route (rather than an argument mark) is deliberate: it covers the
position's whole subtree, and registration is also what makes the bare forms resolvable and
witness-safe at all (see below). The `:raw` route excludes the argument subtree from
mutation, while this family can still rewrite the argument as part of a call-level mutation.

## Resolution: why this family is half of a matched pair

`use Phoenix.Swoosh` injects `import Phoenix.Swoosh, except: [render_body: 3]` plus a local
`def render_body/2,3` wrapper — so a mailer's bare `render_body` calls resolve to a local
definition that Mutare does not resolve. `Mutare.Phoenix.Swoosh` (listed under `:extensions`)
overrides that `use` expansion to return a whole `import Phoenix.Swoosh` in its place.
This family's registry entries serve two purposes:

  * the `/2` wrapper arity is not a real `Phoenix.Swoosh` export, so import reflection can
    never resolve a bare `render_body(email, :welcome)` — the registry's whole-import
    fallback is what resolves it;
  * a bare imported call normally carries a compile-time **witness** that re-imports the
    resolved provider next to the mutant — which, next to the real injected local `def`,
    is an import/local conflict that would fail the single metamutant compile. Registered
    calls have that witness dropped.

Without the extension, bare `render_body` sites are neither mutated nor pinned (the
injected `except:` import excludes exactly this one name); qualified and aliased
sites work regardless.

## Deliberately left alone

  * **The assigns argument** is ordinary runtime data — core's families mutate the *values*
    inside it as they would anywhere (a wrong interpolated value in a body only a
    body-content assertion catches). Its *keys* are likewise subject to core's map-literal
    mutation rules, not pinned here. `Mutare.Phoenix.Swoosh.Layout` mutates the `:layout`
    assign, which configures phoenix_swoosh rather than supplying template data.
  * **Dropping an assigns entry** was considered and rejected: a template referencing the
    dropped assign raises `KeyError`/`ArgumentError` at render — a crash-kill, not a
    test of body-content assertions.
  * **Swapping a format's body field** (`"html" => :html_body` → `:text_body`) is not generated:
    the rendered html would land in `text_body` alongside whatever the text template put
    there, and which one survives depends on map ordering — a mutant whose behaviour a reader
    cannot predict from its diff.

---

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