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