-module(oaisp@route).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/oaisp/route.gleam").
-export([openapi/0, get/2, post/2, put/2, patch/2, delete/2, with_openapi/2, documented/5, to_endpoints/1, match/3]).
-export_type([route/1, open_api/0, query_param/0, response_spec/0, matched/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" A route binds a path and method to a handler *and* carries the OpenAPI\n"
" annotations for that endpoint. One list of routes is the single source of\n"
" truth: it drives your running server (via [`match`](#match)) and the\n"
" generated document (via [`to_endpoints`](#to_endpoints)), so the two can't\n"
" drift.\n"
"\n"
" Annotations are a single [`OpenApi`](#OpenApi) record — build the default\n"
" and spread what you need, mirroring the F# `addOpenApi(OpenApiConfig(…))`:\n"
"\n"
" ```gleam\n"
" route.get(\"/plants\", handle_list_plants)\n"
" |> route.with_openapi(OpenApi(\n"
" ..route.openapi(),\n"
" summary: Some(\"ListPlants\"),\n"
" tags: [\"Portfolio\"],\n"
" responses: [ResponseBody(200, type_ref(\"myapp/types\", \"PlantList\"))],\n"
" ))\n"
" ```\n"
"\n"
" oaisp never inspects the handler — `Route` is generic in it — so oaisp gains\n"
" no dependency on wisp, mist, or any server library. `match` returns the\n"
" matched handler and the captured path parameters for *you* to invoke.\n"
).
-opaque route(EKI) :: {route, oaisp@endpoint:endpoint(), EKI}.
-type open_api() :: {open_api,
gleam@option:option(binary()),
gleam@option:option(binary()),
gleam@option:option(binary()),
list(binary()),
list({binary(), oaisp@schema:schema()}),
list(query_param()),
gleam@option:option(oaisp@schema:schema()),
gleam@option:option(oaisp@schema:schema()),
list(response_spec())}.
-type query_param() :: {query_param, binary(), oaisp@schema:schema(), boolean()}.
-type response_spec() :: {response_body, integer(), oaisp@schema:schema()} |
{empty_response, integer(), binary()}.
-type matched(EKJ) :: {matched, EKJ, list({binary(), binary()})}.
-file("src/oaisp/route.gleam", 73).
?DOC(" An empty [`OpenApi`](#OpenApi) annotation — spread it to set fields.\n").
-spec openapi() -> open_api().
openapi() ->
{open_api, none, none, none, [], [], [], none, none, []}.
-file("src/oaisp/route.gleam", 88).
?DOC(" A `GET` route at `path`, served by `handler`.\n").
-spec get(binary(), EKK) -> route(EKK).
get(Path, Handler) ->
{route, oaisp@endpoint:get(Path), Handler}.
-file("src/oaisp/route.gleam", 93).
?DOC(" A `POST` route at `path`, served by `handler`.\n").
-spec post(binary(), EKM) -> route(EKM).
post(Path, Handler) ->
{route, oaisp@endpoint:post(Path), Handler}.
-file("src/oaisp/route.gleam", 98).
?DOC(" A `PUT` route at `path`, served by `handler`.\n").
-spec put(binary(), EKO) -> route(EKO).
put(Path, Handler) ->
{route, oaisp@endpoint:put(Path), Handler}.
-file("src/oaisp/route.gleam", 103).
?DOC(" A `PATCH` route at `path`, served by `handler`.\n").
-spec patch(binary(), EKQ) -> route(EKQ).
patch(Path, Handler) ->
{route, oaisp@endpoint:patch(Path), Handler}.
-file("src/oaisp/route.gleam", 108).
?DOC(" A `DELETE` route at `path`, served by `handler`.\n").
-spec delete(binary(), EKS) -> route(EKS).
delete(Path, Handler) ->
{route, oaisp@endpoint:delete(Path), Handler}.
-file("src/oaisp/route.gleam", 143).
-spec apply_response(oaisp@endpoint:endpoint(), response_spec()) -> oaisp@endpoint:endpoint().
apply_response(Endpoint, Response) ->
case Response of
{response_body, Status, Schema} ->
oaisp@endpoint:with_response(Endpoint, Status, Schema);
{empty_response, Status@1, Description} ->
oaisp@endpoint:with_empty_response(Endpoint, Status@1, Description)
end.
-file("src/oaisp/route.gleam", 132).
-spec set_optional(
oaisp@endpoint:endpoint(),
gleam@option:option(EKX),
fun((oaisp@endpoint:endpoint(), EKX) -> oaisp@endpoint:endpoint())
) -> oaisp@endpoint:endpoint().
set_optional(Endpoint, Value, With) ->
case Value of
{some, Value@1} ->
With(Endpoint, Value@1);
none ->
Endpoint
end.
-file("src/oaisp/route.gleam", 113).
?DOC(" Apply OpenAPI annotations to a route.\n").
-spec with_openapi(route(EKU), open_api()) -> route(EKU).
with_openapi(Route, Config) ->
Annotated = begin
_pipe = erlang:element(2, Route),
_pipe@1 = set_optional(
_pipe,
erlang:element(2, Config),
fun oaisp@endpoint:with_summary/2
),
_pipe@2 = set_optional(
_pipe@1,
erlang:element(3, Config),
fun oaisp@endpoint:with_description/2
),
_pipe@3 = set_optional(
_pipe@2,
erlang:element(4, Config),
fun oaisp@endpoint:with_operation_id/2
),
_pipe@4 = gleam@list:fold(
erlang:element(5, Config),
_pipe@3,
fun oaisp@endpoint:with_tag/2
),
_pipe@5 = gleam@list:fold(
erlang:element(6, Config),
_pipe@4,
fun(Acc, Param) ->
oaisp@endpoint:with_path_param(
Acc,
erlang:element(1, Param),
erlang:element(2, Param)
)
end
),
_pipe@6 = gleam@list:fold(
erlang:element(7, Config),
_pipe@5,
fun(Acc@1, Param@1) ->
oaisp@endpoint:with_query_param(
Acc@1,
erlang:element(2, Param@1),
erlang:element(3, Param@1),
erlang:element(4, Param@1)
)
end
),
_pipe@7 = set_optional(
_pipe@6,
erlang:element(8, Config),
fun oaisp@endpoint:with_query_record/2
),
_pipe@8 = set_optional(
_pipe@7,
erlang:element(9, Config),
fun oaisp@endpoint:with_body/2
),
gleam@list:fold(
erlang:element(10, Config),
_pipe@8,
fun apply_response/2
)
end,
{route, Annotated, erlang:element(3, Route)}.
-file("src/oaisp/route.gleam", 175).
?DOC(
" Document an endpoint in one call — the common case of a summary, some tags,\n"
" path parameters, and responses, without building an [`OpenApi`](#OpenApi)\n"
" record or wrapping fields in `Some`:\n"
"\n"
" ```gleam\n"
" route.get(\"/todos/{id}\", get_todo)\n"
" |> route.documented(\n"
" summary: \"Get a todo by id\",\n"
" tags: [\"todos\"],\n"
" path: [#(\"id\", param.string())],\n"
" responses: [\n"
" ResponseBody(200, type_ref(\"myapp/types\", \"Todo\")),\n"
" ResponseBody(404, type_ref(\"myapp/types\", \"Error\")),\n"
" ],\n"
" )\n"
" ```\n"
"\n"
" It is exactly [`with_openapi`](#with_openapi) over those four fields, so the\n"
" two produce the same endpoint. Reach for the full record when you also need a\n"
" description, an `operationId`, query parameters, a reflected query record, or\n"
" a request body.\n"
).
-spec documented(
route(EKZ),
binary(),
list(binary()),
list({binary(), oaisp@schema:schema()}),
list(response_spec())
) -> route(EKZ).
documented(Route, Summary, Tags, Path, Responses) ->
with_openapi(
Route,
begin
_record = openapi(),
{open_api,
{some, Summary},
erlang:element(3, _record),
erlang:element(4, _record),
Tags,
Path,
erlang:element(7, _record),
erlang:element(8, _record),
erlang:element(9, _record),
Responses}
end
).
-file("src/oaisp/route.gleam", 190).
?DOC(
" The documented endpoints behind these routes — the input to the document\n"
" generator. Drops the handlers.\n"
).
-spec to_endpoints(list(route(any()))) -> list(oaisp@endpoint:endpoint()).
to_endpoints(Routes) ->
gleam@list:map(Routes, fun(Route) -> erlang:element(2, Route) end).
-file("src/oaisp/route.gleam", 244).
-spec path_segments(binary()) -> list(binary()).
path_segments(Path) ->
_pipe = Path,
_pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>),
gleam@list:filter(_pipe@1, fun(Segment) -> Segment /= <<""/utf8>> end).
-file("src/oaisp/route.gleam", 220).
-spec do_match_path(list(binary()), list(binary()), list({binary(), binary()})) -> {ok,
list({binary(), binary()})} |
{error, nil}.
do_match_path(Pattern, Segments, Captured) ->
case {Pattern, Segments} of
{[], []} ->
{ok, lists:reverse(Captured)};
{[Pattern_segment | Pattern_rest], [Segment | Segments_rest]} ->
case oaisp@endpoint:placeholder_name(Pattern_segment) of
{some, Name} ->
do_match_path(
Pattern_rest,
Segments_rest,
[{Name, Segment} | Captured]
);
none ->
case Pattern_segment =:= Segment of
true ->
do_match_path(Pattern_rest, Segments_rest, Captured);
false ->
{error, nil}
end
end;
{_, _} ->
{error, nil}
end.
-file("src/oaisp/route.gleam", 213).
-spec match_path(binary(), list(binary())) -> {ok, list({binary(), binary()})} |
{error, nil}.
match_path(Pattern, Segments) ->
do_match_path(path_segments(Pattern), Segments, []).
-file("src/oaisp/route.gleam", 198).
?DOC(
" Find the first route whose method and path match the request, returning its\n"
" handler and the captured path parameters. `method` is the lower-cased HTTP\n"
" method (`\"get\"`, `\"post\"`, …); `path_segments` is the request path split on\n"
" `/` (e.g. `[\"todos\", \"42\"]`).\n"
).
-spec match(list(route(ELJ)), binary(), list(binary())) -> {ok, matched(ELJ)} |
{error, nil}.
match(Routes, Method, Path_segments) ->
gleam@list:find_map(
Routes,
fun(Route) ->
gleam@bool:guard(
oaisp@endpoint:method_to_string(
oaisp@endpoint:method(erlang:element(2, Route))
)
/= Method,
{error, nil},
fun() ->
_pipe = match_path(
oaisp@endpoint:path(erlang:element(2, Route)),
Path_segments
),
gleam@result:map(
_pipe,
fun(Params) ->
{matched, erlang:element(3, Route), Params}
end
)
end
)
end
).