[#8] plugin mechanism for custom handlers

This commit is contained in:
Felipe Ripoll 2018-06-06 17:12:25 -06:00
parent e2ff4a48c7
commit a1fde7a574
11 changed files with 182 additions and 43 deletions

View File

@ -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"

7
config/dev.exs Normal file
View File

@ -0,0 +1,7 @@
use Mix.Config
# configuration for custom handlers. The format is {custom_handler_name, module, args}
config :poa_backend,
:custom_handlers,
[
]

7
config/prod.exs Normal file
View File

@ -0,0 +1,7 @@
use Mix.Config
# configuration for custom handlers. The format is {custom_handler_name, module, args}
config :poa_backend,
:custom_handlers,
[
]

7
config/test.exs Normal file
View File

@ -0,0 +1,7 @@
use Mix.Config
# configuration for custom handlers. The format is {custom_handler_name, module, args}
config :poa_backend,
:custom_handlers,
[
]

View File

@ -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
end

View File

@ -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

View File

@ -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

View File

@ -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 """

View File

@ -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
]
]
]

View File

@ -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"},

View File

@ -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