src/rally@parser.erl

-module(rally@parser).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/rally/parser.gleam").
-export([parse_page/2, parse_client_context/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(
    " Page module parser using Glance AST.\n"
    " Parses page source files to extract the page contract:\n"
    " custom types (ToServer, ToClient) with full variant/field info,\n"
    " and function presence (server_update, server_init, load, etc.).\n"
).

-file("src/rally/parser.gleam", 204).
-spec has_function(list(glance:definition(glance:function_())), binary()) -> boolean().
has_function(Functions, Name) ->
    gleam@list:any(
        Functions,
        fun(Def) ->
            (erlang:element(3, erlang:element(3, Def)) =:= Name) andalso (erlang:element(
                4,
                erlang:element(3, Def)
            )
            =:= public)
        end
    ).

-file("src/rally/parser.gleam", 229).
?DOC(
    " Detect presence and variant of `pub const page_auth = auth.Required/Optional`.\n"
    " Returns #(has_page_auth, page_auth_required).\n"
).
-spec detect_page_auth(list(glance:definition(glance:constant()))) -> {boolean(),
    boolean()}.
detect_page_auth(Constants) ->
    _pipe = gleam@list:find_map(
        Constants,
        fun(Def) ->
            {definition, _, Constant} = Def,
            case erlang:element(3, Constant) of
                <<"page_auth"/utf8>> ->
                    Is_required = case erlang:element(6, Constant) of
                        {field_access, _, {variable, _, _}, <<"Required"/utf8>>} ->
                            true;

                        _ ->
                            false
                    end,
                    {ok, {true, Is_required}};

                _ ->
                    {error, nil}
            end
        end
    ),
    gleam@result:unwrap(_pipe, {false, false}).

-file("src/rally/parser.gleam", 300).
-spec type_contains_name(glance:type(), binary()) -> boolean().
type_contains_name(T, Name) ->
    case T of
        {named_type, _, N, _, Parameters} ->
            (N =:= Name) orelse gleam@list:any(
                Parameters,
                fun(P) -> type_contains_name(P, Name) end
            );

        {tuple_type, _, Elements} ->
            gleam@list:any(Elements, fun(E) -> type_contains_name(E, Name) end);

        {function_type, _, Parameters@1, Return} ->
            gleam@list:any(
                Parameters@1,
                fun(P@1) -> type_contains_name(P@1, Name) end
            )
            orelse type_contains_name(Return, Name);

        {variable_type, _, _} ->
            false;

        {hole_type, _, _} ->
            false
    end.

-file("src/rally/parser.gleam", 281).
-spec update_returns_client_context_msg(
    list(glance:definition(glance:function_()))
) -> boolean().
update_returns_client_context_msg(Functions) ->
    case gleam@list:find(
        Functions,
        fun(Def) ->
            (erlang:element(3, erlang:element(3, Def)) =:= <<"update"/utf8>>)
            andalso (erlang:element(4, erlang:element(3, Def)) =:= public)
        end
    ) of
        {ok, Def@1} ->
            case erlang:element(6, erlang:element(3, Def@1)) of
                {some, {tuple_type, _, [_, _, Third]}} ->
                    type_contains_name(Third, <<"ClientContextMsg"/utf8>>);

                _ ->
                    false
            end;

        {error, nil} ->
            false
    end.

-file("src/rally/parser.gleam", 182).
?DOC(" Extract the source text of a named public function using AST span positions.\n").
-spec extract_function_source(
    binary(),
    list(glance:definition(glance:function_())),
    binary()
) -> binary().
extract_function_source(Source, Functions, Name) ->
    case gleam@list:find(
        Functions,
        fun(Def) ->
            (erlang:element(3, erlang:element(3, Def)) =:= Name) andalso (erlang:element(
                4,
                erlang:element(3, Def)
            )
            =:= public)
        end
    ) of
        {error, nil} ->
            <<""/utf8>>;

        {ok, Func_def} ->
            {function, {span, Start, End}, _, _, _, _, _} = erlang:element(
                3,
                Func_def
            ),
            case string:length(Source) >= End of
                true ->
                    gleam@string:slice(Source, Start, End - Start);

                false ->
                    <<""/utf8>>
            end
    end.

-file("src/rally/parser.gleam", 249).
?DOC(" Extract parameter names from the `init` function AST.\n").
-spec extract_init_params_from_ast(list(glance:definition(glance:function_()))) -> list(binary()).
extract_init_params_from_ast(Functions) ->
    case gleam@list:find(
        Functions,
        fun(Def) ->
            (erlang:element(3, erlang:element(3, Def)) =:= <<"init"/utf8>>)
            andalso (erlang:element(4, erlang:element(3, Def)) =:= public)
        end
    ) of
        {error, nil} ->
            [];

        {ok, Func_def} ->
            gleam@list:filter_map(
                erlang:element(5, erlang:element(3, Func_def)),
                fun(Param) -> case Param of
                        {function_parameter, {some, Label}, _, _} ->
                            {ok, Label};

                        {function_parameter, none, {named, Name}, {some, _}} ->
                            {ok, Name};

                        _ ->
                            {error, nil}
                    end end
            )
    end.

-file("src/rally/parser.gleam", 220).
-spec has_type_alias(list(glance:definition(glance:type_alias())), binary()) -> boolean().
has_type_alias(Type_aliases, Name) ->
    gleam@list:any(
        Type_aliases,
        fun(Def) -> erlang:element(3, erlang:element(3, Def)) =:= Name end
    ).

-file("src/rally/parser.gleam", 213).
-spec has_custom_type(list(glance:definition(glance:custom_type())), binary()) -> boolean().
has_custom_type(Custom_types, Name) ->
    gleam@list:any(
        Custom_types,
        fun(Def) -> erlang:element(3, erlang:element(3, Def)) =:= Name end
    ).

-file("src/rally/parser.gleam", 146).
?DOC(" Extract all variants of a named custom type, with field type info.\n").
-spec extract_variants(
    glance:module_(),
    binary(),
    libero@glance_type_resolver:type_resolver(),
    binary()
) -> {ok, list(rally@types:variant_info())} | {error, binary()}.
extract_variants(Ast, Type_name, Resolver, Module_path) ->
    case gleam@list:find(
        erlang:element(3, Ast),
        fun(D) -> erlang:element(3, erlang:element(3, D)) =:= Type_name end
    ) of
        {error, nil} ->
            {ok, []};

        {ok, Ct_def} ->
            Custom_type = erlang:element(3, Ct_def),
            gleam@list:try_map(
                erlang:element(7, Custom_type),
                fun(Variant) ->
                    gleam@result:'try'(
                        gleam@list:try_map(
                            erlang:element(3, Variant),
                            fun(Field) ->
                                {Label@1, Type_} = case Field of
                                    {labelled_variant_field, Item, Label} ->
                                        {Label, Item};

                                    {unlabelled_variant_field, Item@1} ->
                                        {<<"value"/utf8>>, Item@1}
                                end,
                                Path = <<<<<<<<Module_path/binary, "."/utf8>>/binary,
                                            Type_name/binary>>/binary,
                                        "."/utf8>>/binary,
                                    Label@1/binary>>,
                                gleam@result:'try'(
                                    libero@glance_type_resolver:type_to_field_type(
                                        Type_,
                                        Resolver,
                                        Module_path,
                                        {reject_unsupported, Path}
                                    ),
                                    fun(Ft) ->
                                        {ok, {variant_field, Label@1, Ft}}
                                    end
                                )
                            end
                        ),
                        fun(Fields) ->
                            {ok,
                                {variant_info,
                                    erlang:element(2, Variant),
                                    Fields}}
                        end
                    )
                end
            )
    end.

-file("src/rally/parser.gleam", 273).
-spec glance_to_string(glance:error()) -> binary().
glance_to_string(Err) ->
    case Err of
        unexpected_end_of_input ->
            <<"unexpected end of input"/utf8>>;

        {unexpected_token, _, Position} ->
            <<"unexpected token at byte offset "/utf8,
                (erlang:integer_to_binary(erlang:element(2, Position)))/binary>>
    end.

-file("src/rally/parser.gleam", 21).
?DOC(
    " Parse a page module source to extract the contract.\n"
    " Uses Glance AST parsing for robust type extraction.\n"
).
-spec parse_page(binary(), binary()) -> {ok, rally@types:page_contract()} |
    {error, binary()}.
parse_page(Source, Module_path) ->
    gleam@result:'try'(
        begin
            _pipe = glance:module(Source),
            gleam@result:map_error(
                _pipe,
                fun(E) ->
                    gleam_stdlib:println_error(
                        <<"Parse error: "/utf8, (glance_to_string(E))/binary>>
                    ),
                    <<"Parse error"/utf8>>
                end
            )
        end,
        fun(Ast) ->
            gleam@result:'try'(
                libero@glance_type_resolver:resolver_from_imports(
                    erlang:element(2, Ast)
                ),
                fun(Resolver) ->
                    gleam@result:'try'(
                        extract_variants(
                            Ast,
                            <<"Model"/utf8>>,
                            Resolver,
                            Module_path
                        ),
                        fun(Model_variants) ->
                            gleam@result:'try'(
                                extract_variants(
                                    Ast,
                                    <<"Msg"/utf8>>,
                                    Resolver,
                                    Module_path
                                ),
                                fun(Msg_variants) ->
                                    Functions_list = erlang:element(6, Ast),
                                    Has_load = has_function(
                                        Functions_list,
                                        <<"load"/utf8>>
                                    ),
                                    Has_init = has_function(
                                        Functions_list,
                                        <<"init"/utf8>>
                                    ),
                                    Has_init_loaded = has_function(
                                        Functions_list,
                                        <<"init_loaded"/utf8>>
                                    ),
                                    Has_model = has_custom_type(
                                        erlang:element(3, Ast),
                                        <<"Model"/utf8>>
                                    )
                                    orelse has_type_alias(
                                        erlang:element(4, Ast),
                                        <<"Model"/utf8>>
                                    ),
                                    Param_names = extract_init_params_from_ast(
                                        Functions_list
                                    ),
                                    View_source = extract_function_source(
                                        Source,
                                        Functions_list,
                                        <<"view"/utf8>>
                                    ),
                                    Init_source = extract_function_source(
                                        Source,
                                        Functions_list,
                                        <<"init"/utf8>>
                                    ),
                                    Update_source = extract_function_source(
                                        Source,
                                        Functions_list,
                                        <<"update"/utf8>>
                                    ),
                                    Updates_client_context = update_returns_client_context_msg(
                                        Functions_list
                                    ),
                                    {Has_page_auth, Page_auth_required} = detect_page_auth(
                                        erlang:element(5, Ast)
                                    ),
                                    Has_authorize = has_function(
                                        Functions_list,
                                        <<"authorize"/utf8>>
                                    ),
                                    {ok,
                                        {page_contract,
                                            Model_variants,
                                            Msg_variants,
                                            Has_load,
                                            Has_init,
                                            Has_init_loaded,
                                            Has_model,
                                            Updates_client_context,
                                            Param_names,
                                            Source,
                                            View_source,
                                            Init_source,
                                            Update_source,
                                            Has_page_auth,
                                            Page_auth_required,
                                            Has_authorize}}
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/rally/parser.gleam", 103).
?DOC(" Parse a client_context.gleam source to extract the contract.\n").
-spec parse_client_context(binary()) -> {ok,
        rally@types:client_context_contract()} |
    {error, binary()}.
parse_client_context(Source) ->
    gleam@result:'try'(
        begin
            _pipe = glance:module(Source),
            gleam@result:map_error(
                _pipe,
                fun(E) ->
                    gleam_stdlib:println_error(
                        <<"Parse error: "/utf8, (glance_to_string(E))/binary>>
                    ),
                    <<"Parse error"/utf8>>
                end
            )
        end,
        fun(Ast) ->
            gleam@result:'try'(
                libero@glance_type_resolver:resolver_from_imports(
                    erlang:element(2, Ast)
                ),
                fun(Resolver) ->
                    gleam@result:'try'(
                        extract_variants(
                            Ast,
                            <<"ClientContext"/utf8>>,
                            Resolver,
                            <<"client_context"/utf8>>
                        ),
                        fun(Context_variants) ->
                            gleam@result:'try'(
                                extract_variants(
                                    Ast,
                                    <<"ClientContextMsg"/utf8>>,
                                    Resolver,
                                    <<"client_context"/utf8>>
                                ),
                                fun(Msg_variants) ->
                                    Functions_list = erlang:element(6, Ast),
                                    Has_init = has_function(
                                        Functions_list,
                                        <<"init"/utf8>>
                                    ),
                                    Has_update = has_function(
                                        Functions_list,
                                        <<"update"/utf8>>
                                    ),
                                    {ok,
                                        {client_context_contract,
                                            Context_variants,
                                            Msg_variants,
                                            Has_init,
                                            Has_update}}
                                end
                            )
                        end
                    )
                end
            )
        end
    ).