From 219c9c88952d15c60f25f52b963aebf2c331d9d9 Mon Sep 17 00:00:00 2001 From: belaustegui Date: Sat, 13 Jan 2018 20:26:38 +0100 Subject: [PATCH 1/9] Dispatch actions to their modules in runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously Frankt defined a `handle_in/3` function for every available action. This caused the channel exposing the Frankt actions to grow with lots of clauses for the same function. Frankt now dispatches actions in runtime to their corresponding modules. The “notification:mark_as_read” action will now be dispatched by invoking the “mark_as_read” function in the configured “notification” module. The correspondency between handler names such as “notification” and the corresponding modules is configured by the user and can vary from channel to channel. --- lib/frankt.ex | 109 +++++++++++++++--------------------------- priv/static/frankt.js | 2 +- 2 files changed, 40 insertions(+), 71 deletions(-) diff --git a/lib/frankt.ex b/lib/frankt.ex index 03ef1fa..6699818 100644 --- a/lib/frankt.ex +++ b/lib/frankt.ex @@ -68,89 +68,58 @@ defmodule Frankt do """ @type response_handler :: ((params :: map(), socket :: Phoenix.Socket.t()) -> any()) - defmacro __using__(opts) do + defmacro __using__(_opts) do quote do - Module.register_attribute __MODULE__, :responses, accumulate: true - Module.put_attribute __MODULE__, :gettext, unquote(Keyword.get(opts, :gettext)) - - import Frankt, only: [defresponse: 2] - - @before_compile unquote(__MODULE__) + def handle_in("frankt-action", params = %{"action" => action}, socket) do + [handler_name, handler_fn] = String.split(action, ":") + handler_module = Frankt.__handler__(__MODULE__, handler_name) + gettext = Frankt.__gettext__(__MODULE__) + data = Map.get(params, "data", %{}) + + Frankt.__execute_action__(handler_module, String.to_existing_atom(handler_fn), data, socket, gettext) + {:noreply, socket} + end end end - @doc """ - Define a response to client-side triggers. - - This macro generates the code that call the response handler `function` when `message` is received. - - Take into account that to make a certain response behave differently depending on the received - params you must pattern match in the response handler `function` instead of using a second - `defresponse`. For more information take a look at `t:response_handler/0`. - """ - @spec defresponse(message :: String.t(), function :: response_handler()) :: any() - defmacro defresponse(message, function) do - quote do - Module.put_attribute(__MODULE__, :responses, unquote(message)) - - def execute_response(unquote(message), params, socket) do - Frankt.__execute_response__(unquote(function), params, socket, @gettext) + @doc false + def __execute_action__(module, fun, params, socket, gettext) do + invoke_action = fn -> + unless function_exported?(module, fun, 2) do + raise "Frankt is trying to execute an action, but the handler module does not define the appropriate function. Please define a '#{fun}/3' function in your ·#{module} module." end + apply(module, fun, [params, socket]) end - end - - # Before the compilation takes place we need to generate the module's `use` - # handler so it can be used inside the Frankt channel with the response - # handlers exported correctly. - # It's important to point that this process should be done just before the - # compilation takes place to: - # * Be able to generate a bit more of code that go into the compilation - # * Be able to read responses storage to kow which ones needs to inject in the - # socket channel. - defmacro __before_compile__(_) do - quote do - defmacro __using__(_) do - Enum.map(@responses, fn message -> - quote do - def handle_in(unquote(message), params, socket) do - unquote(__MODULE__).execute_response(unquote(message), params, socket) - end - end - end) - end + if gettext do + locale = + case Map.get(socket.assigns, :locale) do + nil -> raise "You have configured Frankt to use Gettext for i18n, but the response does not know which locale to use. Please store the desired locale into a `locale` assign in the socket." + locale -> locale + end + Gettext.with_locale(gettext, locale, invoke_action) + else + invoke_action.() end end - @doc """ - Build the topic name for the Frankt channel. - - The topic name is used when connecting clients to Frankt. It can also be used in other - circumstances such broadcasting server-side updates for certain users. - - The `client` variable can be any value used to identify each connection (for example the - connected user ID). This variable will be base16 encoded for privacy. - """ - @spec topic_name(client :: String.t()) :: String.t() - def topic_name(client), do: "frankt:#{:md5 |> :crypto.hash(client) |> Base.encode16()}" - @doc false - def __execute_response__(function, params, socket, nil) do - function.(params, socket) - {:noreply, socket} - end - def __execute_response__(function, params, socket, gettext) do - Gettext.with_locale(gettext, get_locale(socket), fn -> - function.(params, socket) - {:noreply, socket} - end) + def __handler__(frankt_module, name) when is_binary(name) do + handlers = Application.get_env(:frankt, frankt_module) + if is_nil(handlers) do + raise "You have not configured any handlers for Frankt. Please set at least one handler in your configuration." + end + case get_in(handlers, [:handlers, String.to_existing_atom(name)]) do + nil -> "Frankt can not find a handler for '#{name}'. Please, chech that you are using the correct name or define a new handler in your configuration." + handler -> handler + end end - defp get_locale(socket) do - case Map.get(socket.assigns, :locale) do - nil -> raise "You have configured Frankt to use Gettext for i18n, but the response does not know which locale to use. Please store the desired locale into a `locale` assign in the socket." - locale -> locale - end + @doc false + def __gettext__(frankt_module) do + :frankt + |> Application.get_env(__MODULE__, []) + |> Keyword.get(:gettext) end end diff --git a/priv/static/frankt.js b/priv/static/frankt.js index 6bc24d8..743fbe5 100644 --- a/priv/static/frankt.js +++ b/priv/static/frankt.js @@ -5,7 +5,7 @@ export let channel = {}; export function sendMsg(action, data) { if (channel.state === 'closed') init(true); - return channel.push(action, data); + return channel.push('frankt-action', {action: action, data: data}); } export function serializeForm(element) { From 80a334bb51de927cbac468e1059d0857465b962e Mon Sep 17 00:00:00 2001 From: belaustegui Date: Mon, 15 Jan 2018 12:19:35 +0100 Subject: [PATCH 2/9] Use proper module for gettext config --- lib/frankt.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/frankt.ex b/lib/frankt.ex index 6699818..4037452 100644 --- a/lib/frankt.ex +++ b/lib/frankt.ex @@ -118,7 +118,7 @@ defmodule Frankt do @doc false def __gettext__(frankt_module) do :frankt - |> Application.get_env(__MODULE__, []) + |> Application.get_env(frankt_module, []) |> Keyword.get(:gettext) end From f82e184760a44721bcb5034eb398ebf1aa46e4f1 Mon Sep 17 00:00:00 2001 From: belaustegui Date: Mon, 15 Jan 2018 15:38:09 +0100 Subject: [PATCH 3/9] Add test helper for Frankt actions --- lib/test/action_test.ex | 29 +++++++++++++++++++++++++++++ test/frankt_test.exs | 8 -------- 2 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 lib/test/action_test.ex delete mode 100644 test/frankt_test.exs diff --git a/lib/test/action_test.ex b/lib/test/action_test.ex new file mode 100644 index 0000000..eb926b2 --- /dev/null +++ b/lib/test/action_test.ex @@ -0,0 +1,29 @@ +defmodule Frankt.ActionTest do + @moduledoc """ + Conveniences for testing Frankt actions. + + This module provides useful convenience functions for testing Frankt actions. The provided + functions simply wrap the calls provided by Phoenix. For more information take a look + at `Phoenix.ChannelTest`. + """ + + @doc false + defmacro __using__(_opts) do + quote do + import Frankt.ActionTest + end + end + + @doc """ + Call a Frankt action. + + Pushes a mesasge into the channel which triggers the Frankt `handle_in/3` function and then + dispatches to the corresponding action. + """ + @spec frankt_action(socket :: Socket.t, action :: String.t, payload :: map()) :: reference() + defmacro frankt_action(socket, action, payload \\ %{}) do + quote do + push(unquote(socket), "frankt-action", %{action: unquote(action), data: unquote(payload)}) + end + end +end diff --git a/test/frankt_test.exs b/test/frankt_test.exs deleted file mode 100644 index 0e61306..0000000 --- a/test/frankt_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -defmodule FranktTest do - use ExUnit.Case - doctest Frankt - - test "greets the world" do - assert Frankt.hello() == :world - end -end From eef0c14cdd4c3a2ac19aae318c9fdf3e1e5b831d Mon Sep 17 00:00:00 2001 From: Victor Ortiz Date: Tue, 27 Feb 2018 12:41:06 +0100 Subject: [PATCH 4/9] Clean serialized object with deep compact --- package.json | 3 ++- priv/static/dom.js | 36 +++++++++++++++--------------------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 8a4cf77..570a038 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "homepage": "https://github.com/acutario/frankt#readme", "dependencies": { - "phoenix": "^1.3.0" + "phoenix": "^1.3.0", + "deep-compact": "^1.1.0" } } diff --git a/priv/static/dom.js b/priv/static/dom.js index 2cadb58..3e5cdbb 100644 --- a/priv/static/dom.js +++ b/priv/static/dom.js @@ -1,3 +1,5 @@ +import deepCompact from 'deep-compact'; + const k_r_submitter = /^(?:submit|button|image|reset|file)$/i; const k_r_success_contrls = /^(?:input|select|textarea|keygen)/i; const brackets = /(\[[^\[\]]*\])/g; @@ -20,8 +22,7 @@ function serializer(result, key, value) { if (matches) { const keys = parse_keys(key); hash_assign(result, keys, value); - } - else { + } else { // Non bracket notation can make assignments directly. const existing = result[key]; @@ -33,11 +34,10 @@ function serializer(result, key, value) { // assignment could go through `hash_assign`. if (existing) { if (!Array.isArray(existing)) { - result[key] = [ existing ]; + result[key] = [existing]; } result[key].push(value); - } - else { + } else { result[key] = value; } } @@ -60,7 +60,7 @@ export function serialize(form, options) { //Object store each radio and set if it's empty or not const radio_store = {}; - for (let i=0 ; i Date: Tue, 13 Mar 2018 14:11:11 +0100 Subject: [PATCH 5/9] Allow to pass serialised data with data-attr franktData --- priv/static/frankt.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/priv/static/frankt.js b/priv/static/frankt.js index 428f199..468c84c 100644 --- a/priv/static/frankt.js +++ b/priv/static/frankt.js @@ -16,6 +16,10 @@ export function serializeForm(element) { data.push(Dom.serializeElement(element)); } + if (element.dataset.franktData) { + data.push($(element).data('franktData')); + } + if (element.dataset.franktTarget) { const target = document.querySelector(element.dataset.franktTarget); // Block submit form on enter @@ -32,7 +36,7 @@ export function serializeForm(element) { } function handleEvent(e, selector) { - if (e.target.matches(selector) || e.target.closest(selector)){ + if (e.target.matches(selector) || e.target.closest(selector)) { const target = e.target.matches(selector) ? e.target : e.target.closest(selector); e.preventDefault(); const data = serializeForm(target); From f7594f7de058fbc56a037918941415ca2c9418ce Mon Sep 17 00:00:00 2001 From: belaustegui Date: Mon, 15 Jan 2018 18:31:29 +0100 Subject: [PATCH 6/9] Improve Frankt.ActionTest docs --- lib/test/action_test.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/test/action_test.ex b/lib/test/action_test.ex index eb926b2..7d88109 100644 --- a/lib/test/action_test.ex +++ b/lib/test/action_test.ex @@ -2,9 +2,8 @@ defmodule Frankt.ActionTest do @moduledoc """ Conveniences for testing Frankt actions. - This module provides useful convenience functions for testing Frankt actions. The provided - functions simply wrap the calls provided by Phoenix. For more information take a look - at `Phoenix.ChannelTest`. + Frankt tests are actually channel tests. For more information take a look at + `Phoenix.ChannelTest`. """ @doc false @@ -19,6 +18,8 @@ defmodule Frankt.ActionTest do Pushes a mesasge into the channel which triggers the Frankt `handle_in/3` function and then dispatches to the corresponding action. + After pushing the message to Frankt you can check the response by using + `Phoenix.ChannelTest.assert_push/3`. """ @spec frankt_action(socket :: Socket.t, action :: String.t, payload :: map()) :: reference() defmacro frankt_action(socket, action, payload \\ %{}) do From 984229160c113fd902f116571ed1ca3ee146a95c Mon Sep 17 00:00:00 2001 From: belaustegui Date: Fri, 13 Apr 2018 09:18:13 +0200 Subject: [PATCH 7/9] Remove CSRF token parameter in message payload --- priv/static/frankt.js | 1 - 1 file changed, 1 deletion(-) diff --git a/priv/static/frankt.js b/priv/static/frankt.js index 468c84c..f59c2b3 100644 --- a/priv/static/frankt.js +++ b/priv/static/frankt.js @@ -10,7 +10,6 @@ export function sendMsg(action, data) { export function serializeForm(element) { const data = []; - data.push({csrf_token: document.querySelector('meta[name=csrf]').content}); if (element.name && element.tagName !== "INPUT") { data.push(Dom.serializeElement(element)); From 33c5b8af0097c840ccbef28db82fdcf5b4f90cd0 Mon Sep 17 00:00:00 2001 From: belaustegui Date: Fri, 13 Apr 2018 13:59:31 +0200 Subject: [PATCH 8/9] Protect against empty parameters for messages --- priv/static/frankt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/static/frankt.js b/priv/static/frankt.js index f59c2b3..7e21f07 100644 --- a/priv/static/frankt.js +++ b/priv/static/frankt.js @@ -9,7 +9,7 @@ export function sendMsg(action, data) { } export function serializeForm(element) { - const data = []; + const data = [{}]; if (element.name && element.tagName !== "INPUT") { data.push(Dom.serializeElement(element)); From 10921dc6698118a2f8d9747aec1c7515afc31a2b Mon Sep 17 00:00:00 2001 From: belaustegui Date: Tue, 17 Apr 2018 08:27:51 +0200 Subject: [PATCH 9/9] Fix error message for unknown functions --- lib/frankt.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/frankt.ex b/lib/frankt.ex index 4037452..26e8c1b 100644 --- a/lib/frankt.ex +++ b/lib/frankt.ex @@ -86,7 +86,7 @@ defmodule Frankt do def __execute_action__(module, fun, params, socket, gettext) do invoke_action = fn -> unless function_exported?(module, fun, 2) do - raise "Frankt is trying to execute an action, but the handler module does not define the appropriate function. Please define a '#{fun}/3' function in your ·#{module} module." + raise "Frankt is trying to execute an action, but the handler module does not define the appropriate function. Please define a '#{fun}/2' function in your ·#{module} module." end apply(module, fun, [params, socket]) end