Skip to main content

src/http_server_mock@matcher.erl

-module(http_server_mock@matcher).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/http_server_mock/matcher.gleam").
-export([new/0, method/2, path/2, path_matching/2, path_contains/2, query_param/3, query_param_matching/3, header/3, header_matching/3, body_equal_to/2, body_containing/2, body_json/2, body_matcher/2, apply_string_matcher/2, matches/2]).

-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(
    " Builder for `RequestMatcher` — the rules that decide whether an incoming\n"
    " request should be handled by a given stub.\n"
    "\n"
    " Start with `new()` and pipe through the constraint functions you need.\n"
    " Constraints that are not set match anything, so a `new()` with no\n"
    " constraints will match every request.\n"
    "\n"
    " ```gleam\n"
    " let m =\n"
    "   matcher.new()\n"
    "   |> matcher.method(http.Post)\n"
    "   |> matcher.path(\"/orders\")\n"
    "   |> matcher.header(\"x-api-key\", \"secret\")\n"
    "   |> matcher.body_json(\"{\\\"amount\\\":100}\")\n"
    " ```\n"
).

-file("src/http_server_mock/matcher.gleam", 29).
?DOC(" Returns a new `RequestMatcher` with no constraints — matches every request.\n").
-spec new() -> http_server_mock@types:request_matcher().
new() ->
    {request_matcher, none, none, [], [], any_body}.

-file("src/http_server_mock/matcher.gleam", 40).
?DOC(" Constrains the matcher to only match requests with the given HTTP method.\n").
-spec method(http_server_mock@types:request_matcher(), gleam@http:method()) -> http_server_mock@types:request_matcher().
method(Request_matcher, Method) ->
    {request_matcher,
        {some, Method},
        erlang:element(3, Request_matcher),
        erlang:element(4, Request_matcher),
        erlang:element(5, Request_matcher),
        erlang:element(6, Request_matcher)}.

-file("src/http_server_mock/matcher.gleam", 51).
?DOC(
    " Constrains the matcher to only match requests whose path is exactly equal\n"
    " to `path`.\n"
    "\n"
    " Use `path_matching` or `path_contains` for partial matches.\n"
).
-spec path(http_server_mock@types:request_matcher(), binary()) -> http_server_mock@types:request_matcher().
path(Request_matcher, Path) ->
    {request_matcher,
        erlang:element(2, Request_matcher),
        {some, {exact, Path}},
        erlang:element(4, Request_matcher),
        erlang:element(5, Request_matcher),
        erlang:element(6, Request_matcher)}.

-file("src/http_server_mock/matcher.gleam", 60).
?DOC(
    " Constrains the matcher to only match requests whose path satisfies the\n"
    " given `StringMatcher`.\n"
    "\n"
    " Use this when you need `Contains`, `Prefix`, or `Suffix` path matching\n"
    " instead of an exact match.\n"
).
-spec path_matching(
    http_server_mock@types:request_matcher(),
    http_server_mock@types:string_matcher()
) -> http_server_mock@types:request_matcher().
path_matching(Request_matcher, String_matcher) ->
    {request_matcher,
        erlang:element(2, Request_matcher),
        {some, String_matcher},
        erlang:element(4, Request_matcher),
        erlang:element(5, Request_matcher),
        erlang:element(6, Request_matcher)}.

-file("src/http_server_mock/matcher.gleam", 69).
?DOC(
    " Constrains the matcher to only match requests whose path contains\n"
    " `fragment` as a substring.\n"
).
-spec path_contains(http_server_mock@types:request_matcher(), binary()) -> http_server_mock@types:request_matcher().
path_contains(Request_matcher, Fragment) ->
    {request_matcher,
        erlang:element(2, Request_matcher),
        {some, {contains, Fragment}},
        erlang:element(4, Request_matcher),
        erlang:element(5, Request_matcher),
        erlang:element(6, Request_matcher)}.

-file("src/http_server_mock/matcher.gleam", 80).
?DOC(
    " Constrains the matcher to only match requests that have the query parameter\n"
    " `key` set to exactly `value`.\n"
    "\n"
    " Can be called multiple times to require several query parameters.\n"
).
-spec query_param(http_server_mock@types:request_matcher(), binary(), binary()) -> http_server_mock@types:request_matcher().
query_param(Request_matcher, Key, Value) ->
    {request_matcher,
        erlang:element(2, Request_matcher),
        erlang:element(3, Request_matcher),
        [{Key, {exact, Value}} | erlang:element(4, Request_matcher)],
        erlang:element(5, Request_matcher),
        erlang:element(6, Request_matcher)}.

-file("src/http_server_mock/matcher.gleam", 95).
?DOC(
    " Constrains the matcher to only match requests that have the query parameter\n"
    " `key` satisfying the given `StringMatcher`.\n"
    "\n"
    " Can be called multiple times to require several query parameters.\n"
).
-spec query_param_matching(
    http_server_mock@types:request_matcher(),
    binary(),
    http_server_mock@types:string_matcher()
) -> http_server_mock@types:request_matcher().
query_param_matching(Request_matcher, Key, String_matcher) ->
    {request_matcher,
        erlang:element(2, Request_matcher),
        erlang:element(3, Request_matcher),
        [{Key, String_matcher} | erlang:element(4, Request_matcher)],
        erlang:element(5, Request_matcher),
        erlang:element(6, Request_matcher)}.

-file("src/http_server_mock/matcher.gleam", 110).
?DOC(
    " Constrains the matcher to only match requests that have the header `key`\n"
    " set to exactly `value`. Header names are compared case-insensitively.\n"
    "\n"
    " Can be called multiple times to require several headers.\n"
).
-spec header(http_server_mock@types:request_matcher(), binary(), binary()) -> http_server_mock@types:request_matcher().
header(Request_matcher, Key, Value) ->
    {request_matcher,
        erlang:element(2, Request_matcher),
        erlang:element(3, Request_matcher),
        erlang:element(4, Request_matcher),
        [{string:lowercase(Key), {exact, Value}} |
            erlang:element(5, Request_matcher)],
        erlang:element(6, Request_matcher)}.

-file("src/http_server_mock/matcher.gleam", 126).
?DOC(
    " Constrains the matcher to only match requests that have the header `key`\n"
    " satisfying the given `StringMatcher`. Header names are compared\n"
    " case-insensitively.\n"
    "\n"
    " Can be called multiple times to require several headers.\n"
).
-spec header_matching(
    http_server_mock@types:request_matcher(),
    binary(),
    http_server_mock@types:string_matcher()
) -> http_server_mock@types:request_matcher().
header_matching(Request_matcher, Key, String_matcher) ->
    {request_matcher,
        erlang:element(2, Request_matcher),
        erlang:element(3, Request_matcher),
        erlang:element(4, Request_matcher),
        [{string:lowercase(Key), String_matcher} |
            erlang:element(5, Request_matcher)],
        erlang:element(6, Request_matcher)}.

-file("src/http_server_mock/matcher.gleam", 139).
?DOC(
    " Constrains the matcher to only match requests whose body is exactly equal\n"
    " to `body`.\n"
).
-spec body_equal_to(http_server_mock@types:request_matcher(), binary()) -> http_server_mock@types:request_matcher().
body_equal_to(Request_matcher, Body) ->
    {request_matcher,
        erlang:element(2, Request_matcher),
        erlang:element(3, Request_matcher),
        erlang:element(4, Request_matcher),
        erlang:element(5, Request_matcher),
        {exact_body, Body}}.

-file("src/http_server_mock/matcher.gleam", 148).
?DOC(
    " Constrains the matcher to only match requests whose body contains\n"
    " `fragment` as a substring.\n"
).
-spec body_containing(http_server_mock@types:request_matcher(), binary()) -> http_server_mock@types:request_matcher().
body_containing(Request_matcher, Fragment) ->
    {request_matcher,
        erlang:element(2, Request_matcher),
        erlang:element(3, Request_matcher),
        erlang:element(4, Request_matcher),
        erlang:element(5, Request_matcher),
        {contains_body, Fragment}}.

-file("src/http_server_mock/matcher.gleam", 158).
?DOC(
    " Constrains the matcher to only match requests whose body is semantically\n"
    " equal to `json` when both are parsed as JSON (whitespace and key order are\n"
    " ignored).\n"
).
-spec body_json(http_server_mock@types:request_matcher(), binary()) -> http_server_mock@types:request_matcher().
body_json(Request_matcher, Json) ->
    {request_matcher,
        erlang:element(2, Request_matcher),
        erlang:element(3, Request_matcher),
        erlang:element(4, Request_matcher),
        erlang:element(5, Request_matcher),
        {json_body, Json}}.

-file("src/http_server_mock/matcher.gleam", 169).
?DOC(
    " Constrains the matcher using a custom `BodyMatcher`.\n"
    "\n"
    " Use this when none of the convenience functions (`body_equal_to`,\n"
    " `body_containing`, `body_json`) cover your use case.\n"
).
-spec body_matcher(
    http_server_mock@types:request_matcher(),
    http_server_mock@types:body_matcher()
) -> http_server_mock@types:request_matcher().
body_matcher(Request_matcher, Body_matcher) ->
    {request_matcher,
        erlang:element(2, Request_matcher),
        erlang:element(3, Request_matcher),
        erlang:element(4, Request_matcher),
        erlang:element(5, Request_matcher),
        Body_matcher}.

-file("src/http_server_mock/matcher.gleam", 282).
-spec normalize_json(binary()) -> binary().
normalize_json(Json_string) ->
    _pipe = Json_string,
    _pipe@1 = gleam@string:replace(_pipe, <<" "/utf8>>, <<""/utf8>>),
    _pipe@2 = gleam@string:replace(_pipe@1, <<"\n"/utf8>>, <<""/utf8>>),
    _pipe@3 = gleam@string:replace(_pipe@2, <<"\t"/utf8>>, <<""/utf8>>),
    gleam@string:replace(_pipe@3, <<"\r"/utf8>>, <<""/utf8>>).

-file("src/http_server_mock/matcher.gleam", 241).
-spec body_matches(http_server_mock@types:body_matcher(), binary()) -> boolean().
body_matches(Expected, Actual) ->
    case Expected of
        any_body ->
            true;

        {exact_body, Body} ->
            Actual =:= Body;

        {contains_body, Fragment} ->
            gleam_stdlib:contains_string(Actual, Fragment);

        {json_body, Expected_json} ->
            normalize_json(Actual) =:= normalize_json(Expected_json)
    end.

-file("src/http_server_mock/matcher.gleam", 254).
?DOC(
    " Applies a `StringMatcher` to `value`, returning `True` if it matches.\n"
    "\n"
    " Exposed for use in custom filtering logic over `recorded_requests`.\n"
).
-spec apply_string_matcher(http_server_mock@types:string_matcher(), binary()) -> boolean().
apply_string_matcher(String_matcher, Value) ->
    case String_matcher of
        {exact, Expected} ->
            Value =:= Expected;

        {contains, Fragment} ->
            gleam_stdlib:contains_string(Value, Fragment);

        {prefix, Prefix} ->
            gleam_stdlib:string_starts_with(Value, Prefix);

        {suffix, Suffix} ->
            gleam_stdlib:string_ends_with(Value, Suffix);

        any_string ->
            true
    end.

-file("src/http_server_mock/matcher.gleam", 228).
-spec headers_match(
    list({binary(), http_server_mock@types:string_matcher()}),
    gleam@dict:dict(binary(), binary())
) -> boolean().
headers_match(Expected, Actual) ->
    gleam@list:all(
        Expected,
        fun(Header_pair) ->
            {Key, String_matcher} = Header_pair,
            case gleam_stdlib:map_get(Actual, string:lowercase(Key)) of
                {ok, Value} ->
                    apply_string_matcher(String_matcher, Value);

                {error, nil} ->
                    String_matcher =:= any_string
            end
        end
    ).

-file("src/http_server_mock/matcher.gleam", 267).
-spec parse_query(gleam@option:option(binary())) -> list({binary(), binary()}).
parse_query(Query_string) ->
    case Query_string of
        none ->
            [];

        {some, Query} ->
            _pipe = Query,
            _pipe@1 = gleam@string:split(_pipe, <<"&"/utf8>>),
            gleam@list:filter_map(
                _pipe@1,
                fun(Part) -> case gleam@string:split_once(Part, <<"="/utf8>>) of
                        {ok, {Key, Value}} ->
                            {ok, {Key, Value}};

                        {error, nil} ->
                            {ok, {Part, <<""/utf8>>}}
                    end end
            )
    end.

-file("src/http_server_mock/matcher.gleam", 209).
-spec query_params_match(
    list({binary(), http_server_mock@types:string_matcher()}),
    gleam@option:option(binary())
) -> boolean().
query_params_match(Expected, Query_string) ->
    case Expected of
        [] ->
            true;

        _ ->
            Params = parse_query(Query_string),
            gleam@list:all(
                Expected,
                fun(Query_param_pair) ->
                    {Key, String_matcher} = Query_param_pair,
                    case gleam@list:key_find(Params, Key) of
                        {ok, Value} ->
                            apply_string_matcher(String_matcher, Value);

                        {error, nil} ->
                            String_matcher =:= any_string
                    end
                end
            )
    end.

-file("src/http_server_mock/matcher.gleam", 199).
-spec path_matches(
    gleam@option:option(http_server_mock@types:string_matcher()),
    binary()
) -> boolean().
path_matches(Expected, Actual) ->
    case Expected of
        none ->
            true;

        {some, String_matcher} ->
            apply_string_matcher(String_matcher, Actual)
    end.

-file("src/http_server_mock/matcher.gleam", 192).
-spec method_matches(
    gleam@option:option(gleam@http:method()),
    gleam@http:method()
) -> boolean().
method_matches(Expected, Actual) ->
    case Expected of
        none ->
            true;

        {some, Method} ->
            Method =:= Actual
    end.

-file("src/http_server_mock/matcher.gleam", 181).
?DOC(
    " Returns `True` if `recorded_request` satisfies all constraints on\n"
    " `request_matcher`.\n"
    "\n"
    " This is the same matching logic the server uses internally. You can call it\n"
    " directly when filtering `recorded_requests` for custom assertions.\n"
).
-spec matches(
    http_server_mock@types:request_matcher(),
    http_server_mock@types:recorded_request()
) -> boolean().
matches(Request_matcher, Recorded_request) ->
    (((method_matches(
        erlang:element(2, Request_matcher),
        erlang:element(3, Recorded_request)
    )
    andalso path_matches(
        erlang:element(3, Request_matcher),
        erlang:element(4, Recorded_request)
    ))
    andalso query_params_match(
        erlang:element(4, Request_matcher),
        erlang:element(5, Recorded_request)
    ))
    andalso headers_match(
        erlang:element(5, Request_matcher),
        erlang:element(6, Recorded_request)
    ))
    andalso body_matches(
        erlang:element(6, Request_matcher),
        erlang:element(7, Recorded_request)
    ).