From f52dc1bf7fef9d1b7f09a77b08a8bfaf9aa1ff80 Mon Sep 17 00:00:00 2001 From: German Velasco Date: Fri, 19 Feb 2021 09:33:39 -0500 Subject: [PATCH] Extract Bamboo.Phoenix (#581) What changed? ============= We extract `Bamboo.Phoenix` to a standalone library. That makes it easier for those who want to use Bamboo but don't want to depend on Phoenix. Prior to this commit, the Phoenix dependency was optional. But now, those who want to use `Phoenix`'s rendering engine can use [`bamboo_phoenix`]. [`bamboo_phoenix`]: https://github.com/thoughtbot/bamboo_phoenix --- README.md | 7 +- lib/bamboo/phoenix.ex | 328 ------------------ mix.exs | 6 - mix.lock | 2 - test/lib/bamboo/phoenix_test.exs | 121 ------- .../templates/phoenix_layout/app.html.eex | 2 - .../templates/phoenix_layout/app.text.eex | 2 - 7 files changed, 5 insertions(+), 463 deletions(-) delete mode 100644 lib/bamboo/phoenix.ex delete mode 100644 test/lib/bamboo/phoenix_test.exs delete mode 100644 test/support/templates/phoenix_layout/app.html.eex delete mode 100644 test/support/templates/phoenix_layout/app.text.eex diff --git a/README.md b/README.md index 2e28a884..3c6c6764 100644 --- a/README.md +++ b/README.md @@ -261,8 +261,11 @@ examples. ## Using Phoenix Views and Layouts -Phoenix is not required to use Bamboo. However, if you do use Phoenix, you can -use Phoenix views and layouts with Bamboo. See [`Bamboo.Phoenix`]. +Phoenix is not required to use Bamboo. But if you want to use Phoenix's views +and layouts to render emails, see [`bamboo_phoenix`] and [`Bambooo.Phoenix`]. + +[`bamboo_phoenix`]: https://github.com/thoughtbot/bamboo_phoenix +[`Bambooo.Phoenix`]: https://hexdocs.pm/bamboo_phoenix/Bamboo.Phoenix.html ## Viewing Sent Emails diff --git a/lib/bamboo/phoenix.ex b/lib/bamboo/phoenix.ex deleted file mode 100644 index 538bde60..00000000 --- a/lib/bamboo/phoenix.ex +++ /dev/null @@ -1,328 +0,0 @@ -defmodule Bamboo.Phoenix do - @moduledoc """ - Render emails with Phoenix templates and layouts. - - This module allows rendering emails with Phoenix layouts and views. Pass an - atom (e.g. `:welcome_email`) as the template name to render both HTML and - plain text emails. Use a string if you only want to render one type, e.g. - `"welcome_email.text"` or `"welcome_email.html"`. - - ## Examples - - _Set the text and HTML layout for an email._ - - defmodule MyApp.Email do - use Bamboo.Phoenix, view: MyAppWeb.EmailView - - def welcome_email do - new_email() - |> put_text_layout({MyAppWeb.LayoutView, "email.text"}) - |> put_html_layout({MyAppWeb.LayoutView, "email.html"}) - |> render(:welcome) # Pass atom to render html AND plain text templates - end - end - - _Set both the text and HTML layout at the same time for an email._ - - defmodule MyApp.Email do - use Bamboo.Phoenix, view: MyAppWeb.EmailView - - def welcome_email do - new_email() - |> put_layout({MyAppWeb.LayoutView, :email}) - |> render(:welcome) - end - end - - _Render both text and html emails without layouts._ - - defmodule MyApp.Email do - use Bamboo.Phoenix, view: MyAppWeb.EmailView - - def welcome_email do - new_email() - |> render(:welcome) - end - end - - _Make assigns available to a template._ - - defmodule MyApp.Email do - use Bamboo.Phoenix, view: MyAppWeb.EmailView - - def welcome_email(user) do - new_email() - |> assign(:user, user) - |> render(:welcome) - end - end - - _Make assigns available to a template during render call._ - - defmodule MyApp.Email do - use Bamboo.Phoenix, view: MyAppWeb.EmailView - - def welcome_email(user) do - new_email() - |> put_html_layout({MyAppWeb.LayoutView, "email.html"}) - |> render(:welcome, user: user) - end - end - - _Render an email by passing the template string to render._ - - defmodule MyApp.Email do - use Bamboo.Phoenix, view: MyAppWeb.EmailView - - def html_email do - new_email - |> render("html_email.html") - end - - def text_email do - new_email - |> render("text_email.text") - end - end - - ## HTML Layout Example - - # my_app_web/email.ex - defmodule MyApp.Email do - use Bamboo.Phoenix, view: MyAppWeb.EmailView - - def sign_in_email(person) do - base_email() - |> to(person) - |> subject("Your Sign In Link") - |> assign(:person, person) - |> render(:sign_in) - end - - defp base_email do - new_email - |> from("Rob Ot") - |> put_header("Reply-To", "editors@changelog.com") - # This will use the "email.html.eex" file as a layout when rendering html emails. - # Plain text emails will not use a layout unless you use `put_text_layout` - |> put_html_layout({MyAppWeb.LayoutView, "email.html"}) - end - end - - # my_app_web/views/email_view.ex - defmodule MyAppWeb.EmailView do - use MyAppWeb, :view - end - - # my_app_web/templates/layout/email.html.eex - - - "> - - - <%= render @view_module, @view_template, assigns %> - - - - # my_app_web/templates/email/sign_in.html.eex -

<%= link "Sign In", to: sign_in_url(MyApp.Endpoint, :create, @person) %>

- - # my_app_web/templates/email/sign_in.text.eex - # This will not be rendered within a layout because `put_text_layout` was not used. - Sign In: <%= sign_in_url(MyApp.Endpoint, :create, @person) %> - """ - - import Bamboo.Email, only: [put_private: 3] - - defmacro __using__(view: view_module) do - verify_phoenix_dep() - - quote do - import Bamboo.Email - import Bamboo.Phoenix, except: [render: 3] - - @doc """ - Render an Phoenix template and set the body on the email. - - Pass an atom as the template name (:welcome_email) to render HTML *and* plain - text emails. Use a string if you only want to render one type, e.g. - "welcome_email.text" or "welcome_email.html". Scroll to the top for more examples. - """ - def render(email, template, assigns \\ []) do - Bamboo.Phoenix.render_email(unquote(view_module), email, template, assigns) - end - end - end - - defmacro __using__(opts) do - raise ArgumentError, """ - expected Bamboo.Phoenix to have a view set, instead got: #{inspect(opts)}. - - Please set a view e.g. use Bamboo.Phoenix, view: MyAppWeb.MyView - """ - end - - defp verify_phoenix_dep do - unless Code.ensure_loaded?(Phoenix) do - raise "You tried to use Bamboo.Phoenix, but Phoenix module is not loaded. " <> - "Please add phoenix to your dependencies." - end - end - - @doc """ - Render a Phoenix template and set the body on the email. - - Pass an atom as the template name to render HTML *and* plain text emails, - e.g. `:welcome_email`. Use a string if you only want to render one type, e.g. - `"welcome_email.text"` or `"welcome_email.html"`. Scroll to the top for more examples. - """ - def render(_email, _template_name, _assigns) do - raise "function implemented for documentation only, please call: use Bamboo.Phoenix" - end - - @doc """ - Sets the layout when rendering HTML templates. - - ## Example - - def html_email_layout do - new_email - # Will use MyAppWeb.LayoutView with email.html template when rendering html emails - |> put_html_layout({MyAppWeb.LayoutView, "email.html"}) - end - """ - def put_html_layout(email, layout) do - email |> put_private(:html_layout, layout) - end - - @doc """ - Sets the layout when rendering plain text templates. - - ## Example - - def text_email_layout do - new_email - # Will use MyAppWeb.LayoutView with email.text template when rendering text emails - |> put_text_layout({MyAppWeb.LayoutView, "email.text"}) - end - """ - def put_text_layout(email, layout) do - email |> put_private(:text_layout, layout) - end - - @doc """ - Sets the layout for rendering plain text and HTML templates. - - ## Example - - def text_and_html_email_layout do - new_email - # Will use MyAppWeb.LayoutView with the email.html template for html emails - # and MyAppWeb.LayoutView with the email.text template for text emails - |> put_layout({MyAppWeb.LayoutView, :email}) - end - """ - def put_layout(email, {layout, template}) do - email - |> put_text_layout({layout, to_string(template) <> ".text"}) - |> put_html_layout({layout, to_string(template) <> ".html"}) - end - - @doc """ - Sets an assign for the email. These will be available when rendering the email - """ - def assign(%{assigns: assigns} = email, key, value) do - %{email | assigns: Map.put(assigns, key, value)} - end - - @doc false - def render_email(view, email, template, assigns) do - email - |> put_default_layouts - |> merge_assigns(assigns) - |> put_view(view) - |> put_template(template) - |> render - end - - defp put_default_layouts(%{private: private} = email) do - private = - private - |> Map.put_new(:html_layout, false) - |> Map.put_new(:text_layout, false) - - %{email | private: private} - end - - defp merge_assigns(%{assigns: email_assigns} = email, assigns) do - assigns = email_assigns |> Map.merge(Enum.into(assigns, %{})) - email |> Map.put(:assigns, assigns) - end - - defp put_view(email, view_module) do - email |> put_private(:view_module, view_module) - end - - defp put_template(email, view_template) do - email |> put_private(:view_template, view_template) - end - - defp render(%{private: %{view_template: template}} = email) when is_atom(template) do - render_html_and_text_emails(email) - end - - defp render(email) do - render_text_or_html_email(email) - end - - defp render_html_and_text_emails(email) do - view_template = Atom.to_string(email.private.view_template) - - email - |> Map.put(:html_body, render_html(email, view_template <> ".html")) - |> Map.put(:text_body, render_text(email, view_template <> ".text")) - end - - defp render_text_or_html_email(email) do - template = email.private.view_template - - cond do - String.ends_with?(template, ".html") -> - email |> Map.put(:html_body, render_html(email, template)) - - String.ends_with?(template, ".text") -> - email |> Map.put(:text_body, render_text(email, template)) - - true -> - raise ArgumentError, """ - Template name must end in either ".html" or ".text". Template name was #{ - inspect(template) - } - - If you would like to render both and html and text template, - use an atom without an extension instead. - """ - end - end - - defp render_html(email, template) do - # Phoenix uses the assigns.layout to determine what layout to use - assigns = Map.put(email.assigns, :layout, email.private.html_layout) - - Phoenix.View.render_to_string( - email.private.view_module, - template, - assigns - ) - end - - defp render_text(email, template) do - assigns = Map.put(email.assigns, :layout, email.private.text_layout) - - Phoenix.View.render_to_string( - email.private.view_module, - template, - assigns - ) - end -end diff --git a/mix.exs b/mix.exs index 8fa2ea27..5883931c 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,6 @@ defmodule Bamboo.Mixfile do elixir: "~> 1.6", source_url: @project_url, homepage_url: @project_url, - compilers: compilers(Mix.env()), test_coverage: [tool: ExCoveralls], preferred_cli_env: [coveralls: :test, "coveralls.circle": :test], elixirc_paths: elixirc_paths(Mix.env()), @@ -26,9 +25,6 @@ defmodule Bamboo.Mixfile do ] end - defp compilers(:test), do: [:phoenix] ++ Mix.compilers() - defp compilers(_), do: Mix.compilers() - # Configuration for the OTP application # # Type "mix help compile.app" for more information @@ -57,8 +53,6 @@ defmodule Bamboo.Mixfile do {:mime, "~> 1.4"}, {:ex_machina, "~> 2.4", only: :test}, {:cowboy, "~> 1.0", only: [:test, :dev]}, - {:phoenix, "~> 1.1", optional: true}, - {:phoenix_html, "~> 2.2", only: :test}, {:excoveralls, "~> 0.13", only: :test}, {:floki, "~> 0.29", only: :test}, {:ex_doc, "~> 0.23", only: :dev}, diff --git a/mix.lock b/mix.lock index c0c64ae1..65dd2dc4 100644 --- a/mix.lock +++ b/mix.lock @@ -20,8 +20,6 @@ "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm", "b960d1cbcf40a30963eeee90ab7aeae074cbfa9a238561fb4434add1afc3075c"}, "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, - "phoenix": {:hex, :phoenix, "1.3.3", "bafb5fa408d202e8d9f739e781bdb908446a2c1c1e00797c1158918ed55566a4", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "8bde28268d88de0c3141da0945ee2da93bd33473f94e19305dcf174a8dd42bac"}, - "phoenix_html": {:hex, :phoenix_html, "2.11.2", "86ebd768258ba60a27f5578bec83095bdb93485d646fc4111db8844c316602d6", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2fe8543036a9cb2103efe26c18874512dc0e005afba60dbfe90aa56e27c198a2"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm", "6f9193364c5de86b85e8d3a80294a134aecf6c5618adcbae668608749e00a7f7"}, "plug": {:hex, :plug, "1.6.0", "90d338a44c8cd762c32d3ea324f6728445c6145b51895403854b77f1536f1617", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "b6761cce9b0e369eb3d19da7f8805c1a55e0a7fdc2f65e2fc3802e9305481dea"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, diff --git a/test/lib/bamboo/phoenix_test.exs b/test/lib/bamboo/phoenix_test.exs deleted file mode 100644 index 9ed8688b..00000000 --- a/test/lib/bamboo/phoenix_test.exs +++ /dev/null @@ -1,121 +0,0 @@ -defmodule Bamboo.PhoenixTest do - use ExUnit.Case - - defmodule PhoenixLayoutView do - use Phoenix.View, root: "test/support/templates", namespace: Bamboo.PhoenixLayoutView - end - - defmodule EmailView do - use Phoenix.View, root: "test/support/templates", namespace: Bamboo.EmailView - - def function_in_view do - "function used in Bamboo.TemplateTest but needed because template is compiled" - end - end - - defmodule Email do - use Bamboo.Phoenix, view: EmailView - - def text_and_html_email_with_layout do - new_email() - |> put_layout({PhoenixLayoutView, :app}) - |> render(:text_and_html_email) - end - - def text_and_html_email do - new_email() - |> render(:text_and_html_email) - end - - def email_with_assigns(user) do - new_email() - |> render(:email_with_assigns, user: user) - end - - def email_with_already_assigned_user(user) do - new_email() - |> assign(:user, user) - |> render(:email_with_assigns) - end - - def html_email do - new_email() - |> render("html_email.html") - end - - def text_email do - new_email() - |> render("text_email.text") - end - - def no_template do - new_email() - |> render(:non_existent) - end - - def invalid_template do - new_email() - |> render("template.foobar") - end - end - - test "render/2 allows setting a custom layout" do - email = Email.text_and_html_email_with_layout() - - assert email.html_body =~ "HTML layout" - assert email.html_body =~ "HTML body" - assert email.text_body =~ "TEXT layout" - assert email.text_body =~ "TEXT body" - end - - test "render/2 renders html and text emails" do - email = Email.text_and_html_email() - - assert email.html_body =~ "HTML body" - assert email.text_body =~ "TEXT body" - end - - test "render/2 renders html and text emails with assigns" do - name = "Paul" - email = Email.email_with_assigns(%{name: name}) - assert email.html_body =~ "#{name}" - assert email.text_body =~ name - - name = "Paul" - email = Email.email_with_already_assigned_user(%{name: name}) - assert email.html_body =~ "#{name}" - assert email.text_body =~ name - end - - test "render/2 renders html body if template extension is .html" do - email = Email.html_email() - - assert email.html_body =~ "HTML body" - assert email.text_body == nil - end - - test "render/2 renders text body if template extension is .text" do - email = Email.text_email() - - assert email.html_body == nil - assert email.text_body =~ "TEXT body" - end - - test "render/2 raises if template doesn't exist" do - assert_raise Phoenix.Template.UndefinedError, fn -> - Email.no_template() - end - end - - test "render/2 raises if you pass an invalid template extension" do - assert_raise ArgumentError, ~r/must end in either ".html" or ".text"/, fn -> - Email.invalid_template() - end - end - - test "render raises if called directly" do - assert_raise RuntimeError, ~r/documentation only/, fn -> - Bamboo.Phoenix.render(:foo, :foo, :foo) - end - end -end diff --git a/test/support/templates/phoenix_layout/app.html.eex b/test/support/templates/phoenix_layout/app.html.eex deleted file mode 100644 index 977a17bf..00000000 --- a/test/support/templates/phoenix_layout/app.html.eex +++ /dev/null @@ -1,2 +0,0 @@ -HTML layout -<%= render @view_module, @view_template, assigns %> diff --git a/test/support/templates/phoenix_layout/app.text.eex b/test/support/templates/phoenix_layout/app.text.eex deleted file mode 100644 index faf2c62a..00000000 --- a/test/support/templates/phoenix_layout/app.text.eex +++ /dev/null @@ -1,2 +0,0 @@ -TEXT layout -<%= render @view_module, @view_template, assigns %>