%%% @author Niclas Axelsson %%% @doc %%% Callback controller for handling websockets %%% @end -module(nova_ws_handler). -export([ init/2, terminate/3, websocket_init/1, websocket_handle/2, websocket_info/2 ]). -include_lib("nova/include/nova.hrl"). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. -type nova_ws_state() :: #{controller_data := map(), mod := atom(), subprotocols := [any()], nova_handler := nova_ws_handler, _ := _}. -export_type([nova_ws_state/0]). %%%%%%%%%%%%%%%%%%%%%%%%%%% %% Public functions %% %%%%%%%%%%%%%%%%%%%%%%%%%%% init(Req = #{method := Method}, State = #{entries := Routes}) -> %% Normalize the state. This is a temporary thing and should not be kept, but for now it's a %% quick fix to prove that the new way of routing works :-) case get_route(Method, Routes) of undefined -> Req1 = cowboy_req:reply(405, Req), {ok, Req1, State#{resp_status => 405, req => Req1}}; Route -> NormalizedState = maps:remove(entries, State), NormalizedState0 = maps:merge(NormalizedState, Route), %% Call the http-handler in order to correctly handle potential plugins for the http-request {ok, PrePlugins} = nova_plugin:get_plugins(pre_ws_upgrade), case run_plugins(PrePlugins, pre_ws_upgrade, NormalizedState0) of {ok, State0 = #{controller_data := ControllerData, mod := Mod}} -> ControllerData0 = ControllerData#{req => Req}, upgrade_ws(Mod, Req, State0, ControllerData0); Stop -> Stop end end. upgrade_ws(Module, Req, State, ControllerData) -> case Module:init(ControllerData) of {ok, NewControllerData} -> {cowboy_websocket, Req, State#{controller_data => NewControllerData}}; Error -> ?ERROR("Websocket handler ~p returned unkown result ~p", [Module, Error]), Req1 = cowboy_req:reply(500, Req), {ok, Req1, State} end. websocket_init(State = #{mod := Mod}) -> case erlang:function_exported(Mod, websocket_init, 1) of true -> handle_ws(Mod, websocket_init, [], State); _ -> {ok, State} end. websocket_handle(Frame, State = #{mod := Mod}) -> handle_ws(Mod, websocket_handle, [Frame], State). websocket_info(Msg, State = #{mod := Mod}) -> handle_ws(Mod, websocket_info, [Msg], State). terminate(Reason, PartialReq, State = #{mod := Mod}) -> case erlang:function_exported(Mod, terminate, 3) of true -> handle_ws(Mod, terminate, [Reason, PartialReq], State); _ -> ok end. handle_ws(Mod, Func, Args, State = #{controller_data := _ControllerData}) -> {ok, PrePlugins} = nova_plugin:get_plugins(pre_ws_request), case run_plugins(PrePlugins, pre_ws_request, State) of {ok, State0} -> invoke_controller(Mod, Func, Args, State0); Stop -> Stop end; handle_ws(Mod, Func, Args, State) -> handle_ws(Mod, Func, Args, State#{controller_data => #{}}). invoke_controller(Mod, Func, Args, State = #{controller_data := ControllerData}) -> try ControllerResult = case erlang:apply(Mod, Func, Args ++ [ControllerData]) of {reply, Frame, NewControllerData} -> {reply, Frame, State#{controller_data => NewControllerData}}; {reply, Frame, NewControllerData, hibernate} -> {reply, Frame, State#{controller_data => NewControllerData}, hibernate}; {ok, NewControllerData} -> {ok, State#{controller_data => NewControllerData}}; {ok, NewControllerData, hibernate} -> {ok, State#{controller_data => NewControllerData}, hibernate}; {stop, NewControllerData} -> {stop, State#{controller_data => NewControllerData}}; ok -> {ok, State} end, case ControllerResult of {stop, _} = Stop -> Stop; {_, State0} -> {ok, PostPlugins} = nova_plugin:get_plugins(post_ws_request), run_plugins(PostPlugins, post_ws_request, State0); _ -> ControllerResult end catch Type:Reasons:Stacktrace -> ?ERROR("Websocket failed with ~p:~p.~nStacktrace:~n~p", [Type, Reasons, Stacktrace]), {stop, State} end. run_plugins([], _Callback, State) -> {ok, State}; run_plugins([{_Prio, #{id := Id, module := Module, options := Options}}|Tl], Callback, State) -> try Module:Callback(State, Options) of {ok, State0} -> run_plugins(Tl, Callback, State0); %% Stop indicates that we want the entire pipe of plugins/controller to be stopped. {stop, State0} -> {stop, State0}; %% Break is used to signal that we are stopping further executing of plugins within the same Callback {break, State0} -> {ok, State0}; {error, Reason} -> ?ERROR("Plugin (~p:~p/2) with id: ~p returned error with reason ~p", [Module, Callback, Id, Reason]), {stop, State} catch Type:Reason:Stacktrace -> ?ERROR("Plugin with id: ~p failed in execution. Type: ~p Reason: ~p~nStacktrace:~n~p", [Id, Type, Reason, Stacktrace]), {stop, State} end. get_route(Route, Routes = #{'_' := V}) -> maps:get(Route, Routes, V); get_route(Route, Routes) -> maps:get(Route, Routes, undefined). -ifdef(TEST). -endif.