defmodule Tamnoon.MethodManager do @moduledoc """ This module handles the management of different methods as you create them. Notably, it provides the `defmethod/2` macro. > #### Importing the module {: .info} > In order to create handlers for the methods you set up, you must `import Tamnoon.MethodManager` > in your _methods module_. Then, you can use the `defmethod/2` macro to implement handling > of the methods. """ require Logger @doc """ Defines a function named `tmnn_[name]`. Functions with this prefix in your _methods module_ will automatically be added to the possible methods when invoking `route_request/3`. Inside the created function, you can access the state of the client and the request object by the `state` and `req` variables respectively. Method handlers must return a tuple of `{return_value, new_state}`. ## Example ``` defmethod :get do key = get_key(req, state) if (key != nil) do {state[key], state} else {"Error: no matching key", state} end end ``` """ defmacro defmethod(name, do: block) do method_name = String.to_atom("tmnn_" <> Atom.to_string(name)) quote do def unquote(method_name)(req, state) do var!(req) = req # Use the `req` variable to stop unused variable warnings. var!(state) = var!(req) ^var!(state) = state unquote(block) end end end @doc """ The function used by `Tamnoon.SocketHandler.websocket_handle/2` to route the requests to the appropriate method handler. """ @spec route_request(list(module()), map(), map()) :: {:reply, {:text, return_value :: String.t()}, new_state :: map()} def route_request(methods_modules, payload, state) do method = payload["method"] found_func_info = Enum.find_value(methods_modules, fn methods_module -> func_info = methods_module.__info__(:functions) |> Enum.find(fn {name, arity} -> arity == 2 && Atom.to_string(name) == "tmnn_" <> method end) if func_info, do: {methods_module, func_info}, else: nil end) if found_func_info == nil do Logger.error("Method #{method} not found in any methods module.") {:reply, {:text, elem(Jason.encode(%{error: "Method #{method} not found."}, []), 1)}, state} else {methods_module, {func, _arity}} = found_func_info {{ret_val, new_state}, []} = quote do unquote(methods_module).unquote(func)( unquote(Macro.escape(payload)), unquote(Macro.escape(state)) ) end |> Code.eval_quoted() {:reply, {:text, elem(Jason.encode(ret_val, []), 1)}, new_state} end end @doc """ Returns a tuple containing the diffs and the new state after applying the diffs. Can be used in method handlers to update a value easily. ## Example ``` defmethod :change_something do diffs = %{some_key: "New value", another_key: "Another value"} diff(diffs, state) end ``` """ @spec diff(map(), map()) :: {map(), map()} def diff(diffs, state) do {diffs, Map.merge(state, diffs, fn _key, _old, new -> new end)} end @doc """ Triggers a method with the given name and payload. An additional timeout can be specified to delay the triggering of the method. """ @spec trigger_method(atom(), map(), non_neg_integer()) :: :ok | :noconnect | :nosuspend | reference() def trigger_method(method_name, payload, timeout \\ 0) def trigger_method(method, req, 0) do Process.send(self(), Jason.encode!(Map.merge(req, %{method: method})), []) end def trigger_method(method, req, timeout) do Process.send_after(self(), Jason.encode!(Map.merge(req, %{method: method})), timeout) end end