diff --git a/config/config.exs b/config/config.exs index 9176cd0..c9c59bb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,30 +1,3 @@ -# This file is responsible for configuring your application -# and its dependencies with the aid of the Mix.Config module. use Mix.Config -# This configuration is loaded before any dependency and is restricted -# to this project. If another project depends on this project, this -# file won't be loaded nor affect the parent project. For this reason, -# if you want to provide default values for your application for -# 3rd-party users, it should be done in your "mix.exs" file. - -# You can configure your application as: -# -# config :poa_backend, key: :value -# -# and access this configuration in your application as: -# -# Application.get_env(:poa_backend, :key) -# -# You can also configure a 3rd-party app: -# -# config :logger, level: :info -# - -# It is also possible to import configuration files, relative to this -# directory. For example, you can emulate configuration per environment -# by uncommenting the line below and defining dev.exs, test.exs and such. -# Configuration from the imported file will override the ones defined -# here (which is why it is important to import them last). -# -# import_config "#{Mix.env}.exs" +import_config "#{Mix.env}.exs" diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..a60f82e --- /dev/null +++ b/config/dev.exs @@ -0,0 +1,7 @@ +use Mix.Config + +# configuration for custom handlers. The format is {custom_handler_name, module, args} +config :poa_backend, + :custom_handlers, + [ + ] \ No newline at end of file diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 0000000..a60f82e --- /dev/null +++ b/config/prod.exs @@ -0,0 +1,7 @@ +use Mix.Config + +# configuration for custom handlers. The format is {custom_handler_name, module, args} +config :poa_backend, + :custom_handlers, + [ + ] \ No newline at end of file diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..a60f82e --- /dev/null +++ b/config/test.exs @@ -0,0 +1,7 @@ +use Mix.Config + +# configuration for custom handlers. The format is {custom_handler_name, module, args} +config :poa_backend, + :custom_handlers, + [ + ] \ No newline at end of file diff --git a/lib/poa_backend/application.ex b/lib/poa_backend/application.ex index 5ddc372..b6eb587 100644 --- a/lib/poa_backend/application.ex +++ b/lib/poa_backend/application.ex @@ -1,17 +1,16 @@ defmodule POABackend.Application do - # See https://hexdocs.pm/elixir/Application.html - # for more information on OTP Applications @moduledoc false use Application def start(_type, _args) do - # List all child processes to be supervised - children = [ + import Supervisor.Spec + children = [ + supervisor(POABackend.CustomHandler.Supervisor, []) ] opts = [strategy: :one_for_one, name: POABackend.Supervisor] Supervisor.start_link(children, opts) end -end \ No newline at end of file +end diff --git a/lib/poa_backend/custom_handler.ex b/lib/poa_backend/custom_handler.ex new file mode 100644 index 0000000..2204677 --- /dev/null +++ b/lib/poa_backend/custom_handler.ex @@ -0,0 +1,67 @@ +defmodule POABackend.CustomHandler do + alias POABackend.Protocol.Message + @moduledoc """ + A Custom Handler is responsible of handling data sent from Agents (i.e. REST over HTTP, WebSockets...) "speaking" the POA Protocol. + + The main responsability is getting calls from Agents, transform the data into a [POABackend.Protocol.Message](POABackend.Protocol.Message.html#content) + Struct and sending it to the receivers. + + ### Writing your custom Handler + + You must to _use_ the POABackend.CustomHandler module. That will requires you to implement the function `child_spec/1` which will be + called from the `POABackend.CustomHandler.Supervisor` and it must returns the child spec for the process you are going to spawn. + + defmodule MyHandler do + use POABackend.CustomHandler + + def child_spec(options) do + Plug.Adapters.Cowboy.child_spec(scheme: options[:scheme], plug: POABackend.CustomHandler.Rest.Router, options: [port: options[:port]]) + end + + end + + In this example we are initializing our CustomHandler for REST requests using the Cowboy Plug and defining the endpoints in the `POABackend.CustomHandler.Rest.Router` + module. + + ### Configuring the handlers in the config file + + So far we have created a Custom Handler but we didn't tell `poa_backend` to start it. In order to do it we have to define the new Handler in + the config file. + + config :poa_backend, + :custom_handlers, + [ + {:rest_custom_handler, POABackend.CustomHandler.Rest, [port: 4002]} + ] + + Inside the `:custom_handlers` list we define the handlers we want to start. Each Handler is defined in a triple tuple where the first argument + is the id for that handler, the second one is the Elixir module which implements the CustomHandler behaviour and the third one is a list for arguments + which will be passed to the `child_spec/1` function as a parameter + + """ + + @doc """ + This function will be called from the `POABackend.CustomHandler.Supervisor` in order + to get the child specification for start the custom handler process. + """ + @callback child_spec(options :: list()) :: :supervisor.child_spec() + + defmacro __using__(_opts) do + quote do + @behaviour POABackend.CustomHandler + end + end + + @doc """ + This function dispatches the given Message to the appropiate receivers based on the Data Type. + + The mapping between Data Types and Receivers is done in the config file. + """ + @spec send_to_receivers(Message.t) :: :ok + def send_to_receivers(%Message{} = _message) do + require Logger + # this will be implemented when working with the receivers + Logger.warn("To be implemented") + :ok + end +end \ No newline at end of file diff --git a/lib/poa_backend/custom_handler/supervisor.ex b/lib/poa_backend/custom_handler/supervisor.ex new file mode 100644 index 0000000..699ac59 --- /dev/null +++ b/lib/poa_backend/custom_handler/supervisor.ex @@ -0,0 +1,24 @@ +defmodule POABackend.CustomHandler.Supervisor do + @moduledoc false + + def start_link do + Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) + end + + def init(:ok) do + require Logger + # create the children from the config file + custom_handlers = Application.get_env(:poa_backend, :custom_handlers) + + children = for {name, module, options} <- custom_handlers do + spec = apply(module, :child_spec, [options]) + + Logger.info("Custom handler #{name} started.") + + spec + end + + opts = [strategy: :one_for_one] + Supervisor.init(children, opts) + end +end \ No newline at end of file diff --git a/lib/poa_backend/protocol/message.ex b/lib/poa_backend/protocol/message.ex index 15f7bbc..ec69129 100644 --- a/lib/poa_backend/protocol/message.ex +++ b/lib/poa_backend/protocol/message.ex @@ -35,13 +35,13 @@ defmodule POABackend.Protocol.Message do That keeps all the message data and metadata """ @type t :: %__MODULE__{ - agent_id: String.t(), + agent_id: String.t() | nil, receivers: [atom()], - data_type: POABackend.CustomProtocol.DataType.t(), - message_type: POABackend.CustomProtocol.MessageType.t(), + data_type: POABackend.CustomProtocol.DataType.t() | nil, + message_type: POABackend.CustomProtocol.MessageType.t() | nil, assigns: %{atom() => any()}, - peer: {:inet.ip_address(), :inet.port_number()}, - data: Map.t() + peer: {:inet.ip_address(), :inet.port_number()} | nil, + data: Map.t() | nil } @doc """ diff --git a/mix.exs b/mix.exs index fdf7d58..68c2dc7 100644 --- a/mix.exs +++ b/mix.exs @@ -19,7 +19,7 @@ defmodule POABackend.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger, :cowboy, :plug], + extra_applications: [:logger], mod: {POABackend.Application, []} ] end @@ -27,9 +27,6 @@ defmodule POABackend.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:cowboy, "~> 1.0.0"}, - {:plug, "~> 1.0"}, - # Tests {:credo, "~> 0.9", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 0.5", only: [:dev], runtime: false}, @@ -51,6 +48,9 @@ defmodule POABackend.MixProject do POABackend.Protocol.Message, POABackend.Protocol.MessageType, POABackend.Protocol.DataType + ], + "Custom Handler": [ + POABackend.CustomHandler ] ] ] diff --git a/mix.lock b/mix.lock index bfa614d..5c50b11 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,10 @@ %{ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [], [], "hexpm"}, "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, - "credo": {:hex, :credo, "0.9.2", "841d316612f568beb22ba310d816353dddf31c2d94aa488ae5a27bb53760d0bf", [], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [], [], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.8.2", "b941a08a1842d7aa629e0bbc969186a4cefdd035bad9fe15d43aaaaaeb8fae36", [], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.12.1", "8bf2d0e11e722e533903fe126e14d6e7e94d9b7983ced595b75f532e04b7fdc7", [], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/custom_handler_test.exs b/test/custom_handler_test.exs new file mode 100644 index 0000000..b460585 --- /dev/null +++ b/test/custom_handler_test.exs @@ -0,0 +1,53 @@ +defmodule CustomHandlerTest do + use ExUnit.Case + + defmodule MyCustomHandler do + use POABackend.CustomHandler + use GenServer + + # CustomHandler Callbacks + + def child_spec(options) do + import Supervisor.Spec + + worker(__MODULE__, [options[:caller_pid]]) + end + + # GenServer Callbacks + + def start_link(args) do + GenServer.start_link(__MODULE__, args, [name: __MODULE__]) + end + + def init(caller_pid) do + {:ok, caller_pid} + end + + def handle_info(:hello, caller_pid) do + + # simulate we send something to the receivers + + POABackend.CustomHandler.send_to_receivers(POABackend.Protocol.Message.new()) + + send(caller_pid, :world) + {:noreply, caller_pid} + end + end + + test "initialize a custom handler in the supervisor" do + + Application.stop(:poa_backend) + Application.put_env(:poa_backend, :custom_handlers, [{:my_ch, MyCustomHandler, [caller_pid: self()]}]) + Application.ensure_all_started(:poa_backend) + + # sending a message and wait for the reply + + CustomHandlerTest.MyCustomHandler + |> Process.whereis() + |> send(:hello) + + assert_receive :world, 20_000 + + Application.stop(:poa_backend) + end +end \ No newline at end of file