Skip to main content

src/oaisp@route.erl

-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
    ).