defmodule Exampple.Router do
@moduledoc """
Router implements the dynamic to define the flow where a stanza
will be following for find the controller and function where it
should be attended.
Inside of this module are defined all of the macros we can use
for the creation of our router template.
You can see in the general documentation information about the
router.
"""
require Logger
alias Exampple.Xml.Xmlel
@dynsup Exampple.Router.Task.Monitor.Supervisor
@monitor Exampple.Router.Task.Monitor
@default_timeout 5_000
@doc """
Run from the component for each incoming stanza to ensuring where it
should go based on the routing module we have compiled. Our routing
module should be available through the application configuration and
that's because we are sending the `otp_app` here.
The first parameter provided (`xmlel`) is the stanza in
`Exampple.Xml.Xmlel` format. The second parameter is the XMPP `domain`
for the component inside of the XMPP network. The last parameter is
optional, `timeout` specified in milliseconds specify how much time
have the stanza to be processed, by default it's 5 seconds.
See the general documentation to know more about the routing process.
"""
def route(xmlel, domain, otp_app, timeout \\ @default_timeout) do
Logger.debug("[router] processing: #{inspect(xmlel)}")
DynamicSupervisor.start_child(@dynsup, {@monitor, [xmlel, domain, otp_app, timeout]})
end
@doc false
defmacro __using__(_opts) do
quote do
import Exampple.Router
Module.register_attribute(__MODULE__, :routes, accumulate: true)
Module.register_attribute(__MODULE__, :namespaces, accumulate: true)
Module.register_attribute(__MODULE__, :identities, accumulate: true)
Module.register_attribute(__MODULE__, :disco_extra, accumulate: true)
Module.register_attribute(__MODULE__, :includes, accumulate: true)
Module.register_attribute(__MODULE__, :features, accumulate: true)
@envelopes []
@namespace_separator ":"
@before_compile Exampple.Router
end
end
defp process_route({stanza_type, type, "", controller, function}) do
quote do
def route(
%Exampple.Router.Conn{
stanza_type: unquote(stanza_type),
type: unquote(type)
} = conn,
stanza
) do
unquote(controller).unquote(function)(conn, stanza)
end
end
end
defp process_route({stanza_type, type, xmlns, controller, function}) do
quote do
def route(
%Exampple.Router.Conn{
stanza_type: unquote(stanza_type),
xmlns: unquote(xmlns),
type: unquote(type)
} = conn,
stanza
) do
unquote(controller).unquote(function)(conn, stanza)
end
end
end
defp process_route({route, {stanza_type, type, "", _, _}}) do
quote do
def route(
%Exampple.Router.Conn{
stanza_type: unquote(stanza_type),
type: unquote(type)
} = conn,
stanza
) do
unquote(route).route(conn, stanza)
end
end
end
defp process_route({route, {stanza_type, type, xmlns, _, _}}) do
quote do
def route(
%Exampple.Router.Conn{
stanza_type: unquote(stanza_type),
xmlns: unquote(xmlns),
type: unquote(type)
} = conn,
stanza
) do
unquote(route).route(conn, stanza)
end
end
end
defp process_routes(routes) do
for route <- routes, do: process_route(route)
end
@doc false
defmacro __before_compile__(env) do
envelopes = Module.get_attribute(env.module, :envelopes)
disco = Module.get_attribute(env.module, :disco, false)
includes = Module.get_attribute(env.module, :includes, [])
routes =
for route <- Module.get_attribute(env.module, :routes) do
{route, _} = Code.eval_quoted(route)
route
end
inc_routes =
for module <- includes do
Code.ensure_compiled(module)
for route <- module.route_info(:paths) do
{module, route}
end
end
|> List.flatten()
route_functions = process_routes(routes)
inc_route_functions = process_routes(inc_routes)
all_routes = routes ++ for {_, route} <- inc_routes, do: route
fallback =
if fback = Module.get_attribute(env.module, :fallback) do
{controller, function} = fback
{controller, []} = Code.eval_quoted(controller)
[
quote do
def route(conn, stanza) do
unquote(controller).unquote(function)(conn, stanza)
end
end
]
else
[]
end
envelope_functions =
for envelope_xmlns <- envelopes do
quote do
def route(
%Exampple.Router.Conn{
xmlns: unquote(envelope_xmlns)
} = conn,
stanza
) do
case Exampple.Xmpp.Envelope.handle(conn, stanza) do
{conn, stanza} -> route(conn, stanza)
nil -> :ok
end
end
end
end
namespaces =
env.module
|> Module.get_attribute(:namespaces)
|> Enum.reject(&(&1 == ""))
namespaces =
(namespaces ++ Module.get_attribute(env.module, :features, []))
|> Enum.uniq()
|> Enum.sort()
inc_namespaces =
for module <- includes do
module.route_info(:namespaces)
end
|> List.flatten()
disco_info =
if disco do
namespaces =
for namespace <- namespaces ++ inc_namespaces do
Macro.escape(Xmlel.new("feature", %{"var" => namespace}))
end
identity =
for {_, _, [category, type, name]} <- Module.get_attribute(env.module, :identities) do
Macro.escape(
Xmlel.new("identity", %{
"category" => category,
"type" => type,
"name" => name
})
)
end
extra =
Module.get_attribute(env.module, :disco_extra)
|> List.wrap()
|> List.flatten()
identity ++ namespaces ++ extra
else
[]
end
discovery =
quote do
def route(
%Exampple.Router.Conn{
to_jid: %Exampple.Xmpp.Jid{node: "", resource: ""},
xmlns: "http://jabber.org/protocol/disco#info"
} = conn,
[stanza]
) do
payload = %Xmlel{stanza | children: unquote(disco_info)}
conn
|> Exampple.Xmpp.Stanza.iq_resp([payload])
|> Exampple.Component.send()
end
end
route_info_function =
quote do
def route_info(:paths), do: unquote(Macro.escape(all_routes))
def route_info(:namespaces), do: unquote(namespaces)
end
[route_info_function | envelope_functions] ++
[discovery] ++
inc_route_functions ++
route_functions ++
[fallback]
end
@doc """
Use this whenever you want to change the way a namespace will be join. For
some configurations like this one:
```elixir
iq "http://jabber.org/protocol" do
join_with "/"
get "disco#info", MyController, :disco_info_get
end
```
The namespace is join in this case using the slash (/) because we defined it
previously to define the first route.
The default value for join is `:` so for each example like which I put above
you have to use `join_with` and it's restarted for each block.
"""
defmacro join_with(separator) when is_binary(separator) do
quote do
@namespace_separator unquote(separator)
end
end
defmacro join_with(other) do
raise """
join_with only accepts String as parameter #{inspect(other)} is
not permitted. Default is ":".
"""
end
@doc """
Did you split your code in different applications but still want to keep
only one point to connect to the XMPP server? It's possible if you define
different routers and then, for the main one, you use `includes/1`. This
macro is accepting a module containing the routes to be included.
"""
defmacro includes(module) do
quote do
@includes unquote(module)
end
end
@doc """
The envelopes are very usual in components and mainly if we are using the
delegation specification ([XEP-0355](https://xmpp.org/extensions/xep-0355.html)).
You can see more information regarding this in the general documentation about
routing.
"""
defmacro envelope(xmlns) do
xmlns_list = if is_list(xmlns), do: xmlns, else: [xmlns]
quote location: :keep do
xmlns_list = unquote(xmlns_list)
Module.put_attribute(__MODULE__, :envelopes, xmlns_list)
for xmlns <- xmlns_list do
Module.put_attribute(__MODULE__, :namespaces, xmlns)
end
end
end
@doc """
Specify a route block where we can add the different routes. This could
have the base namespace or not. This is based on the `iq` stanza type.
"""
defmacro iq(xmlns_partial \\ "", do: block) do
quote location: :keep do
Module.put_attribute(__MODULE__, :stanza_type, "iq")
Module.put_attribute(__MODULE__, :xmlns_partial, unquote(xmlns_partial))
@namespace_separator ":"
unquote(block)
end
end
@doc """
Specify a route block where we can add the different routes. This could
have the base namespace or not. This is based on the `message` stanza type.
"""
defmacro message(xmlns_partial \\ "", do: block) do
quote location: :keep do
Module.put_attribute(__MODULE__, :stanza_type, "message")
Module.put_attribute(__MODULE__, :xmlns_partial, unquote(xmlns_partial))
@namespace_separator ":"
unquote(block)
end
end
@doc """
Specify a route block where we can add the different routes. This could
have the base namespace or not. This is based on the `presence` stanza type.
"""
defmacro presence(xmlns_partial \\ "", do: block) do
quote location: :keep do
Module.put_attribute(__MODULE__, :stanza_type, "presence")
Module.put_attribute(__MODULE__, :xmlns_partial, unquote(xmlns_partial))
@namespace_separator ":"
unquote(block)
end
end
@doc false
def validate_controller!(controller) do
{module, []} = Code.eval_quoted(controller)
try do
module.module_info()
rescue
UndefinedFunctionError ->
module_name =
module
|> Module.split()
|> Enum.join(".")
raise ArgumentError, """
\nThe module #{module_name} was not found to create the route,
use absolute paths or aliases to be sure all of the modules
are reachable.
"""
end
end
@doc false
def validate_function!(controller, function) do
{module, []} = Code.eval_quoted(controller)
{function, []} = Code.eval_quoted(function)
unless function_exported?(module, function, 2) do
module_name =
module
|> Module.split()
|> Enum.join(".")
raise ArgumentError, """
\nThe function #{module_name}.#{function}/2 was not found to create
the route, check the function exists and have 2 parameters to
receive "conn" and "stanza".
"""
end
end
defmacro discovery(block \\ nil) do
if block do
quote do
Module.put_attribute(__MODULE__, :disco, true)
unquote(block)
end
else
quote do
Module.put_attribute(__MODULE__, :disco, true)
end
end
end
defmacro identity(opts) do
quote do
opts = unquote(opts)
unless Module.get_attribute(__MODULE__, :disco, false) do
raise """
identity MUST be inside of a discovery block.
"""
end
unless category = opts[:category] do
raise """
identity MUST contain a category option.
"""
end
unless type = opts[:type] do
raise """
identity MUST contain a type option.
"""
end
unless name = opts[:name] do
raise """
identity MUST contain a name option.
"""
end
Module.put_attribute(__MODULE__, :identities, Macro.escape({category, type, name}))
end
end
defmacro extra(stanzas) do
quote do
unless Module.get_attribute(__MODULE__, :disco, false) do
raise """
identity MUST be inside of a discovery block.
"""
end
stanzas = unquote(stanzas)
Module.put_attribute(__MODULE__, :disco_extra, Macro.escape(stanzas))
end
end
defmacro feature(namespace) do
quote do
@features unquote(namespace)
end
end
def ns_join([], _separator), do: ""
def ns_join(["" | chunks], separator), do: ns_join(chunks, separator)
def ns_join(chunks, separator) do
chunks
|> Enum.map(&String.trim(&1, separator))
|> Enum.join(separator)
end
defmacro error(xmlns \\ "", controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape({@stanza_type, "error", namespace, unquote(controller), unquote(function)})
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro unavailable(xmlns \\ "", controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape(
{@stanza_type, "unavailable", namespace, unquote(controller), unquote(function)}
)
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro subscribe(xmlns \\ "", controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape(
{@stanza_type, "subscribe", namespace, unquote(controller), unquote(function)}
)
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro subscribed(xmlns \\ "", controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape(
{@stanza_type, "subscribed", namespace, unquote(controller), unquote(function)}
)
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro unsubscribe(xmlns \\ "", controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape(
{@stanza_type, "unsubscribe", namespace, unquote(controller), unquote(function)}
)
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro unsubscribed(xmlns \\ "", controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape(
{@stanza_type, "unsubscribed", namespace, unquote(controller), unquote(function)}
)
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro probe(xmlns \\ "", controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape({@stanza_type, "probe", namespace, unquote(controller), unquote(function)})
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro normal(xmlns \\ "", controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape({@stanza_type, "normal", namespace, unquote(controller), unquote(function)})
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro headline(xmlns \\ "", controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape(
{@stanza_type, "headline", namespace, unquote(controller), unquote(function)}
)
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro groupchat(xmlns \\ "", controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape(
{@stanza_type, "groupchat", namespace, unquote(controller), unquote(function)}
)
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro chat(xmlns \\ "", controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape({@stanza_type, "chat", namespace, unquote(controller), unquote(function)})
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro get(xmlns, controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape({@stanza_type, "get", namespace, unquote(controller), unquote(function)})
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro set(xmlns, controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
namespace = Exampple.Router.ns_join([@xmlns_partial, unquote(xmlns)], @namespace_separator)
Module.put_attribute(
__MODULE__,
:routes,
Macro.escape({@stanza_type, "set", namespace, unquote(controller), unquote(function)})
)
Module.put_attribute(__MODULE__, :namespaces, namespace)
end
end
defmacro fallback(controller, function) do
validate_controller!(controller)
validate_function!(controller, function)
quote location: :keep do
Module.put_attribute(
__MODULE__,
:fallback,
Macro.escape({unquote(controller), unquote(function)})
)
end
end
end