src/rally@scanner.erl
-module(rally@scanner).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/rally/scanner.gleam").
-export([scan/1]).
-export_type([scan_acc/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type scan_acc() :: {scan_acc,
list(rally@types:scanned_route()),
list(binary())}.
-file("src/rally/scanner.gleam", 13).
?DOC(" Convert a snake_case name to PascalCase.\n").
-spec to_pascal_case(binary()) -> binary().
to_pascal_case(Name) ->
_pipe = Name,
_pipe@1 = gleam@string:split(_pipe, <<"_"/utf8>>),
_pipe@2 = gleam@list:map(_pipe@1, fun gleam@string:capitalise/1),
gleam@string:join(_pipe@2, <<""/utf8>>).
-file("src/rally/scanner.gleam", 21).
?DOC(" Determine param type: \"id\" or anything ending in \"_id\" -> IntParam, else StringParam.\n").
-spec param_type_for(binary()) -> rally@types:param_type().
param_type_for(Name) ->
case (Name =:= <<"id"/utf8>>) orelse gleam_stdlib:string_ends_with(
Name,
<<"_id"/utf8>>
) of
true ->
int_param;
_ ->
string_param
end.
-file("src/rally/scanner.gleam", 29).
?DOC(" Parse a filename stem (without .gleam extension) into a UrlSegment.\n").
-spec parse_segment(binary()) -> rally@types:url_segment().
parse_segment(Stem) ->
case gleam_stdlib:string_ends_with(Stem, <<"_"/utf8>>) of
true ->
Param_name = gleam@string:drop_end(Stem, 1),
{dynamic_segment, Param_name, param_type_for(Param_name)};
_ ->
{static_segment, Stem}
end.
-file("src/rally/scanner.gleam", 40).
?DOC(" Check for duplicate elements in a list.\n").
-spec has_duplicates(list(binary())) -> boolean().
has_duplicates(Names) ->
case Names of
[] ->
false;
[Name | Rest] ->
case gleam@list:contains(Rest, Name) of
true ->
true;
false ->
has_duplicates(Rest)
end
end.
-file("src/rally/scanner.gleam", 52).
?DOC(" Build a ScannedRoute from a list of UrlSegments and a module path.\n").
-spec build_route(list(rally@types:url_segment()), binary()) -> {ok,
rally@types:scanned_route()} |
{error, binary()}.
build_route(Segments, Module_path) ->
Variant_name = begin
_pipe = Segments,
_pipe@1 = gleam@list:map(_pipe, fun(Seg) -> case Seg of
{static_segment, Name} ->
to_pascal_case(Name);
{dynamic_segment, Param_name, _} ->
to_pascal_case(Param_name)
end end),
gleam@string:join(_pipe@1, <<""/utf8>>)
end,
Params = begin
_pipe@2 = Segments,
gleam@list:filter_map(_pipe@2, fun(Seg@1) -> case Seg@1 of
{dynamic_segment, Param_name@1, Param_type} ->
{ok, {Param_name@1, Param_type}};
{static_segment, _} ->
{error, nil}
end end)
end,
Param_names = gleam@list:map(Params, fun(P) -> erlang:element(1, P) end),
case has_duplicates(Param_names) of
true ->
{error,
<<<<<<"Duplicate dynamic parameter names in route "/utf8,
Module_path/binary>>/binary,
". Use distinct names (e.g. user_id_, post_id_) instead of: "/utf8>>/binary,
(gleam@string:join(Param_names, <<", "/utf8>>))/binary>>};
false ->
{ok,
{scanned_route,
Segments,
Variant_name,
Params,
Module_path,
none}}
end.
-file("src/rally/scanner.gleam", 266).
?DOC(" Derive a Gleam module path from a filesystem path under pages_root.\n").
-spec derive_module_path(binary(), binary()) -> binary().
derive_module_path(Module_prefix, Relative_path) ->
<<<<Module_prefix/binary, "/"/utf8>>/binary, Relative_path/binary>>.
-file("src/rally/scanner.gleam", 101).
?DOC(" Recursively scan a directory, accumulating routes and layout modules.\n").
-spec scan_dir(
binary(),
list(rally@types:url_segment()),
list(binary()),
binary(),
scan_acc()
) -> {ok, scan_acc()} | {error, binary()}.
scan_dir(Path, Prefix_segments, Path_parts, Module_prefix, Acc) ->
gleam@result:'try'(
begin
_pipe = simplifile_erl:read_directory(Path),
gleam@result:map_error(
_pipe,
fun(E) ->
<<<<<<"Failed to read directory "/utf8, Path/binary>>/binary,
": "/utf8>>/binary,
(simplifile:describe_error(E))/binary>>
end
)
end,
fun(Entries) ->
Sorted_entries = gleam@list:sort(
Entries,
fun gleam@string:compare/2
),
gleam@list:try_fold(
Sorted_entries,
Acc,
fun(Acc@1, Entry) ->
Entry_path = <<<<Path/binary, "/"/utf8>>/binary,
Entry/binary>>,
gleam@result:'try'(
begin
_pipe@1 = simplifile_erl:is_directory(Entry_path),
gleam@result:map_error(
_pipe@1,
fun(E@1) ->
<<<<<<"Failed to stat "/utf8,
Entry_path/binary>>/binary,
": "/utf8>>/binary,
(simplifile:describe_error(E@1))/binary>>
end
)
end,
fun(Is_dir) -> case Is_dir of
true ->
case Entry =:= <<"sql"/utf8>> of
true ->
{ok, Acc@1};
false ->
Seg = parse_segment(Entry),
scan_dir(
Entry_path,
lists:append(
Prefix_segments,
[Seg]
),
lists:append(
Path_parts,
[Entry]
),
Module_prefix,
Acc@1
)
end;
false ->
case gleam_stdlib:string_ends_with(
Entry,
<<".gleam"/utf8>>
) of
false ->
{ok, Acc@1};
true ->
Stem = gleam@string:drop_end(
Entry,
string:length(<<".gleam"/utf8>>)
),
Relative_path = gleam@string:join(
lists:append(Path_parts, [Stem]),
<<"/"/utf8>>
),
Module_path = derive_module_path(
Module_prefix,
Relative_path
),
case Stem of
<<"layout"/utf8>> ->
{ok,
{scan_acc,
erlang:element(
2,
Acc@1
),
[Module_path |
erlang:element(
3,
Acc@1
)]}};
<<"index"/utf8>> ->
gleam@result:'try'(
case gleam@list:is_empty(
Prefix_segments
) of
true ->
{ok,
{scanned_route,
[],
<<"Home"/utf8>>,
[],
Module_path,
none}};
false ->
build_route(
Prefix_segments,
Module_path
)
end,
fun(Route) ->
{ok,
{scan_acc,
[Route |
erlang:element(
2,
Acc@1
)],
erlang:element(
3,
Acc@1
)}}
end
);
_ ->
Route@1 = case Stem =:= <<"home_"/utf8>> of
true ->
case gleam@list:is_empty(
Prefix_segments
) of
true ->
{ok,
{scanned_route,
[],
<<"Home"/utf8>>,
[],
Module_path,
none}};
false ->
build_route(
Prefix_segments,
Module_path
)
end;
false ->
Segments = lists:append(
Prefix_segments,
[parse_segment(
Stem
)]
),
build_route(
Segments,
Module_path
)
end,
gleam@result:'try'(
Route@1,
fun(Route@2) ->
{ok,
{scan_acc,
[Route@2 |
erlang:element(
2,
Acc@1
)],
erlang:element(
3,
Acc@1
)}}
end
)
end
end
end end
)
end
)
end
).
-file("src/rally/scanner.gleam", 226).
-spec find_nearest_layout(
list(binary()),
list(binary()),
gleam@set:set(binary())
) -> gleam@option:option(binary()).
find_nearest_layout(Remaining, _, Layout_set) ->
case Remaining of
[] ->
none;
_ ->
Candidate = <<(gleam@string:join(Remaining, <<"/"/utf8>>))/binary,
"/layout"/utf8>>,
case gleam@set:contains(Layout_set, Candidate) of
true ->
{some, Candidate};
false ->
find_nearest_layout(
gleam@list:take(Remaining, erlang:length(Remaining) - 1),
[],
Layout_set
)
end
end.
-file("src/rally/scanner.gleam", 212).
?DOC(
" Resolve the nearest layout module for a page route.\n"
" Walks up the module path, checking if <dir>/layout exists in the layout set.\n"
).
-spec resolve_layout(rally@types:scanned_route(), gleam@set:set(binary())) -> rally@types:scanned_route().
resolve_layout(Route, Layout_set) ->
Parts = gleam@string:split(erlang:element(5, Route), <<"/"/utf8>>),
Dirs = case erlang:length(Parts) of
1 ->
[];
N ->
gleam@list:take(Parts, N - 1)
end,
Layout = find_nearest_layout(Dirs, [], Layout_set),
{scanned_route,
erlang:element(2, Route),
erlang:element(3, Route),
erlang:element(4, Route),
erlang:element(5, Route),
Layout}.
-file("src/rally/scanner.gleam", 270).
-spec route_root_segments(binary()) -> list(rally@types:url_segment()).
route_root_segments(Route_root) ->
_pipe = Route_root,
_pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>),
_pipe@2 = gleam@list:filter(_pipe@1, fun(Part) -> Part /= <<""/utf8>> end),
gleam@list:map(_pipe@2, fun(Field@0) -> {static_segment, Field@0} end).
-file("src/rally/scanner.gleam", 300).
-spec drop_through_src(list(binary())) -> list(binary()).
drop_through_src(Parts) ->
case Parts of
[] ->
[];
[<<"src"/utf8>> | Rest] ->
Rest;
[_ | Rest@1] ->
drop_through_src(Rest@1)
end.
-file("src/rally/scanner.gleam", 289).
-spec split_before_pages(list(binary()), list(binary())) -> {ok, list(binary())} |
{error, nil}.
split_before_pages(Parts, Acc) ->
case Parts of
[] ->
{error, nil};
[<<"pages"/utf8>> | _] ->
{ok, Acc};
[Part | Rest] ->
split_before_pages(Rest, lists:append(Acc, [Part]))
end.
-file("src/rally/scanner.gleam", 277).
-spec derive_module_prefix(binary()) -> binary().
derive_module_prefix(Pages_root) ->
Parts = gleam@string:split(Pages_root, <<"/"/utf8>>),
case split_before_pages(Parts, []) of
{ok, Prefix_parts} ->
case drop_through_src(Prefix_parts) of
[] ->
<<"pages"/utf8>>;
Parts@1 ->
gleam@string:join(
lists:append(Parts@1, [<<"pages"/utf8>>]),
<<"/"/utf8>>
)
end;
{error, nil} ->
<<"pages"/utf8>>
end.
-file("src/rally/scanner.gleam", 249).
?DOC(" Scan a root directory and return all routes found with layout assignments.\n").
-spec scan(rally@types:scan_config()) -> {ok, list(rally@types:scanned_route())} |
{error, binary()}.
scan(Config) ->
Module_prefix = derive_module_prefix(erlang:element(2, Config)),
Route_segments = route_root_segments(erlang:element(14, Config)),
gleam@result:'try'(
scan_dir(
erlang:element(2, Config),
Route_segments,
[],
Module_prefix,
{scan_acc, [], []}
),
fun(Acc) ->
Layout_set = gleam@set:from_list(erlang:element(3, Acc)),
Routes_with_layouts = gleam@list:map(
erlang:element(2, Acc),
fun(Route) -> resolve_layout(Route, Layout_set) end
),
{ok, lists:reverse(Routes_with_layouts)}
end
).