%%% @doc Trails main interface. %%% Use the functions provided in this module to inteact with `trails'. -module(trails). -export([single_host_compile/1]). -export([compile/1]). -export([trail/2]). -export([trail/3]). -export([trail/4]). -export([trail/5]). -export([trails/1]). -export([path_match/1]). -export([handler/1]). -export([options/1]). -export([metadata/1]). -export([constraints/1]). -export([store/1, store/2]). -export([all/0, all/1, all/2]). -export([retrieve/1, retrieve/2, retrieve/3]). -export([api_root/0, api_root/1]). -export([servers/0]). -export([host_matches/1]). %% Trail specification -opaque trail() :: #{path_match => route_match(), constraints => cowboy:fields(), handler => module(), options => any(), metadata => metadata(any())}. -export_type([trail/0, route_match/0]). %% Exported from cowboy_router.erl -type route_match() :: '_' | iodata(). -type route_path() :: {Path :: route_match(), Handler :: module(), Opts :: any()} | {Path :: route_match(), cowboy:fields(), Handler :: module(), Opts :: any()}. -type route_rule() :: {Host :: route_match(), Paths :: [route_path()]} | {Host :: route_match(), cowboy:fields(), Paths :: [route_path()]}. %% End of exported functions -type trails() :: [trail() | route_path()]. -export_type([trails/0]). -type method() :: get | put | post | delete | patch | head | options. -export_type([method/0]). -type metadata(X) :: #{method() => X}. -export_type([metadata/1]). -elvis([{elvis_style, no_throw, disable}]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% API %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @equiv compile([{'_', Trails}]) -spec single_host_compile(trails()) -> cowboy_router:dispatch_rules(). single_host_compile(Trails) -> compile([{'_', Trails}]). %% @doc Compiles the given list of trails routes, also compatible with %% `cowboy' routes. -spec compile([{Host :: route_match(), Trails :: trails()}]) -> cowboy_router:dispatch_rules(). compile([]) -> []; compile(Routes) -> cowboy_router:compile([{Host, to_route_paths(Trails)} || {Host, Trails} <- Routes]). %% @doc Translates the given trails paths into `cowboy' routes. -spec to_route_paths([trail()]) -> cowboy_router:routes(). to_route_paths(Paths) -> [to_route_path(Path) || Path <- Paths]. %% @doc Translates a trail path into a route rule. -spec to_route_path(trail()) -> route_rule(). to_route_path(Trail) when is_map(Trail) -> ApiRoot = api_root(), PathMatch = maps:get(path_match, Trail), ModuleHandler = maps:get(handler, Trail), Options = maps:get(options, Trail, []), Constraints = maps:get(constraints, Trail, []), {ApiRoot ++ PathMatch, Constraints, ModuleHandler, Options}; to_route_path(Trail) when is_tuple(Trail) -> Trail. %% @equiv trail(PathMatch, ModuleHandler, [], #{}, []) -spec trail(route_match(), module()) -> trail(). trail(PathMatch, ModuleHandler) -> trail(PathMatch, ModuleHandler, [], #{}, []). %% @equiv trail(PathMatch, ModuleHandler, Options, #{}, []) -spec trail(route_match(), module(), any()) -> trail(). trail(PathMatch, ModuleHandler, Options) -> trail(PathMatch, ModuleHandler, Options, #{}, []). %% @equiv trail(PathMatch, ModuleHandler, Options, MetaData, []) -spec trail(route_match(), module(), any(), map()) -> trail(). trail(PathMatch, ModuleHandler, Options, MetaData) -> trail(PathMatch, ModuleHandler, Options, MetaData, []). %% @doc This function allows you to add additional information to the %% `cowboy' handler, such as: resource path, handler module, %% options and metadata. Normally used to document handlers. -spec trail(route_match(), module(), any(), map(), cowboy:fields()) -> trail(). trail(PathMatch, ModuleHandler, Options, MetaData, Constraints) -> #{path_match => PathMatch, handler => ModuleHandler, options => Options, metadata => MetaData, constraints => Constraints}. %% @doc Gets the `path_match' from the given `trail'. -spec path_match(trail()) -> route_match(). path_match(Trail) -> maps:get(path_match, Trail, []). %% @doc Gets the `handler' from the given `trail'. -spec handler(trail()) -> module(). handler(Trail) -> maps:get(handler, Trail, []). %% @doc Gets the `options' from the given `trail'. -spec options(trail()) -> any(). options(Trail) -> maps:get(options, Trail, []). %% @doc Gets the `metadata' from the given `trail'. -spec metadata(trail()) -> map(). metadata(Trail) -> maps:get(metadata, Trail, #{}). %% @doc Gets the `constraints' from the given `trail'. -spec constraints(trail()) -> cowboy:fields(). constraints(Trail) -> maps:get(constraints, Trail, []). %% @doc This function allows you to define the routes on each resource handler, %% instead of defining them all in one place (as you're required to do %% with `cowboy'). Your handler must implement the callback `c:trails_handler:trails/0' %% and return the specific routes for that handler. That callback is %% invoked for each given module and then the results are concatenated. -spec trails(module() | [module()] | {module(), map()} | [{module(), map()}]) -> trails(). trails(Handlers) when is_list(Handlers) -> trails(Handlers, []); trails(Handler) -> trails([Handler], []). %% @doc Store the given list of trails. -spec store(Trails :: trails() | [{HostMatch :: route_match(), Trails :: trails()}]) -> ok. store(Trails) -> store('_', Trails). -spec store(Server :: ranch:ref(), Trails :: trails() | [{HostMatch :: route_match(), Trails :: trails()}]) -> ok. store(_Server, []) -> ok; store(Server, [{HostMatch, Trails} | Hosts]) -> NormalizedPaths = normalize_store_input(Trails), do_store(Server, HostMatch, NormalizedPaths), store(Server, Hosts); store(Server, Trails) -> NormalizedPaths = normalize_store_input(Trails), do_store(Server, '_', NormalizedPaths). %% @doc Retrieves all stored trails. -spec all() -> [trail()]. all() -> all('_'). %% @doc Retrieves all stored trails for the given `HostMatch' -spec all(HostMatch :: route_match()) -> [trail()]. all(HostMatch) -> all('_', HostMatch). %% @doc Retrieves all stored trails for the given `Server' and `HostMatch' -spec all(Server :: ranch:ref(), HostMatch :: route_match()) -> [trail()]. all(Server, HostMatch) -> case application:get_application(trails) of {ok, trails} -> MatchSpec = case {Server, HostMatch} of {'_', HostMatch} -> [{{{'$1', HostMatch, '$2'}, '$3'}, [], ['$$']}]; {Server, HostMatch} -> [{{{Server, HostMatch, '$1'}, '$2'}, [], ['$$']}] end, Matches = ets:select(trails, MatchSpec), FoundServers = [Srvr || [Srvr, _PathMatch, _Trail] <- Matches], % Extract unique elements Servers = lists:usort(FoundServers), % There should be no more than one element in this list. % If there is more than one, it means the same trail is defined % in other host(s). case Servers of [_, _ | _] -> throw(multiple_servers); _ -> ok end, Trails = all_trails(Matches, []), SortIdFun = fun(A, B) -> maps:get(trails_id, A) < maps:get(trails_id, B) end, SortedStoredTrails = lists:sort(SortIdFun, Trails), lists:map(fun remove_id/1, SortedStoredTrails); _ -> throw({not_started, trails}) end. %% @doc Fetch the trail that matches with the given path. -spec retrieve(PathMatch :: string()) -> trail() | notfound. retrieve(PathMatch) -> retrieve('_', PathMatch). %% @doc Fetch the trail that matches with the given host and path. -spec retrieve(HostMatch :: route_match(), PathMatch :: string()) -> trail() | notfound. retrieve(HostMatch, PathMatch) -> retrieve('_', HostMatch, PathMatch). %% @doc Fetch the trail that matches with the given server and host and path. -spec retrieve(Server :: ranch:ref(), HostMatch :: route_match(), PathMatch :: string()) -> trail() | notfound. retrieve(Server, HostMatch, PathMatch) -> case application:get_application(trails) of {ok, trails} -> Key = {Server, HostMatch, PathMatch}, case ets:select(trails, [{{Key, '$1'}, [], ['$$']}]) of % No elements found [] -> notfound; % One element found [[Trail = #{path_match := PathMatch}]] -> remove_id(Trail); % More than one element found [_, _ | _] -> throw(multiple_trails) end; _ -> throw({not_started, trails}) end. %% @doc Get api_root env param value if any, empty otherwise. -spec api_root() -> string(). api_root() -> application:get_env(trails, api_root, ""). %% @doc Set api_root env param to the given Path. -spec api_root(string()) -> ok. api_root(Path) -> application:set_env(trails, api_root, Path). -spec servers() -> [ranch:ref()]. servers() -> lists:flatten( ets:match(ranch_server, {{conns_sup, '$1', '_'}, '_'})). -spec host_matches(ranch:ref()) -> [route_match()]. host_matches(ServerRef) -> [Opts] = lists:flatten( ets:match(ranch_server, {{proto_opts, ServerRef}, '$1'})), Env = maps:get(env, Opts, #{}), Dispatchs = maps:get(dispatch, Env, []), lists:flatten([Host || {Host, _, _} <- Dispatchs]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Private API. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @private trails([], Acc) -> Acc; trails([Module | T], Acc) -> trails(T, Acc ++ trails_handler:trails(Module)). %% @private -spec do_store(Server :: ranch:ref(), HostMatch :: route_match(), Trails :: [route_path()]) -> ok. do_store(_Server, _HostMatch, []) -> ok; do_store(Server, HostMatch, [Trail = #{path_match := PathMatch} | Trails]) -> ets:insert(trails, {{Server, HostMatch, PathMatch}, Trail}), do_store(Server, HostMatch, Trails). %% @private all_trails([], Acc) -> Acc; all_trails([[_, Trail] | T], Acc) -> all_trails(T, [Trail | Acc]); all_trails([[_Server, _PathMatch, Trail] | T], Acc) -> all_trails(T, [Trail | Acc]). %% @private -spec normalize_store_input(trails()) -> [map()]. normalize_store_input(RoutesPaths) -> normalize_id(normalize_paths(RoutesPaths)). -spec normalize_id([route_path()]) -> [map()]. normalize_id(Trails) -> Length = length(Trails), AddIdFun = fun(Trail, Id) -> Trail#{trails_id => Id} end, lists:zipwith(AddIdFun, Trails, lists:seq(1, Length)). %% @private -spec normalize_paths(trails()) -> [trail()]. normalize_paths(RoutesPaths) -> [normalize_path(Path) || Path <- RoutesPaths]. %% @private -spec remove_id(trail()) -> trail(). remove_id(Trail) -> maps:remove(trails_id, Trail). %% @private -spec normalize_path(route_path() | trail()) -> trail(). normalize_path({PathMatch, ModuleHandler, Options}) -> trail(PathMatch, ModuleHandler, Options); normalize_path({PathMatch, Constraints, ModuleHandler, Options}) -> trail(PathMatch, ModuleHandler, Options, #{}, Constraints); normalize_path(Trail) when is_map(Trail) -> Trail.