Packages

Join URL path segments and normalize the result.

Current section

Files

Jump to
url_join src url_join.erl
Raw

src/url_join.erl

-module(url_join).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/url_join.gleam").
-export([join/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(
" Join URL path segments and normalize the result.\n"
"\n"
" A simple and clean library for combining URL segments into a properly\n"
" formatted URL. Handles protocols, slashes, query parameters, and hash\n"
" fragments automatically.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import url_join\n"
"\n"
" let url = url_join.join([\n"
" \"https://example.com\",\n"
" \"api\",\n"
" \"v1\",\n"
" \"users\",\n"
" \"?page=1\",\n"
" ])\n"
" // -> \"https://example.com/api/v1/users?page=1\"\n"
" ```\n"
).
-file("src/url_join.gleam", 96).
-spec only_slashes_or_empty(binary()) -> boolean().
only_slashes_or_empty(S) ->
gleam@string:replace(S, <<"/"/utf8>>, <<""/utf8>>) =:= <<""/utf8>>.
-file("src/url_join.gleam", 88).
-spec is_plain_protocol(binary()) -> boolean().
is_plain_protocol(S) ->
case gleam@string:split_once(S, <<":"/utf8>>) of
{ok, {Before, After}} ->
not gleam_stdlib:contains_string(Before, <<"/"/utf8>>) andalso only_slashes_or_empty(
After
);
{error, _} ->
false
end.
-file("src/url_join.gleam", 129).
-spec drop_leading_slashes(binary()) -> binary().
drop_leading_slashes(S) ->
case gleam_stdlib:string_starts_with(S, <<"/"/utf8>>) of
true ->
drop_leading_slashes(gleam@string:drop_start(S, 1));
false ->
S
end.
-file("src/url_join.gleam", 104).
-spec normalize_protocol(binary()) -> binary().
normalize_protocol(S) ->
Is_ipv6 = gleam_stdlib:string_starts_with(S, <<"["/utf8>>) andalso gleam_stdlib:contains_string(
S,
<<"]"/utf8>>
),
Has_full_protocol = gleam_stdlib:contains_string(S, <<"://"/utf8>>),
case S of
<<"file:///"/utf8, _/binary>> ->
S;
<<"file:"/utf8, Rest/binary>> ->
<<"file:///"/utf8, (drop_leading_slashes(Rest))/binary>>;
_ ->
case Is_ipv6 orelse Has_full_protocol of
true ->
S;
false ->
case gleam@string:split_once(S, <<":"/utf8>>) of
{ok, {Protocol, Rest@1}} ->
<<<<Protocol/binary, "://"/utf8>>/binary,
(drop_leading_slashes(Rest@1))/binary>>;
{error, _} ->
S
end
end
end.
-file("src/url_join.gleam", 76).
-spec merge_leading_parts(binary(), list(binary())) -> list(binary()).
merge_leading_parts(First, Rest) ->
case Rest of
[] ->
[normalize_protocol(First)];
[Next | Tail] ->
case (First =:= <<"/"/utf8>>) orelse is_plain_protocol(First) of
true ->
merge_leading_parts(<<First/binary, Next/binary>>, Tail);
false ->
[normalize_protocol(First) | Rest]
end
end.
-file("src/url_join.gleam", 136).
-spec drop_trailing_slashes(binary()) -> binary().
drop_trailing_slashes(S) ->
case gleam_stdlib:string_ends_with(S, <<"/"/utf8>>) of
true ->
drop_trailing_slashes(gleam@string:drop_end(S, 1));
false ->
S
end.
-file("src/url_join.gleam", 143).
-spec collapse_trailing_slashes(binary()) -> binary().
collapse_trailing_slashes(S) ->
Trimmed = drop_trailing_slashes(S),
case gleam_stdlib:string_ends_with(S, <<"/"/utf8>>) andalso (Trimmed /= <<""/utf8>>) of
true ->
<<Trimmed/binary, "/"/utf8>>;
false ->
Trimmed
end.
-file("src/url_join.gleam", 156).
-spec process_parts(list(binary())) -> list(binary()).
process_parts(Parts) ->
Len = erlang:length(Parts),
_pipe = Parts,
_pipe@1 = gleam@list:index_map(
_pipe,
fun(Part, Index) ->
Is_first = Index =:= 0,
Is_last = Index =:= (Len - 1),
No_leading = case Is_first of
true ->
Part;
false ->
drop_leading_slashes(Part)
end,
case Is_last of
true ->
collapse_trailing_slashes(No_leading);
false ->
drop_trailing_slashes(No_leading)
end
end
),
gleam@list:filter(_pipe@1, fun(P) -> P /= <<""/utf8>> end).
-file("src/url_join.gleam", 183).
-spec join_two_parts(binary(), binary()) -> binary().
join_two_parts(Prev, Part) ->
case gleam_stdlib:string_ends_with(Prev, <<"?"/utf8>>) orelse gleam_stdlib:string_ends_with(
Prev,
<<"#"/utf8>>
) of
true ->
<<Prev/binary, Part/binary>>;
false ->
<<<<Prev/binary, "/"/utf8>>/binary, Part/binary>>
end.
-file("src/url_join.gleam", 176).
-spec join_parts(list(binary())) -> binary().
join_parts(Parts) ->
case Parts of
[] ->
<<""/utf8>>;
[First | Rest] ->
gleam@list:fold(Rest, First, fun join_two_parts/2)
end.
-file("src/url_join.gleam", 223).
-spec split_query_parts(binary()) -> list(binary()).
split_query_parts(S) ->
_pipe = S,
_pipe@1 = gleam@string:replace(_pipe, <<"?"/utf8>>, <<"&"/utf8>>),
_pipe@2 = gleam@string:split(_pipe@1, <<"&"/utf8>>),
gleam@list:filter(_pipe@2, fun(X) -> X /= <<""/utf8>> end).
-file("src/url_join.gleam", 215).
-spec build_query(binary()) -> binary().
build_query(S) ->
case split_query_parts(S) of
[] ->
S;
[Path] ->
Path;
[Path@1 | Params] ->
<<<<Path@1/binary, "?"/utf8>>/binary,
(gleam@string:join(Params, <<"&"/utf8>>))/binary>>
end.
-file("src/url_join.gleam", 202).
-spec normalize_query_params(binary()) -> binary().
normalize_query_params(S) ->
case gleam@string:split_once(S, <<"#"/utf8>>) of
{ok, {Before_hash, After_hash}} ->
Query = build_query(Before_hash),
case After_hash of
<<""/utf8>> ->
Query;
_ ->
<<<<Query/binary, "#"/utf8>>/binary, After_hash/binary>>
end;
{error, _} ->
build_query(S)
end.
-file("src/url_join.gleam", 194).
-spec normalize_query(binary()) -> binary().
normalize_query(S) ->
_pipe = S,
_pipe@1 = gleam@string:replace(_pipe, <<"/?"/utf8>>, <<"?"/utf8>>),
_pipe@2 = gleam@string:replace(_pipe@1, <<"/#"/utf8>>, <<"#"/utf8>>),
_pipe@3 = gleam@string:replace(_pipe@2, <<"/&"/utf8>>, <<"&"/utf8>>),
normalize_query_params(_pipe@3).
-file("src/url_join.gleam", 60).
-spec normalize(list(binary())) -> binary().
normalize(Segments) ->
case Segments of
[] ->
<<""/utf8>>;
[First | Rest] ->
_pipe = First,
_pipe@1 = merge_leading_parts(_pipe, Rest),
_pipe@2 = process_parts(_pipe@1),
_pipe@3 = join_parts(_pipe@2),
normalize_query(_pipe@3)
end.
-file("src/url_join.gleam", 54).
?DOC(
" Join URL path segments and normalize the result.\n"
"\n"
" This function takes a list of URL segments and combines them into a single,\n"
" properly normalized URL. It handles protocols, slashes, query strings, and\n"
" hash fragments intelligently.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" join([\"http://www.google.com\", \"a\", \"/b/cd\", \"?foo=123\"])\n"
" // -> \"http://www.google.com/a/b/cd?foo=123\"\n"
"\n"
" join([\"https://example.com/\", \"/api/\", \"users\"])\n"
" // -> \"https://example.com/api/users\"\n"
"\n"
" join([\"a\", \"b\", \"c\"])\n"
" // -> \"a/b/c\"\n"
" ```\n"
"\n"
" ## Behaviour\n"
"\n"
" - Filters out empty segments\n"
" - Merges a leading bare protocol (e.g. `\"http:\"`) with the next segment\n"
" - Merges a leading `\"/\"` with the next segment \n"
" - Normalises protocols to `protocol://` (and `file:///` for file)\n"
" - Trims and collapses slashes between segments\n"
" - Does not add a slash before `?`, `#`, or `&`\n"
" - Normalises query string (multiple `?`/`&` become a single `?` then `&`)\n"
).
-spec join(list(binary())) -> binary().
join(Parts) ->
_pipe = Parts,
_pipe@1 = gleam@list:filter(_pipe, fun(Part) -> Part /= <<""/utf8>> end),
normalize(_pipe@1).