Mutare.Phoenix.Swoosh.Layout (mutare_phoenix_swoosh v0.2.1)

Copy Markdown View Source

:mail_layout — mutates calls that configure which layout wraps the rendered body.

Two kinds, variant-labelled:

Setter removal (labels put / put_new) — a Phoenix.Swoosh layout-configuration call collapses to the email it would have returned:

put_layout(email, {LayoutView, "email.html"})      ->  email
put_new_layout(email, {LayoutView, :email})        ->  email
email |> put_layout({LayoutView, "email.html"})    ->  email

put (put_layout/2) leaves the email with its previous layout, usually the use-configured one; put_new (put_new_layout/2) leaves the layout unset and the body renders bare. Either way the templates still render and the email is still sent.

Layout suppression at the render site (label off) — render_body/2,3 takes the layout from its assigns when one is given there (phoenix_swoosh's per-render override), so setting it to false renders the body with no layout at all:

render_body(email, :welcome, %{name: name})
  ->  render_body(email, :welcome, %{name: name, layout: false})
render_body(email, :welcome, %{layout: {LayoutView, :promo}})
  ->  render_body(email, :welcome, %{layout: false})
email |> render_body(:welcome)
  ->  email |> render_body(:welcome, %{layout: false})

A survivor of either kind means no test asserts which layout wrapped the rendered body — the branded wrapper, the unsubscribe footer, the logo header. Suppress one kind with # mutare:ignore[mail_layout:put] / [mail_layout:put_new] / [mail_layout:off], or the family with # mutare:ignore[mail_layout].

Why the render site, and when off applies

Real mailers rarely call put_layout at all: the idiomatic place for a layout is the use line (use Phoenix.Swoosh, view: MyApp.EmailView, layout: {MyApp.LayoutView, :email}), which is compile-time configuration excluded from mutation (see the README's "What's deliberately out of scope"). The off mutant tests layout assertions in mailers that configure their layout this way. Setting the assigns override to false disables the layout at runtime, wherever it was configured.

Suppressing a layout that was never in effect is an equivalent mutant, so off is generated only when a layout demonstrably is in effect at the site:

  • the mailer's use Phoenix.Swoosh line configured one — reported to this family by Mutare.Phoenix.Swoosh.LayoutConfigured, the marker the package's :extensions entry injects while expanding that use; or
  • the call's own assigns literal contains a truthy layout: entry, which needs no marker because the author wrote the layout right there.

Without the :extensions entry there is no marker, so off applies to the second case only. A mailer that configures its layout by calling put_layout gets the put removal at that call instead, testing the same gap without a second mutation at the render call.

Written forms and arities

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

Matches direct (Phoenix.Swoosh.put_layout(...)), aliased, and bare-imported calls. Unlike :render_body, the setters' bare form needs no use-expansion override: they are genuine Phoenix.Swoosh exports that the injected import Phoenix.Swoosh, except: [render_body: 3] brings into scope, so Mutare's in-process use expansion resolves them without an override. Bare render_body calls do need it, which is why this family declares the same render_body routes as :render_body, using a shared internal routes helper. Identical declarations from two providers coalesce in the registry, so either family alone still resolves and pins the call.

The layout argument is pinned

The layout value — {LayoutView, "email.html"}, {LayoutView, :email}, a bare name once a layout view is set, or false — is structural configuration, not a computed value: a perturbed template name inside the tuple is a missing-template crash at render (an uninformative kill), and a flipped false is a put_layout-contract raise. Both setters' argument 1 is routed :raw in the call-routing registry, which covers the position's whole subtree (the string inside the tuple included) against every family — the reason a route is used here rather than an argument mark, which pins only the argument's own node.

The same value written as a layout: assign is not pinned: it sits inside the assigns map, which stays ordinary runtime data so core's value families can keep mutating the template variables around it. Core will therefore perturb an assigns layout tuple into missing-template crash-kills, as it would anywhere. Routing the whole assigns argument :raw would exclude every useful mutation inside it, and :interior (which excludes only the map's own node from mutation) does not exclude a tuple nested in it. Use # mutare:ignore at the site to suppress these mutations.

Deliberately left alone

  • put_view/2 and put_new_view/2 are not mutated: removing them leaves the render with no view module in the standalone flow, and render_body then raises ("a view module was not specified") — a crash-kill, not a signal. There is nothing to pin either: their argument is a module alias, which no value family mutates. layout/1 is a getter with no seam.
  • Narrowing a layout name ({LayoutView, :email} → {LayoutView, "email.html"}, so both body parts render inside the html layout) is the exact analogue of :render_body's template narrowing and probes a real gap — no test asserts the text part's wrapper. It is deferred rather than rejected: whether an html layout renders around a text body cleanly or raises inside Phoenix.View has not yet been verified against the real library. The sites are also vanishingly rare (see "Why the render site" above).
  • Swapping put_new_layout for put_layout (and the same for the view setters) is the phoenix_swoosh analogue of Mutare.Mutators.MapKeyword's put ↔ put_new lattice and would not overlap with existing mutations. It is not generated because the only put_new_layout call in a typical mailer is the one the use wrapper generates — macro-generated code, which Mutare does not mutate.