From 98824e89cb7e5651123903c781d4b4ace8937636 Mon Sep 17 00:00:00 2001 From: ddink Date: Wed, 21 Aug 2024 21:05:48 -0500 Subject: [PATCH 1/4] add shared handle_info callbacks --- lib/beacon/boot.ex | 2 + lib/beacon/content.ex | 181 ++++++++++++++++++ lib/beacon/content/info_handler.ex | 46 +++++ lib/beacon/loader.ex | 10 + lib/beacon/loader/page.ex | 20 +- lib/beacon/loader/worker.ex | 4 + lib/beacon/migrations/v001.ex | 10 + .../web/components/layouts/app.html.heex | 11 -- .../web/components/layouts/dynamic.html.heex | 1 + lib/beacon/web/live/page_live.ex | 21 +- mix.exs | 5 +- mix.lock | 1 + test/beacon/content_test.exs | 140 ++++++++++++++ test/beacon/lifecycle/template_test.exs | 3 +- test/beacon_web/live/page_live_test.exs | 24 +++ test/support/fixtures.ex | 26 +++ 16 files changed, 485 insertions(+), 20 deletions(-) create mode 100644 lib/beacon/content/info_handler.ex diff --git a/lib/beacon/boot.ex b/lib/beacon/boot.ex index c85cb656d..8deb4346a 100644 --- a/lib/beacon/boot.ex +++ b/lib/beacon/boot.ex @@ -44,6 +44,8 @@ defmodule Beacon.Boot do Beacon.Loader.populate_default_error_pages(config.site) Beacon.Loader.populate_default_home_page(config.site) + Beacon.Loader.reload_info_handlers(config.site) + assets = [ Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_runtime_js(config.site) end), Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_runtime_css(config.site) end) diff --git a/lib/beacon/content.ex b/lib/beacon/content.ex index 652d5f216..2e77b4644 100644 --- a/lib/beacon/content.ex +++ b/lib/beacon/content.ex @@ -35,6 +35,7 @@ defmodule Beacon.Content do alias Beacon.Content.ComponentSlot alias Beacon.Content.ComponentSlotAttr alias Beacon.Content.ErrorPage + alias Beacon.Content.InfoHandler alias Beacon.Content.Layout alias Beacon.Content.LayoutEvent alias Beacon.Content.LayoutSnapshot @@ -3931,6 +3932,186 @@ defmodule Beacon.Content do repo(site).delete(live_data_assign) end + @doc """ + Creates a new info handler for creating shared handle_info callbacks. + + ## Example + + iex> create_info_handler(%{site: "my_site", msg: "{:new_msg, arg}", code: "{:noreply, socket}"}) + {:ok, %InfoHandler{}} + + """ + @doc type: :info_handlers + @spec create_info_handler(map()) :: {:ok, InfoHandler.t()} | {:error, Changeset.t()} + def create_info_handler(attrs) do + variable_names = + attrs + |> ExUtils.Map.atomize_keys() + |> Map.get(:msg) + |> list_variable_names() + + changeset = + %InfoHandler{} + |> InfoHandler.changeset(attrs) + |> validate_info_handler(variable_names) + + site = Changeset.get_field(changeset, :site) + + changeset + |> repo(site).insert() + |> tap(&maybe_broadcast_updated_content_event(&1, :info_handler)) + end + + @spec validate_info_handler(Changeset.t(), [String.t()], [String.t()]) :: Changeset.t() + defp validate_info_handler(changeset, variable_names, imports \\ []) do + code = Changeset.get_field(changeset, :code) + site = Changeset.get_field(changeset, :site) + metadata = %Beacon.Template.LoadMetadata{site: site} + var = ["socket"] ++ variable_names + imports = ["Phoenix.LiveView"] ++ imports + + do_validate_template(changeset, :code, :elixir, code, metadata, var, imports) + end + + @spec list_variable_names(String.t()) :: [String.t()] + defp list_variable_names(msg) do + {_msg, variables} = + msg + |> String.replace("{", "") + |> String.replace("}", "") + |> String.split(", ") + |> List.pop_at(0) + + variables + end + + @doc """ + Creates a info handler, raising an error if unsuccessful. + + Returns the new info handler if successful, otherwise raises a `RuntimeError`. + + ## Example + + iex> create_info_handler!(%{site: "my_site", msg: "{:new_msg, arg}", code: "{:noreply, socket}"}) + %InfoHandler{} + """ + @doc type: :info_handlers + @spec create_info_handler!(map()) :: InfoHandler.t() + def create_info_handler!(attrs \\ %{}) do + case create_info_handler(attrs) do + {:ok, info_handler} -> info_handler + {:error, changeset} -> raise "failed to create info handler, got: #{inspect(changeset.errors)}" + end + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking info handler changes. + + ## Example + + iex> change_info_handler(info_handler, %{code: {:noreply, socket}}) + %Ecto.Changeset{data: %InfoHandler{}} + + """ + @doc type: :info_handlers + @spec change_info_handler(InfoHandler.t(), map()) :: Changeset.t() + def change_info_handler(%InfoHandler{} = info_handler, attrs \\ %{}) do + InfoHandler.changeset(info_handler, attrs) + end + + @doc """ + Gets a single info handler by `id`. + + ## Example + + iex> get_single_info_handler(:my_site, "fefebbfe-f732-4119-9116-d031d04f5a2c") + %InfoHandler{} + + """ + @doc type: :info_handlers + @spec get_info_handler(Site.t(), UUID.t()) :: InfoHandler.t() | nil + def get_info_handler(site, id) when is_atom(site) and is_binary(id) do + repo(site).get(InfoHandler, id) + end + + @doc """ + Same as `get_info_handler/2` but raises an error if no result is found. + """ + @doc type: :info_handlers + @spec get_info_handler!(Site.t(), UUID.t()) :: InfoHandler.t() + def get_info_handler!(site, id) when is_atom(site) and is_binary(id) do + repo(site).get!(InfoHandler, id) + end + + @doc """ + Lists all info handlers for a given site. + + ## Example + + iex> list_info_handlers() + + """ + @doc type: :info_handlers + @spec list_info_handlers(Site.t()) :: [InfoHandler.t()] + def list_info_handlers(site) do + repo(site).all( + from h in InfoHandler, + where: h.site == ^site, + order_by: [asc: h.inserted_at] + ) + end + + @doc """ + Updates a info handler. + + ## Example + + iex> update_info_handler(info_handler, %{msg: "{:new_msg, arg}"}) + {:ok, %InfoHandler{}} + + """ + @doc type: :info_handlers + @spec update_info_handler(InfoHandler.t(), map()) :: {:ok, InfoHandler.t()} + def update_info_handler(%InfoHandler{} = info_handler, attrs) do + variable_names = + info_handler + |> retrieve_msg(attrs) + |> list_variable_names() + + changeset = + info_handler + |> InfoHandler.changeset(attrs) + |> validate_info_handler(variable_names, ["Phoenix.Component"]) + + site = Changeset.get_field(changeset, :site) + + changeset + |> repo(site).update() + |> tap(&maybe_broadcast_updated_content_event(&1, :info_handler)) + end + + @spec retrieve_msg(InfoHandler.t(), map()) :: String.t() + defp retrieve_msg(info_handler, attrs) do + attrs = ExUtils.Map.atomize_keys(attrs) + + case Map.get(attrs, :msg) do + nil -> + info_handler.msg + + msg -> + msg + end + end + + @doc """ + Deletes info handler. + """ + @doc type: :info_handlers + @spec delete_info_handler(InfoHandler.t()) :: {:ok, InfoHandler.t()} | {:error, Changeset.t()} + def delete_info_handler(info_handler) do + repo(info_handler.site).delete(info_handler) + end + ## Utils defp do_validate_template(changeset, field, format, template, metadata, vars \\ [], imports \\ []) diff --git a/lib/beacon/content/info_handler.ex b/lib/beacon/content/info_handler.ex new file mode 100644 index 000000000..2627c4246 --- /dev/null +++ b/lib/beacon/content/info_handler.ex @@ -0,0 +1,46 @@ +defmodule Beacon.Content.InfoHandler do + @moduledoc """ + Beacon's representation of a LiveView [handle_info/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_info/2) + that applies to all of a site's pages. + + This is the Elixir code which will handle messages from other Elixir processes. + + > #### Do not create or edit info handlers manually {: .warning} + > + > Use the public functions in `Beacon.Content` instead. + > The functions in that module guarantee that all dependencies + > are created correctly and all processes are updated. + > Manipulating data manually will most likely result + > in inconsistent behavior and crashes. + """ + + use Beacon.Schema + + import Ecto.Changeset + + @type t :: %__MODULE__{ + id: Ecto.UUID.t(), + site: Beacon.Types.Site.t(), + msg: binary(), + code: binary(), + inserted_at: DateTime.t(), + updated_at: DateTime.t() + } + + schema "beacon_info_handlers" do + field :site, Beacon.Types.Site + field :msg, :string + field :code, :string + + timestamps() + end + + @doc false + def changeset(%__MODULE__{} = info_handler, attrs) do + fields = ~w(site msg code)a + + info_handler + |> cast(attrs, fields) + |> validate_required(fields) + end +end diff --git a/lib/beacon/loader.ex b/lib/beacon/loader.ex index 846668ef9..f4295b4bb 100644 --- a/lib/beacon/loader.ex +++ b/lib/beacon/loader.ex @@ -210,6 +210,10 @@ defmodule Beacon.Loader do GenServer.call(worker(site), {:unload_page_module, page_id}, @timeout) end + def reload_info_handlers(site) do + GenServer.call(worker(site), :reload_info_handlers, @timeout) + end + defp maybe_reload(module, reload_fun) do if :erlang.module_loaded(module) do {:ok, module} @@ -288,6 +292,12 @@ defmodule Beacon.Loader do {:noreply, config} end + def handle_info({:content_updated, :info_handler, %{site: site}}, config) do + reload_info_handlers(site) + reload_runtime_css(site) + {:noreply, config} + end + def handle_info(msg, config) do raise inspect(msg) Logger.warning("Beacon.Loader can not handle the message: #{inspect(msg)}") diff --git a/lib/beacon/loader/page.ex b/lib/beacon/loader/page.ex index e8262cdf3..46a913c8a 100644 --- a/lib/beacon/loader/page.ex +++ b/lib/beacon/loader/page.ex @@ -15,7 +15,7 @@ defmodule Beacon.Loader.Page do # Group function heads together to avoid compiler warnings functions = [ - for fun <- [&page/1, &page_assigns/1, &handle_event/1, &helper/1] do + for fun <- [&page/1, &page_assigns/1, &handle_event/1, &handle_info/1, &helper/1] do fun.(page) end, render(page), @@ -36,9 +36,11 @@ defmodule Beacon.Loader.Page do import PhoenixHTMLHelpers.Link import PhoenixHTMLHelpers.Tag import PhoenixHTMLHelpers.Format + import Phoenix.LiveView import Phoenix.Component, except: [assign: 2, assign: 3, assign_new: 3] import Beacon.Web, only: [assign: 2, assign: 3, assign_new: 3] import Beacon.Router, only: [beacon_asset_path: 2, beacon_asset_url: 2] + import Beacon.Web.Gettext import unquote(routes_module) import unquote(components_module) @@ -137,6 +139,22 @@ defmodule Beacon.Loader.Page do end) end + defp handle_info(page) do + %{site: site} = page + + info_handlers = Beacon.Content.list_info_handlers(site) + + Enum.map(info_handlers, fn info_handler -> + Beacon.safe_code_check!(site, info_handler.code) + + quote do + def handle_info(unquote(Code.string_to_quoted!(info_handler.msg)), var!(socket)) do + unquote(Code.string_to_quoted!(info_handler.code)) + end + end + end) + end + # TODO: validate fn name and args def helper(%{site: site, helpers: helpers}) do Enum.map(helpers, fn helper -> diff --git a/lib/beacon/loader/worker.ex b/lib/beacon/loader/worker.ex index fcb674669..d24cbe13c 100644 --- a/lib/beacon/loader/worker.ex +++ b/lib/beacon/loader/worker.ex @@ -436,6 +436,10 @@ defmodule Beacon.Loader.Worker do end end + def handle_call(:reload_info_handlers, _from, config) do + stop(Beacon.Content.list_info_handlers(config.site), config) + end + def handle_info(msg, config) do Logger.warning("Beacon.Loader.Worker can not handle the message: #{inspect(msg)}") {:noreply, config} diff --git a/lib/beacon/migrations/v001.ex b/lib/beacon/migrations/v001.ex index 56fa3931d..04b098f16 100644 --- a/lib/beacon/migrations/v001.ex +++ b/lib/beacon/migrations/v001.ex @@ -55,6 +55,15 @@ defmodule Beacon.Migrations.V001 do create_if_not_exists index(:beacon_live_data_assigns, [:live_data_id]) + create_if_not_exists table(:beacon_info_handlers, primary_key: false) do + add :id, :binary_id, primary_key: true + add :site, :text, null: false + add :msg, :text, null: false + add :code, :text, null: false + + timestamps(type: :utc_datetime_usec) + end + create_if_not_exists table(:beacon_snippet_helpers, primary_key: false) do add :id, :binary_id, primary_key: true add :site, :text, null: false @@ -250,6 +259,7 @@ defmodule Beacon.Migrations.V001 do drop_if_exists table(:beacon_assets) drop_if_exists table(:beacon_stylesheets) drop_if_exists table(:beacon_live_data_assigns) + drop_if_exists table(:beacon_info_handlers) drop_if_exists table(:beacon_live_data) drop_if_exists table(:beacon_snippet_helpers) drop_if_exists table(:beacon_component_attrs) diff --git a/lib/beacon/web/components/layouts/app.html.heex b/lib/beacon/web/components/layouts/app.html.heex index bcfe1599d..81ac27d9e 100644 --- a/lib/beacon/web/components/layouts/app.html.heex +++ b/lib/beacon/web/components/layouts/app.html.heex @@ -1,16 +1,5 @@
- - - - Attempting to reconnect - <%= @inner_content %>
diff --git a/lib/beacon/web/components/layouts/dynamic.html.heex b/lib/beacon/web/components/layouts/dynamic.html.heex index 30a1d5885..e486bf020 100644 --- a/lib/beacon/web/components/layouts/dynamic.html.heex +++ b/lib/beacon/web/components/layouts/dynamic.html.heex @@ -1 +1,2 @@ + <%= render_dynamic_layout(assigns) %> diff --git a/lib/beacon/web/live/page_live.ex b/lib/beacon/web/live/page_live.ex index 242dc242e..b789aae57 100644 --- a/lib/beacon/web/live/page_live.ex +++ b/lib/beacon/web/live/page_live.ex @@ -53,14 +53,25 @@ defmodule Beacon.Web.PageLive do end def handle_info(msg, socket) do - Logger.warning(""" - unhandled message: + %{beacon: %{private: %{page_module: page_module, live_path: live_path}}} = socket.assigns + %{site: site, id: page_id} = Beacon.apply_mfa(page_module, :page_assigns, [[:site, :id]]) - #{inspect(msg)} + result = + Beacon.apply_mfa( + page_module, + :handle_info, + [msg, socket], + context: %{site: site, page_id: page_id, live_path: live_path} + ) - """) + case result do + {:noreply, %Phoenix.LiveView.Socket{} = socket} -> + {:noreply, socket} - {:noreply, socket} + other -> + raise Beacon.Web.ServerError, + "handle_info for #{live_path} expected return of {:noreply, %Phoenix.LiveView.Socket{}}, but got #{inspect(other)}" + end end def handle_event(event_name, event_params, socket) do diff --git a/mix.exs b/mix.exs index 1033cbb7e..7c8946185 100644 --- a/mix.exs +++ b/mix.exs @@ -78,6 +78,7 @@ defmodule Beacon.MixProject do {:safe_code, "~> 0.2"}, {:solid, "~> 0.14"}, {:tailwind, "~> 0.2"}, + {:ex_utils, "~> 0.1.7"}, # Dev, Test, Docs {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, @@ -131,7 +132,8 @@ defmodule Beacon.MixProject do "Functions: Snippets": &(&1[:type] == :snippets), "Functions: Page Event Handlers": &(&1[:type] == :page_event_handlers), "Functions: Error Pages": &(&1[:type] == :error_pages), - "Functions: Live Data": &(&1[:type] == :live_data) + "Functions: Live Data": &(&1[:type] == :live_data), + "Functions: Info Handlers": &(&1[:type] == :info_handlers) ], skip_undefined_reference_warnings_on: ["CHANGELOG.md"] ] @@ -164,6 +166,7 @@ defmodule Beacon.MixProject do Beacon.Content.ComponentSlot, Beacon.Content.ComponentSlotAttr, Beacon.Content.ErrorPage, + Beacon.Content.InfoHandler, Beacon.Content.Layout, Beacon.Content.LayoutEvent, Beacon.Content.LayoutSnapshot, diff --git a/mix.lock b/mix.lock index 84b6a4992..562f47a3d 100644 --- a/mix.lock +++ b/mix.lock @@ -20,6 +20,7 @@ "ex_aws_s3": {:hex, :ex_aws_s3, "2.4.0", "ce8decb6b523381812798396bc0e3aaa62282e1b40520125d1f4eff4abdff0f4", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "85dda6e27754d94582869d39cba3241d9ea60b6aa4167f9c88e309dc687e56bb"}, "ex_brotli": {:hex, :ex_brotli, "0.4.1", "b9d43d1ba0641b460b8477b1dc20f224827651c5af5977419b92c4e63c27d619", [:mix], [{:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "840eeace8354abfa470fa094b8b0231ef184f5692cd4b219143c403bcf8f0756"}, "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm", "66d4fe75285948f2d1e69c2a5ddd651c398c813574f8d36a9eef11dc20356ef6"}, "exconstructor": {:hex, :exconstructor, "1.2.13", "7021eed1450202dcbcd1ef021d6aacf7351854ff9d7964f166931567f9dfa9fb", [:mix], [], "hexpm", "69d3f0251a07bb7c5ef85bde22a1eee577dfbb49852d77fb7ad7b937035aeef2"}, "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, diff --git a/test/beacon/content_test.exs b/test/beacon/content_test.exs index 49b1456ae..6e65aa2b0 100644 --- a/test/beacon/content_test.exs +++ b/test/beacon/content_test.exs @@ -6,6 +6,7 @@ defmodule Beacon.ContentTest do alias Beacon.Content alias Beacon.Content.Component alias Beacon.Content.ErrorPage + alias Beacon.Content.InfoHandler alias Beacon.Content.Layout alias Beacon.Content.LayoutEvent alias Beacon.Content.LayoutSnapshot @@ -1010,4 +1011,143 @@ defmodule Beacon.ContentTest do assert %{assigns: []} = Repo.preload(live_data, :assigns) end end + + describe "info_handlers" do + setup do + code = ~S""" + socket = + socket + |> redirect(to: "/home") + |> put_flash( + :error, + "Your email (#{email_address}) is incorrectly formatted. Please format it correctly." + ) + + {:noreply, socket} + """ + + msg = "{:incorrect_format, email_address}" + + %{msg: msg, code: code} + end + + test "success: create_info_handler/1", %{msg: msg, code: code} do + attrs = %{site: :my_site, msg: msg, code: code} + + assert {:ok, %InfoHandler{} = info_handler} = Content.create_info_handler(attrs) + assert %InfoHandler{site: :my_site, msg: ^msg, code: ^code} = info_handler + end + + test "error: create_info_handler/1 validates code", %{msg: msg} do + assert {:error, %{errors: [error]}} = + Content.create_info_handler(%{ + site: :my_site, + msg: msg, + code: ":no_reply, socket" + }) + + {:code, {"invalid", [compilation_error: compilation_error]}} = error + + assert compilation_error =~ "unexpectedly reached end of line" + + refute Repo.one(InfoHandler) + end + + test "success: create_info_handler!/1", %{msg: msg, code: code} do + Content.create_info_handler!(%{ + site: :my_site, + msg: msg, + code: code + }) + + assert %InfoHandler{site: :my_site, msg: ^msg, code: ^code} = Repo.one(InfoHandler) + end + + test "success: get_info_handler/2", %{msg: msg, code: code} do + info_handler = info_handler_fixture(%{msg: msg, code: code}) + site = info_handler.site + handler_from_db = Content.get_info_handler(site, info_handler.id) + + assert %InfoHandler{site: ^site, msg: ^msg, code: ^code} = handler_from_db + end + + test "success: get_info_handler!/2", %{msg: msg, code: code} do + info_handler = info_handler_fixture(%{msg: msg, code: code}) + site = info_handler.site + handler_from_db = Content.get_info_handler!(site, info_handler.id) + + assert %InfoHandler{site: ^site, msg: ^msg, code: ^code} = handler_from_db + end + + test "success: list_info_handlers/1" do + info_handlers = for _ <- 1..3, do: info_handler_fixture() + + result = Content.list_info_handlers(:my_site) + + assert Enum.sort(info_handlers) == Enum.sort(result) + end + + test "success: update_info_handler/2" do + code = ~S""" + socket = + socket + |> assign(email: nil) + |> put_flash( + :error, + "There was an error." + ) + + {:noreply, socket} + """ + + msg = "{:email_address_error}" + + info_handler = info_handler_fixture() + attrs = %{code: code, msg: msg} + + refute info_handler.code == code + refute info_handler.msg == msg + + assert {:ok, %InfoHandler{} = info_handler_from_db} = Content.update_info_handler(info_handler, attrs) + assert %InfoHandler{code: ^code, msg: ^msg} = info_handler_from_db + end + + test "error: update_info_handler/2" do + code = ~S""" + socket = + socket + |> assign(email: nil) + |> put_flash( + :error, + "There was an error." + ) + + :noreply, socket + """ + + msg = "{:email_address_error}" + + info_handler = info_handler_fixture() + attrs = %{code: code, msg: msg} + + refute info_handler.code == code + refute info_handler.msg == msg + + {:error, %{errors: [error]}} = Content.update_info_handler(info_handler, attrs) + + {:code, {"invalid", [compilation_error: compilation_error]}} = error + + assert compilation_error =~ "unexpectedly reached end of line" + end + + test "success: delete_info_handler/1", %{msg: msg, code: code} do + info_handler = info_handler_fixture(%{msg: msg, code: code}) + site = info_handler.site + + assert Repo.one(InfoHandler) + + assert {:ok, %InfoHandler{site: ^site, msg: ^msg, code: ^code}} = Content.delete_info_handler(info_handler) + refute Repo.one(InfoHandler) + end + end end diff --git a/test/beacon/lifecycle/template_test.exs b/test/beacon/lifecycle/template_test.exs index b41fd0bd5..0799a7d89 100644 --- a/test/beacon/lifecycle/template_test.exs +++ b/test/beacon/lifecycle/template_test.exs @@ -20,7 +20,6 @@ defmodule Beacon.Lifecycle.TemplateTest do Beacon.Loader.reload_page_module(page.site, page.id) env = Beacon.Web.PageLive.make_env(:my_site) - assert %Phoenix.LiveView.Rendered{static: ["
\n

my_site#home

\n
"]} = - Lifecycle.Template.render_template(page, %{}, env) + assert %Phoenix.LiveView.Rendered{static: ["
\n

my_site#home

\n
"]} = Lifecycle.Template.render_template(page, %{}, env) end end diff --git a/test/beacon_web/live/page_live_test.exs b/test/beacon_web/live/page_live_test.exs index 4ae7bd59b..531d530ed 100644 --- a/test/beacon_web/live/page_live_test.exs +++ b/test/beacon_web/live/page_live_test.exs @@ -90,6 +90,18 @@ defmodule Beacon.Web.Live.PageLiveTest do """ }) + info_handler_fixture(%{ + site: :my_site, + msg: "{:incorrect_format, email}", + code: """ + socket = + socket + |> put_flash(:error, "Your email (\#{email}) is incorrectly formatted. Please format it correctly.") + + {:noreply, socket} + """ + }) + Content.publish_page(page_home) _page_without_meta_tags = @@ -260,6 +272,18 @@ defmodule Beacon.Web.Live.PageLiveTest do |> render_submit() =~ "Hello Beacon" end + test "info handler", %{conn: conn} do + {:ok, view, _html} = live(conn, "/home/hello") + + email = "email@email" + + refute render(view) =~ "Your email (#{email}) is incorrectly formatted. Please format it correctly." + + send(view.pid, {:incorrect_format, email}) + + assert render(view) =~ "Your email (#{email}) is incorrectly formatted. Please format it correctly." + end + test "helper", %{conn: conn} do {:ok, _view, html} = live(conn, "/home/hello") diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 06926146b..8f079c362 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -3,6 +3,7 @@ defmodule Beacon.Fixtures do alias Beacon.Content alias Beacon.Content.ErrorPage + alias Beacon.Content.InfoHandler alias Beacon.Content.PageEventHandler alias Beacon.Content.PageVariant alias Beacon.MediaLibrary @@ -234,4 +235,29 @@ defmodule Beacon.Fixtures do live_data end + + def info_handler_fixture(attrs \\ %{}) do + code = ~S""" + socket = + socket + |> put_flash( + :info, + "We just sent an email to your address (#{email})!" + ) + + {:noreply, socket} + """ + + msg = "{:email_sent, email}" + + full_attrs = %{ + site: attrs[:site] || :my_site, + msg: attrs[:msg] || msg, + code: attrs[:code] || code + } + + %InfoHandler{} + |> InfoHandler.changeset(full_attrs) + |> repo(full_attrs.site).insert!() + end end From 92a4a385545d43bca52f67dace7b04a2d74b52d5 Mon Sep 17 00:00:00 2001 From: ddink Date: Wed, 28 Aug 2024 15:34:04 -0500 Subject: [PATCH 2/4] adds info_handlers loader --- lib/beacon/boot.ex | 5 ++-- lib/beacon/loader.ex | 11 +++++--- lib/beacon/loader/info_handlers.ex | 32 ++++++++++++++++++++++ lib/beacon/loader/page.ex | 7 ++--- lib/beacon/loader/worker.ex | 8 ++++-- lib/beacon/web/beacon_assigns.ex | 3 +++ lib/beacon/web/live/page_live.ex | 4 +-- lib/errors.ex | 4 +-- test/beacon_web/live/page_live_test.exs | 1 + test/test_helper.exs | 35 +++++++++++-------------- 10 files changed, 74 insertions(+), 36 deletions(-) create mode 100644 lib/beacon/loader/info_handlers.ex diff --git a/lib/beacon/boot.ex b/lib/beacon/boot.ex index 8deb4346a..76f65945e 100644 --- a/lib/beacon/boot.ex +++ b/lib/beacon/boot.ex @@ -44,8 +44,6 @@ defmodule Beacon.Boot do Beacon.Loader.populate_default_error_pages(config.site) Beacon.Loader.populate_default_home_page(config.site) - Beacon.Loader.reload_info_handlers(config.site) - assets = [ Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_runtime_js(config.site) end), Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_runtime_css(config.site) end) @@ -57,7 +55,8 @@ defmodule Beacon.Boot do Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_live_data_module(config.site) end), Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_layouts_modules(config.site) end), Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_error_page_module(config.site) end), - Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_pages_modules(config.site, per_page: 20) end) + Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_pages_modules(config.site, per_page: 20) end), + Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_info_handlers_module(config.site) end) # TODO: load main pages (order_by: path, per_page: 10) to avoid SEO issues ] diff --git a/lib/beacon/loader.ex b/lib/beacon/loader.ex index f4295b4bb..409bcab19 100644 --- a/lib/beacon/loader.ex +++ b/lib/beacon/loader.ex @@ -161,6 +161,10 @@ defmodule Beacon.Loader do Loader.Page.module_name(site, page_id) end + def fetch_info_handlers_module(site) do + Loader.InfoHandlers.module_name(site) + end + def maybe_reload_page_module(site, page_id) do maybe_reload(Loader.Page.module_name(site, page_id), fn -> reload_page_module(site, page_id) end) end @@ -210,8 +214,8 @@ defmodule Beacon.Loader do GenServer.call(worker(site), {:unload_page_module, page_id}, @timeout) end - def reload_info_handlers(site) do - GenServer.call(worker(site), :reload_info_handlers, @timeout) + def reload_info_handlers_module(site) do + GenServer.call(worker(site), :reload_info_handlers_module, @timeout) end defp maybe_reload(module, reload_fun) do @@ -293,8 +297,7 @@ defmodule Beacon.Loader do end def handle_info({:content_updated, :info_handler, %{site: site}}, config) do - reload_info_handlers(site) - reload_runtime_css(site) + reload_info_handlers_module(site) {:noreply, config} end diff --git a/lib/beacon/loader/info_handlers.ex b/lib/beacon/loader/info_handlers.ex new file mode 100644 index 000000000..6e6a23ce5 --- /dev/null +++ b/lib/beacon/loader/info_handlers.ex @@ -0,0 +1,32 @@ +defmodule Beacon.Loader.InfoHandlers do + @moduledoc false + alias Beacon.Loader + + def module_name(site), do: Loader.module_name(site, "InfoHandlers") + + def build_ast(site, info_handlers) do + module = module_name(site) + functions = Enum.map(info_handlers, &build_fn/1) + + quote do + defmodule unquote(module) do + import Phoenix.LiveView + import Phoenix.Component, except: [assign: 2, assign: 3, assign_new: 3] + import Beacon.Web, only: [assign: 2, assign: 3, assign_new: 3] + + (unquote_splicing(functions)) + end + end + end + + defp build_fn(info_handler) do + %{site: site, msg: msg, code: code} = info_handler + Beacon.safe_code_check!(site, code) + + quote do + def handle_info(unquote(Code.string_to_quoted!(msg)), var!(socket)) do + unquote(Code.string_to_quoted!(code)) + end + end + end +end diff --git a/lib/beacon/loader/page.ex b/lib/beacon/loader/page.ex index 46a913c8a..d3fcbef37 100644 --- a/lib/beacon/loader/page.ex +++ b/lib/beacon/loader/page.ex @@ -1,11 +1,12 @@ defmodule Beacon.Loader.Page do @moduledoc false - - require Logger + alias Beacon.Content alias Beacon.Lifecycle alias Beacon.Loader alias Beacon.Template.HEEx + require Logger + def module_name(site, page_id), do: Loader.module_name(site, "Page#{page_id}") def build_ast(site, page) do @@ -142,7 +143,7 @@ defmodule Beacon.Loader.Page do defp handle_info(page) do %{site: site} = page - info_handlers = Beacon.Content.list_info_handlers(site) + info_handlers = Content.list_info_handlers(site) Enum.map(info_handlers, fn info_handler -> Beacon.safe_code_check!(site, info_handler.code) diff --git a/lib/beacon/loader/worker.ex b/lib/beacon/loader/worker.ex index d24cbe13c..e8f85276f 100644 --- a/lib/beacon/loader/worker.ex +++ b/lib/beacon/loader/worker.ex @@ -436,8 +436,12 @@ defmodule Beacon.Loader.Worker do end end - def handle_call(:reload_info_handlers, _from, config) do - stop(Beacon.Content.list_info_handlers(config.site), config) + def handle_call(:reload_info_handlers_module, _from, config) do + %{site: site} = config + info_handlers = Content.list_info_handlers(site) + ast = Loader.InfoHandlers.build_ast(site, info_handlers) + result = compile_module(site, ast, "info_handlers") + stop(result, config) end def handle_info(msg, config) do diff --git a/lib/beacon/web/beacon_assigns.ex b/lib/beacon/web/beacon_assigns.ex index 11eb99c64..7c721617b 100644 --- a/lib/beacon/web/beacon_assigns.ex +++ b/lib/beacon/web/beacon_assigns.ex @@ -39,6 +39,7 @@ defmodule Beacon.Web.BeaconAssigns do private: %{ page_module: nil, components_module: nil, + info_handlers_module: nil, live_data_keys: [], live_path: [] } @@ -65,6 +66,7 @@ defmodule Beacon.Web.BeaconAssigns do path_params = Beacon.Router.path_params(page.path, path_info) page_title = Beacon.Web.DataSource.page_title(site, page.id, live_data) components_module = Beacon.Loader.Components.module_name(site) + info_handlers_module = Beacon.Loader.InfoHandlers.module_name(site) %__MODULE__{ site: page.site, @@ -74,6 +76,7 @@ defmodule Beacon.Web.BeaconAssigns do private: %{ page_module: page_module, components_module: components_module, + info_handlers_module: info_handlers_module, live_data_keys: Map.keys(live_data), live_path: path_info } diff --git a/lib/beacon/web/live/page_live.ex b/lib/beacon/web/live/page_live.ex index b789aae57..4d46f01f6 100644 --- a/lib/beacon/web/live/page_live.ex +++ b/lib/beacon/web/live/page_live.ex @@ -53,12 +53,12 @@ defmodule Beacon.Web.PageLive do end def handle_info(msg, socket) do - %{beacon: %{private: %{page_module: page_module, live_path: live_path}}} = socket.assigns + %{page_module: page_module, live_path: live_path, info_handlers_module: info_handlers_module} = socket.assigns.beacon.private %{site: site, id: page_id} = Beacon.apply_mfa(page_module, :page_assigns, [[:site, :id]]) result = Beacon.apply_mfa( - page_module, + info_handlers_module, :handle_info, [msg, socket], context: %{site: site, page_id: page_id, live_path: live_path} diff --git a/lib/errors.ex b/lib/errors.ex index 5ba00cde6..c5f8ad243 100644 --- a/lib/errors.ex +++ b/lib/errors.ex @@ -83,9 +83,9 @@ end defmodule Beacon.Web.ServerError do @moduledoc """ - Raised when a `Beacon.Content.PageEventHandler` returns an invalid response. + Raised when a `Beacon.Content.EventHandler` or a `Beacon.Content.InfoHandler` returns an invalid response. - If you're seeing this error, check the code in your site's event handlers, and + If you're seeing this error, check the code in your site's event handlers or info handlers, and ensure that each one returns `{:noreply, socket}`. """ diff --git a/test/beacon_web/live/page_live_test.exs b/test/beacon_web/live/page_live_test.exs index 531d530ed..48b7922ef 100644 --- a/test/beacon_web/live/page_live_test.exs +++ b/test/beacon_web/live/page_live_test.exs @@ -120,6 +120,7 @@ defmodule Beacon.Web.Live.PageLiveTest do Loader.reload_components_module(:my_site) Loader.reload_layouts_modules(:my_site) Loader.reload_pages_modules(:my_site) + Loader.reload_info_handlers_module(:my_site) [layout: layout] end diff --git a/test/test_helper.exs b/test/test_helper.exs index 95cc72b86..ddcea2434 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -159,28 +159,23 @@ Supervisor.start_link( ) # TODO: better control :booted default data when we introduce Beacon.Test functions -Beacon.BeaconTest.Repo.delete_all(Beacon.Content.Component) -Beacon.BeaconTest.Repo.delete_all(Beacon.Content.ErrorPage) -Beacon.BeaconTest.Repo.delete_all(Beacon.Content.Page) -Beacon.BeaconTest.Repo.delete_all(Beacon.Content.Layout) +Enum.each( + [ + Beacon.Content.Component, + Beacon.Content.ErrorPage, + Beacon.Content.Page, + Beacon.Content.Layout, + Beacon.Content.InfoHandler + ], + &Beacon.BeaconTest.Repo.delete_all/1 +) # TODO: add hooks into Beacon.Testing to reload these shared/global modules -Beacon.Loader.reload_routes_module(:my_site) -Beacon.Loader.reload_routes_module(:not_booted) -Beacon.Loader.reload_routes_module(:booted) -Beacon.Loader.reload_routes_module(:s3_site) -Beacon.Loader.reload_routes_module(:data_source_test) -Beacon.Loader.reload_routes_module(:default_meta_tags_test) -Beacon.Loader.reload_routes_module(:lifecycle_test) -Beacon.Loader.reload_routes_module(:lifecycle_test_fail) -Beacon.Loader.reload_components_module(:my_site) -Beacon.Loader.reload_components_module(:not_booted) -Beacon.Loader.reload_components_module(:booted) -Beacon.Loader.reload_components_module(:s3_site) -Beacon.Loader.reload_components_module(:data_source_test) -Beacon.Loader.reload_components_module(:default_meta_tags_test) -Beacon.Loader.reload_components_module(:lifecycle_test) -Beacon.Loader.reload_components_module(:lifecycle_test_fail) +for site <- [:my_site, :not_booted, :s3_site, :data_source_test, :default_meta_tags_test, :lifecycle_test, :lifecycle_test_fail] do + Beacon.Loader.reload_routes_module(site) + Beacon.Loader.reload_components_module(site) + Beacon.Loader.reload_info_handlers_module(site) +end ExUnit.start(exclude: [:skip]) Ecto.Adapters.SQL.Sandbox.mode(Beacon.BeaconTest.Repo, :manual) From 1fc2396b5aca70657095621163dfd759cb29994f Mon Sep 17 00:00:00 2001 From: ddink Date: Tue, 10 Sep 2024 14:11:14 -0500 Subject: [PATCH 3/4] add shared handle_info callbacks PR feedback --- lib/beacon/content.ex | 59 ++++++++++++++--------------------- lib/beacon/loader/page.ex | 2 +- lib/beacon/migrations/v001.ex | 10 ------ lib/beacon/migrations/v002.ex | 11 +++++++ mix.exs | 1 - mix.lock | 3 -- 6 files changed, 35 insertions(+), 51 deletions(-) diff --git a/lib/beacon/content.ex b/lib/beacon/content.ex index 60ee4ece1..019f4c98f 100644 --- a/lib/beacon/content.ex +++ b/lib/beacon/content.ex @@ -3940,16 +3940,12 @@ defmodule Beacon.Content do @doc type: :info_handlers @spec create_info_handler(map()) :: {:ok, InfoHandler.t()} | {:error, Changeset.t()} def create_info_handler(attrs) do - variable_names = - attrs - |> ExUtils.Map.atomize_keys() - |> Map.get(:msg) - |> list_variable_names() + msg = retrieve_msg(attrs) changeset = %InfoHandler{} |> InfoHandler.changeset(attrs) - |> validate_info_handler(variable_names) + |> validate_info_handler(msg) site = Changeset.get_field(changeset, :site) @@ -3959,26 +3955,31 @@ defmodule Beacon.Content do end @spec validate_info_handler(Changeset.t(), [String.t()], [String.t()]) :: Changeset.t() - defp validate_info_handler(changeset, variable_names, imports \\ []) do + defp validate_info_handler(changeset, msg, imports \\ []) do code = Changeset.get_field(changeset, :code) site = Changeset.get_field(changeset, :site) metadata = %Beacon.Template.LoadMetadata{site: site} - var = ["socket"] ++ variable_names + var = ["socket"] ++ msg imports = ["Phoenix.LiveView"] ++ imports do_validate_template(changeset, :code, :elixir, code, metadata, var, imports) end - @spec list_variable_names(String.t()) :: [String.t()] - defp list_variable_names(msg) do - {_msg, variables} = - msg - |> String.replace("{", "") - |> String.replace("}", "") - |> String.split(", ") - |> List.pop_at(0) + @spec retrieve_msg(map(), InfoHandler.t()) :: [String.t()] + defp retrieve_msg(attrs, info_handler \\ %{}) do + case Map.get(attrs, :msg) do + nil -> + case Map.get(attrs, "msg") do + nil -> + [Map.get(info_handler, :msg)] + + msg -> + [msg] + end - variables + msg -> + [msg] + end end @doc """ @@ -4069,15 +4070,12 @@ defmodule Beacon.Content do @doc type: :info_handlers @spec update_info_handler(InfoHandler.t(), map()) :: {:ok, InfoHandler.t()} def update_info_handler(%InfoHandler{} = info_handler, attrs) do - variable_names = - info_handler - |> retrieve_msg(attrs) - |> list_variable_names() + msg = retrieve_msg(attrs, info_handler) changeset = info_handler |> InfoHandler.changeset(attrs) - |> validate_info_handler(variable_names, ["Phoenix.Component"]) + |> validate_info_handler(msg, ["Phoenix.Component"]) site = Changeset.get_field(changeset, :site) @@ -4086,26 +4084,15 @@ defmodule Beacon.Content do |> tap(&maybe_broadcast_updated_content_event(&1, :info_handler)) end - @spec retrieve_msg(InfoHandler.t(), map()) :: String.t() - defp retrieve_msg(info_handler, attrs) do - attrs = ExUtils.Map.atomize_keys(attrs) - - case Map.get(attrs, :msg) do - nil -> - info_handler.msg - - msg -> - msg - end - end - @doc """ Deletes info handler. """ @doc type: :info_handlers @spec delete_info_handler(InfoHandler.t()) :: {:ok, InfoHandler.t()} | {:error, Changeset.t()} def delete_info_handler(info_handler) do - repo(info_handler.site).delete(info_handler) + info_handler + |> repo(info_handler).delete() + |> tap(&maybe_broadcast_updated_content_event(&1, :info_handler)) end ## Utils diff --git a/lib/beacon/loader/page.ex b/lib/beacon/loader/page.ex index f380e8fa4..c32fc84b9 100644 --- a/lib/beacon/loader/page.ex +++ b/lib/beacon/loader/page.ex @@ -41,7 +41,7 @@ defmodule Beacon.Loader.Page do import Phoenix.Component, except: [assign: 2, assign: 3, assign_new: 3] import Beacon.Web, only: [assign: 2, assign: 3, assign_new: 3] import Beacon.Router, only: [beacon_asset_path: 2, beacon_asset_url: 2] - import Beacon.Web.Gettext + use Gettext, backend: Beacon.Gettext import unquote(routes_module) import unquote(components_module) diff --git a/lib/beacon/migrations/v001.ex b/lib/beacon/migrations/v001.ex index 04b098f16..56fa3931d 100644 --- a/lib/beacon/migrations/v001.ex +++ b/lib/beacon/migrations/v001.ex @@ -55,15 +55,6 @@ defmodule Beacon.Migrations.V001 do create_if_not_exists index(:beacon_live_data_assigns, [:live_data_id]) - create_if_not_exists table(:beacon_info_handlers, primary_key: false) do - add :id, :binary_id, primary_key: true - add :site, :text, null: false - add :msg, :text, null: false - add :code, :text, null: false - - timestamps(type: :utc_datetime_usec) - end - create_if_not_exists table(:beacon_snippet_helpers, primary_key: false) do add :id, :binary_id, primary_key: true add :site, :text, null: false @@ -259,7 +250,6 @@ defmodule Beacon.Migrations.V001 do drop_if_exists table(:beacon_assets) drop_if_exists table(:beacon_stylesheets) drop_if_exists table(:beacon_live_data_assigns) - drop_if_exists table(:beacon_info_handlers) drop_if_exists table(:beacon_live_data) drop_if_exists table(:beacon_snippet_helpers) drop_if_exists table(:beacon_component_attrs) diff --git a/lib/beacon/migrations/v002.ex b/lib/beacon/migrations/v002.ex index 571f60827..bc7d2efb0 100644 --- a/lib/beacon/migrations/v002.ex +++ b/lib/beacon/migrations/v002.ex @@ -34,6 +34,15 @@ defmodule Beacon.Migrations.V002 do |> then(&repo().insert_all("beacon_event_handlers", &1, [])) drop_if_exists table(:beacon_page_event_handlers) + + create_if_not_exists table(:beacon_info_handlers, primary_key: false) do + add :id, :binary_id, primary_key: true + add :site, :text, null: false + add :msg, :text, null: false + add :code, :text, null: false + + timestamps(type: :utc_datetime_usec) + end end def down do @@ -50,5 +59,7 @@ defmodule Beacon.Migrations.V002 do # global event handlers can't be converted back into page event handlers drop_if_exists table(:beacon_event_handlers) + + drop_if_exists table(:beacon_info_handlers) end end diff --git a/mix.exs b/mix.exs index 937b1f6ad..b1ec47bdb 100644 --- a/mix.exs +++ b/mix.exs @@ -78,7 +78,6 @@ defmodule Beacon.MixProject do {:safe_code, "~> 0.2"}, {:solid, "~> 0.14"}, {:tailwind, "~> 0.2"}, - {:ex_utils, "~> 0.1.7"}, # Dev, Test, Docs {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index 562f47a3d..f1342af3e 100644 --- a/mix.lock +++ b/mix.lock @@ -20,12 +20,9 @@ "ex_aws_s3": {:hex, :ex_aws_s3, "2.4.0", "ce8decb6b523381812798396bc0e3aaa62282e1b40520125d1f4eff4abdff0f4", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "85dda6e27754d94582869d39cba3241d9ea60b6aa4167f9c88e309dc687e56bb"}, "ex_brotli": {:hex, :ex_brotli, "0.4.1", "b9d43d1ba0641b460b8477b1dc20f224827651c5af5977419b92c4e63c27d619", [:mix], [{:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "840eeace8354abfa470fa094b8b0231ef184f5692cd4b219143c403bcf8f0756"}, "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, - "ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm", "66d4fe75285948f2d1e69c2a5ddd651c398c813574f8d36a9eef11dc20356ef6"}, "exconstructor": {:hex, :exconstructor, "1.2.13", "7021eed1450202dcbcd1ef021d6aacf7351854ff9d7964f166931567f9dfa9fb", [:mix], [], "hexpm", "69d3f0251a07bb7c5ef85bde22a1eee577dfbb49852d77fb7ad7b937035aeef2"}, - "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, - "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, From 837458b946fcbc879310379eb641d6297e769ecf Mon Sep 17 00:00:00 2001 From: ddink Date: Wed, 11 Sep 2024 09:53:37 -0500 Subject: [PATCH 4/4] add shared handle_info callbacks PR feedback --- lib/beacon/content.ex | 42 ++++++++---------------------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/lib/beacon/content.ex b/lib/beacon/content.ex index ab9699dc9..3a7312a59 100644 --- a/lib/beacon/content.ex +++ b/lib/beacon/content.ex @@ -3944,48 +3944,27 @@ defmodule Beacon.Content do @doc type: :info_handlers @spec create_info_handler(map()) :: {:ok, InfoHandler.t()} | {:error, Changeset.t()} def create_info_handler(attrs) do - msg = retrieve_msg(attrs) - - changeset = - %InfoHandler{} - |> InfoHandler.changeset(attrs) - |> validate_info_handler(msg) - + changeset = InfoHandler.changeset(%InfoHandler{}, attrs) site = Changeset.get_field(changeset, :site) changeset + |> validate_info_handler() |> repo(site).insert() |> tap(&maybe_broadcast_updated_content_event(&1, :info_handler)) end - @spec validate_info_handler(Changeset.t(), [String.t()], [String.t()]) :: Changeset.t() - defp validate_info_handler(changeset, msg, imports \\ []) do + @spec validate_info_handler(Changeset.t(), [String.t()]) :: Changeset.t() + defp validate_info_handler(changeset, imports \\ []) do code = Changeset.get_field(changeset, :code) + msg = Changeset.get_field(changeset, :msg) site = Changeset.get_field(changeset, :site) metadata = %Beacon.Template.LoadMetadata{site: site} - var = ["socket"] ++ msg + var = ["socket", msg] imports = ["Phoenix.LiveView"] ++ imports do_validate_template(changeset, :code, :elixir, code, metadata, var, imports) end - @spec retrieve_msg(map(), InfoHandler.t()) :: [String.t()] - defp retrieve_msg(attrs, info_handler \\ %{}) do - case Map.get(attrs, :msg) do - nil -> - case Map.get(attrs, "msg") do - nil -> - [Map.get(info_handler, :msg)] - - msg -> - [msg] - end - - msg -> - [msg] - end - end - @doc """ Creates a info handler, raising an error if unsuccessful. @@ -4074,16 +4053,11 @@ defmodule Beacon.Content do @doc type: :info_handlers @spec update_info_handler(InfoHandler.t(), map()) :: {:ok, InfoHandler.t()} def update_info_handler(%InfoHandler{} = info_handler, attrs) do - msg = retrieve_msg(attrs, info_handler) - - changeset = - info_handler - |> InfoHandler.changeset(attrs) - |> validate_info_handler(msg, ["Phoenix.Component"]) - + changeset = InfoHandler.changeset(info_handler, attrs) site = Changeset.get_field(changeset, :site) changeset + |> validate_info_handler(["Phoenix.Component"]) |> repo(site).update() |> tap(&maybe_broadcast_updated_content_event(&1, :info_handler)) end