From 179e2dfe6c7953e02127eb5af726df3fe64a8890 Mon Sep 17 00:00:00 2001 From: Javier Garea Date: Wed, 9 Oct 2024 09:43:18 +0200 Subject: [PATCH 1/2] feat: add route to request --- rebar.config | 1 + src/erf.erl | 92 ++++++++++++++++++-- src/erf_conf.erl | 15 ++++ src/erf_http_server/erf_http_server_elli.erl | 16 ++-- 4 files changed, 111 insertions(+), 13 deletions(-) diff --git a/rebar.config b/rebar.config index 659a6c7..402cfe4 100644 --- a/rebar.config +++ b/rebar.config @@ -94,6 +94,7 @@ {gradualizer_opts, [ %% TODO: address {exclude, [ + "src/erf.erl", "src/erf_router.erl" ]} ]}. diff --git a/src/erf.erl b/src/erf.erl index 1a111b7..69ec8fd 100644 --- a/src/erf.erl +++ b/src/erf.erl @@ -31,6 +31,7 @@ %%% EXTERNAL EXPORTS -export([ get_router/1, + match_route/2, reload_conf/2 ]). @@ -81,6 +82,7 @@ headers := [header()], body := body(), peer := undefined | binary(), + route := binary(), context => any() }. -type response() :: { @@ -88,6 +90,7 @@ Headers :: [header()], Body :: body() | {file, binary()} }. +-type route_patterns() :: [{Route :: binary(), RouteRegEx :: re:mp()}]. -type static_dir() :: {dir, binary()}. -type static_file() :: {file, binary()}. -type static_route() :: {Path :: binary(), Resource :: static_file() | static_dir()}. @@ -103,9 +106,14 @@ query_parameter/0, request/0, response/0, + route_patterns/0, static_route/0 ]). +%%% MACROS +-define(URL_ENCODED_STRING_REGEX, <<"(?:[^%]|%[0-9A-Fa-f]{2})+">>). +% from https://rgxdb.com/r/48L3HPJP + %%%----------------------------------------------------------------------------- %%% START/STOP EXPORTS %%%----------------------------------------------------------------------------- @@ -162,6 +170,20 @@ get_router(Name) -> {error, server_not_started} end. +-spec match_route(Name, RawPath) -> Result when + Name :: atom(), + RawPath :: binary(), + Result :: {ok, Route} | {error, Reason}, + Route :: binary(), + Reason :: term(). +match_route(Name, RawPath) -> + case erf_conf:route_patterns(Name) of + {ok, RoutePatterns} -> + match_route_(RawPath, RoutePatterns); + Error -> + Error + end. + -spec reload_conf(Name, Conf) -> Result when Name :: atom(), Conf :: erf_conf:t(), @@ -186,8 +208,13 @@ reload_conf(Name, NewConf) -> SwaggerUI = maps:get(swagger_ui, Conf), case build_router(SpecPath, SpecParser, Callback, StaticRoutes, SwaggerUI) of - {ok, RouterMod, Router} -> - erf_conf:set(Name, Conf#{router_mod => RouterMod, router => Router}), + {ok, RouterMod, Router, API} -> + RoutePatterns = route_patterns(API), + erf_conf:set(Name, Conf#{ + route_patterns => RoutePatterns, + router_mod => RouterMod, + router => Router + }), ok; {error, Reason} -> {error, Reason} @@ -215,8 +242,13 @@ init([Name, RawConf]) -> SwaggerUI = maps:get(swagger_ui, RawErfConf), case build_router(SpecPath, SpecParser, Callback, StaticRoutes, SwaggerUI) of - {ok, RouterMod, Router} -> - ErfConf = RawErfConf#{router_mod => RouterMod, router => Router}, + {ok, RouterMod, Router, API} -> + RoutePatterns = route_patterns(API), + ErfConf = RawErfConf#{ + route_patterns => RoutePatterns, + router_mod => RouterMod, + router => Router + }, ok = erf_conf:set(Name, ErfConf), {HTTPServer, HTTPServerExtraConf} = maps:get( @@ -285,9 +317,10 @@ build_http_server_conf(ErfConf) -> Callback :: module(), StaticRoutes :: [static_route()], SwaggerUI :: boolean(), - Result :: {ok, RouterMod, Router} | {error, Reason}, + Result :: {ok, RouterMod, Router, API} | {error, Reason}, RouterMod :: module(), Router :: erl_syntax:syntaxTree(), + API :: api(), Reason :: term(). build_router(SpecPath, SpecParser, Callback, RawStaticRoutes, SwaggerUI) -> case erf_parser:parse(SpecPath, SpecParser) of @@ -319,10 +352,10 @@ build_router(SpecPath, SpecParser, Callback, RawStaticRoutes, SwaggerUI) -> }), case erf_router:load(Router) of ok -> - {ok, RouterMod, Router}; + {ok, RouterMod, Router, API}; {ok, Warnings} -> log_warnings(Warnings, <<"router generation">>), - {ok, RouterMod, Router}; + {ok, RouterMod, Router, API}; error -> {error, {router_loading_failed, [unknown_error]}}; {error, {Errors, Warnings}} -> @@ -346,3 +379,48 @@ log_warnings(Warnings, Step) -> end, Warnings ). + +-spec match_route_(RawPath, RoutePatterns) -> Result when + RawPath :: binary(), + RoutePatterns :: erf:route_patterns(), + Result :: {ok, Route} | {error, not_found}, + Route :: binary(). +match_route_(_RawPath, []) -> + {error, not_found}; +match_route_(RawPath, [{Route, RouteRegEx} | Routes]) -> + case re:run(RawPath, RouteRegEx) of + nomatch -> + match_route_(RawPath, Routes); + _Otherwise -> + {ok, Route} + end. + +-spec route_patterns(API) -> RoutePatterns when + API :: api(), + RoutePatterns :: route_patterns(). +route_patterns(API) -> + RawRoutes = [maps:get(path, Endpoint) || Endpoint <- maps:get(endpoints, API)], + route_patterns(RawRoutes, []). + +-spec route_patterns(RawRoutes, Acc) -> RoutePatterns when + RawRoutes :: [binary()], + Acc :: list(), + RoutePatterns :: route_patterns(). +route_patterns([], Acc) -> + Acc; +route_patterns([Route | Routes], Acc) -> + RegExParts = lists:map( + fun + (<<"{", _Variable/binary>>) -> + ?URL_ENCODED_STRING_REGEX; + (Part) -> + Part + end, + erlang:tl(string:split(Route, <<"/">>, all)) + ), + RegEx = + <<"^", + (erlang:list_to_binary([ + <<"/">> | lists:join(<<"/">>, RegExParts) + ]))/binary, "$">>, + route_patterns(Routes, [{Route, RegEx} | Acc]). diff --git a/src/erf_conf.erl b/src/erf_conf.erl index 9a2051c..857547c 100644 --- a/src/erf_conf.erl +++ b/src/erf_conf.erl @@ -22,6 +22,7 @@ get/1, preprocess_middlewares/1, postprocess_middlewares/1, + route_patterns/1, router/1, router_mod/1, set/2 @@ -33,6 +34,7 @@ log_level => logger:level(), preprocess_middlewares => [module()], postprocess_middlewares => [module()], + route_patterns => erf:route_patterns(), router => erl_syntax:syntaxTree(), router_mod => module(), spec_path => binary(), @@ -114,6 +116,19 @@ postprocess_middlewares(Name) -> {ok, maps:get(postprocess_middlewares, Conf)} end. +-spec route_patterns(Name) -> Result when + Name :: atom(), + Result :: {ok, RoutePatterns} | {error, not_found}, + RoutePatterns :: erf:route_patterns(). +%% @doc Returns a mapping between the routes and their matching RegEx for a given Name. +route_patterns(Name) -> + case ?MODULE:get(Name) of + {error, not_found} -> + {error, not_found}; + {ok, Conf} -> + {ok, maps:get(route_patterns, Conf)} + end. + -spec router(Name) -> Result when Name :: atom(), Result :: {ok, Router} | {error, not_found}, diff --git a/src/erf_http_server/erf_http_server_elli.erl b/src/erf_http_server/erf_http_server_elli.erl index f356081..2b51461 100644 --- a/src/erf_http_server/erf_http_server_elli.erl +++ b/src/erf_http_server/erf_http_server_elli.erl @@ -74,7 +74,7 @@ start_link(Name, Conf, ExtraConf) -> %% @doc Handles an HTTP request. %% @private handle(ElliRequest, [Name]) -> - ErfRequest = preprocess(ElliRequest), + ErfRequest = preprocess(Name, ElliRequest), ErfResponse = erf_router:handle(Name, ErfRequest), postprocess(ErfRequest, ErfResponse). @@ -90,12 +90,12 @@ handle_event(request_throw, [Request, Exception, Stacktrace], [Name]) -> handle_event(request_error, [Request, Exception, Stacktrace], [Name]) -> {ok, LogLevel} = erf_conf:log_level(Name), ?LOG(LogLevel, "[erf] Request ~p errored with exception ~p.~nStacktrace:~n~p", [ - preprocess(Request), Exception, Stacktrace + preprocess(Name, Request), Exception, Stacktrace ]); handle_event(request_exit, [Request, Exception, Stacktrace], [Name]) -> {ok, LogLevel} = erf_conf:log_level(Name), ?LOG(LogLevel, "[erf] Request ~p exited with exception ~p.~nStacktrace:~n~p", [ - preprocess(Request), Exception, Stacktrace + preprocess(Name, Request), Exception, Stacktrace ]); handle_event(file_error, [ErrorReason], [Name]) -> {ok, LogLevel} = erf_conf:log_level(Name), @@ -155,10 +155,11 @@ postprocess( postprocess(_Request, {Status, RawHeaders, RawBody}) -> {Status, RawHeaders, RawBody}. --spec preprocess(Req) -> Request when +-spec preprocess(Name, Req) -> Request when + Name :: atom(), Req :: elli:req(), Request :: erf:request(). -preprocess(Req) -> +preprocess(Name, Req) -> Scheme = elli_request:scheme(Req), Host = elli_request:host(Req), Port = elli_request:port(Req), @@ -174,6 +175,8 @@ preprocess(Req) -> ElliBody -> ElliBody end, + JoinPath = erlang:list_to_binary([<<"/">> | lists:join(<<"/">>, Path)]), + {ok, Route} = erf:match_route(Name, JoinPath), #{ scheme => Scheme, host => Host, @@ -183,7 +186,8 @@ preprocess(Req) -> query_parameters => QueryParameters, headers => Headers, body => RawBody, - peer => Peer + peer => Peer, + route => Route }. -spec preprocess_method(ElliMethod) -> Result when From 96afea1583ec63ed63127b5bbc31b31b5fe969c5 Mon Sep 17 00:00:00 2001 From: Javier Garea Date: Wed, 9 Oct 2024 10:30:28 +0200 Subject: [PATCH 2/2] fix: handle not_found and static_routes --- src/erf.erl | 33 +++++++++++++++++--- src/erf_http_server/erf_http_server_elli.erl | 8 ++++- test/erf_SUITE.erl | 10 ++++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/erf.erl b/src/erf.erl index 69ec8fd..14e8c4a 100644 --- a/src/erf.erl +++ b/src/erf.erl @@ -209,7 +209,7 @@ reload_conf(Name, NewConf) -> case build_router(SpecPath, SpecParser, Callback, StaticRoutes, SwaggerUI) of {ok, RouterMod, Router, API} -> - RoutePatterns = route_patterns(API), + RoutePatterns = route_patterns(API, StaticRoutes, SwaggerUI), erf_conf:set(Name, Conf#{ route_patterns => RoutePatterns, router_mod => RouterMod, @@ -243,7 +243,7 @@ init([Name, RawConf]) -> case build_router(SpecPath, SpecParser, Callback, StaticRoutes, SwaggerUI) of {ok, RouterMod, Router, API} -> - RoutePatterns = route_patterns(API), + RoutePatterns = route_patterns(API, StaticRoutes, SwaggerUI), ErfConf = RawErfConf#{ route_patterns => RoutePatterns, router_mod => RouterMod, @@ -395,12 +395,35 @@ match_route_(RawPath, [{Route, RouteRegEx} | Routes]) -> {ok, Route} end. --spec route_patterns(API) -> RoutePatterns when +-spec route_patterns(API, StaticRoutes, SwaggerUI) -> RoutePatterns when API :: api(), + StaticRoutes :: [static_route()], + SwaggerUI :: boolean(), RoutePatterns :: route_patterns(). -route_patterns(API) -> +route_patterns(API, StaticRoutes, SwaggerUI) -> + Acc = + lists:map( + fun + ({Path, {file, _ResourcePath}}) -> + {Path, <<"^", Path/binary, "$">>}; + ({Path, {dir, _ResourcePath}}) -> + {Path, <<"^", Path/binary>>} + end, + StaticRoutes + ), + Acc1 = + case SwaggerUI of + true -> + [ + {<<"/swagger">>, <<"^/swagger$">>}, + {<<"/swagger/spec.json">>, <<"^/swagger/spec.json$">>} + | Acc + ]; + _false -> + Acc + end, RawRoutes = [maps:get(path, Endpoint) || Endpoint <- maps:get(endpoints, API)], - route_patterns(RawRoutes, []). + route_patterns(RawRoutes, Acc1). -spec route_patterns(RawRoutes, Acc) -> RoutePatterns when RawRoutes :: [binary()], diff --git a/src/erf_http_server/erf_http_server_elli.erl b/src/erf_http_server/erf_http_server_elli.erl index 2b51461..352f2b3 100644 --- a/src/erf_http_server/erf_http_server_elli.erl +++ b/src/erf_http_server/erf_http_server_elli.erl @@ -176,7 +176,13 @@ preprocess(Name, Req) -> ElliBody end, JoinPath = erlang:list_to_binary([<<"/">> | lists:join(<<"/">>, Path)]), - {ok, Route} = erf:match_route(Name, JoinPath), + Route = + case erf:match_route(Name, JoinPath) of + {ok, R} -> + R; + {error, not_found} -> + JoinPath + end, #{ scheme => Scheme, host => Host, diff --git a/test/erf_SUITE.erl b/test/erf_SUITE.erl index d5db7d9..d0ef7ac 100644 --- a/test/erf_SUITE.erl +++ b/test/erf_SUITE.erl @@ -120,6 +120,16 @@ foo(_Conf) -> ) ), + ?assertMatch( + {ok, {{"HTTP/1.1", 404, "Not Found"}, _Result3Headers, <<>>}}, + httpc:request( + get, + {"http://localhost:8789/1/not_found", []}, + [], + [{body_format, binary}] + ) + ), + ok = erf:stop(erf_server), meck:unload(erf_callback),