Hello!
I have a real example I’d like to discuss. Please let me know if I can read about it somewhere in docs, I didn’t find any related.
I’m doing an app using channels for the front-end <-> back-end data exchange. The problem is that application connects to the channel topic and have a long events list with different reactions on each of them. It very hard to maintain a long channel module with all of these methods, so I split up the main channel module into logical parts.
The main piece has a simple handle_in(topic, payload, socket)
method which in order calls a proceed_incoming(topic, payload, socket)
method defined depending on the topic
(with pattern matching) in each logical chunks.
At the same time I have a general method to catch all topic\payload pairs wasn’t described in the code which is must be the last one.
def proceed_incoming(_topic, _payload, socket) do
{:reply, {:error, %{"status" => "400", "reason" => "bad request"}}, socket}
end
Because of this last requirement I need to insert a module contained that methods at the very end of my channel module.
Now I’m using @before_compile macro and it works, but is very error prone. For example, now I’ve got the warning below and can’t easily find a corresponding clause:
warning: this clause cannot match because a previous clause at line 1 always matches
lib/profile_web/channels/customer_channel.ex:1
So the question is:
Is there any other, more convenient way to accomplish my goal or is this my approach is generally wrong and I must go an another way?
Thanks!
Below is example of my “main” channel module:
defmodule ProfileWeb.CustomerChannel do
use ProfileWeb, :channel
alias Profile.Sessions
alias Profile.Config
# we need `require` each module before use their __before_compile__/1 macroses below
require ProfileWeb.CustomerChannel.{Application, Step2, Step3, Step4, UnknownMessage}
@before_compile ProfileWeb.CustomerChannel.Application
@before_compile ProfileWeb.CustomerChannel.Step2
@before_compile ProfileWeb.CustomerChannel.Step3
@before_compile ProfileWeb.CustomerChannel.Step4
@before_compile ProfileWeb.CustomerChannel.UnknownMessage # must be the last!
def join("customer:lobby", payload, socket) do
if authorized?(payload, socket) do
# subscribe to personalized messages
ProfileWeb.Endpoint.subscribe("customer:#{socket.assigns.connection.customer_id}")
# send init data
send(self(), :after_join)
# please check this as its handle_info(:set_application_timeout) handles is in the ProfileWeb.CustomerChannel.Application module
send(self(), :set_application_timeout)
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
# handle init request
def handle_info(:after_join, socket) do
data = %{
# skipped
}
push socket, "init", data
{:noreply, socket}
end
@doc """
The main handle in function to route all incoming messages to the use'd modules.
It takes a topic, message and socket and call a `proceed_incoming/3` function
to handle such a message type.
Each message handler module must implement this function for the every incoming message
it awaits.
Any message begins with "step" must be checked for application data
"""
def handle_in("step" <> _ = topic, payload, socket) do
check_application(topic, payload, socket)
end
def handle_in(topic, payload, socket) do
proceed_incoming(topic, payload, socket)
end
# Add authorization logic here as required.
defp authorized?(_payload, socket) do
Sessions.valid?(socket.assigns.connection.session_id) # and Security.access_allowed?(connection)
end
end
And here is the UnknownMessage
module just to explain idea:
defmodule ProfileWeb.CustomerChannel.UnknownMessage do
@moduledoc """
This module handles unknown topic/message
It must be the last @before_compile
"""
defmacro __before_compile__(_env) do
quote do
alias Profile.Config
alias Phoenix.Socket.Broadcast
# Must be the last handle_in!
# Unknown topic or payload format
def proceed_incoming(_topic, _payload, socket) do
{:reply, {:error, %{"status" => "400", "reason" => "bad request"}}, socket}
end
# handle personalized messages
def handle_info(%Broadcast{topic: _, event: ev, payload: payload}, socket) do
push socket, ev, payload
{:noreply, socket}
end
end
end
end