Current section

Files

Jump to
libero src libero@scanner.erl
Raw

src/libero@scanner.erl

-module(libero@scanner).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/libero/scanner.gleam").
-export([walk_directory/1, derive_module_path/1, parse_module/1, scan_excluding/3, scan/2, collect_seeds/1, build_type_import_map/1, build_type_alias_originals/1, build_alias_resolution_map/1]).
-export_type([handler_endpoint/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.
?MODULEDOC(
" Scanning for handler-as-contract endpoints.\n"
"\n"
" Walks a source tree to discover handler functions whose signatures\n"
" define the wire contract: public functions with a `server_` prefix,\n"
" a context parameter, and a Result return type.\n"
).
-type handler_endpoint() :: {handler_endpoint,
binary(),
binary(),
libero@field_type:field_type(),
libero@field_type:field_type(),
list({binary(), libero@field_type:field_type()}),
boolean(),
gleam@option:option({binary(), binary()})}.
-file("src/libero/scanner.gleam", 92).
-spec visit_file(list(binary()), binary(), binary()) -> list(binary()).
visit_file(Acc, Entry, Child) ->
gleam@bool:guard(
not gleam_stdlib:string_ends_with(Entry, <<".gleam"/utf8>>),
Acc,
fun() -> [Child | Acc] end
).
-file("src/libero/scanner.gleam", 82).
-spec visit_subdirectory(list(binary()), binary(), binary()) -> {ok,
list(binary())} |
{error, libero@gen_error:gen_error()}.
visit_subdirectory(Acc, Entry, Child) ->
gleam@bool:guard(
Entry =:= <<"generated"/utf8>>,
{ok, Acc},
fun() ->
gleam@result:'try'(
walk_directory(Child),
fun(Nested) -> {ok, lists:append(Nested, Acc)} end
)
end
).
-file("src/libero/scanner.gleam", 67).
-spec visit_entry(list(binary()), binary(), binary()) -> {ok, list(binary())} |
{error, libero@gen_error:gen_error()}.
visit_entry(Acc, Parent, Entry) ->
Child = <<<<Parent/binary, "/"/utf8>>/binary, Entry/binary>>,
Is_symlink = begin
_pipe = simplifile_erl:is_symlink(Child),
gleam@result:unwrap(_pipe, false)
end,
Is_dir = gleam@result:unwrap(simplifile_erl:is_directory(Child), false),
gleam@bool:guard(
Is_symlink andalso Is_dir,
{ok, Acc},
fun() -> case Is_dir of
true ->
visit_subdirectory(Acc, Entry, Child);
false ->
{ok, visit_file(Acc, Entry, Child)}
end end
).
-file("src/libero/scanner.gleam", 54).
?DOC(
" Recursively walk a directory, returning every `.gleam` file found.\n"
" Skips any subdirectory named `generated`.\n"
).
-spec walk_directory(binary()) -> {ok, list(binary())} |
{error, libero@gen_error:gen_error()}.
walk_directory(Path) ->
gleam@result:'try'(
begin
_pipe = simplifile_erl:read_directory(Path),
gleam@result:map_error(
_pipe,
fun(Cause) -> {cannot_read_dir, Path, Cause} end
)
end,
fun(Entries) ->
gleam@result:map(
gleam@list:try_fold(
Entries,
[],
fun(Acc, Entry) -> visit_entry(Acc, Path, Entry) end
),
fun(Files) ->
gleam@list:sort(Files, fun gleam@string:compare/2)
end
)
end
).
-file("src/libero/scanner.gleam", 106).
?DOC(
" Derive the Gleam module path from a file path by finding the last\n"
" occurrence of `/src/` and taking everything after it, then stripping\n"
" the `.gleam` extension.\n"
).
-spec derive_module_path(binary()) -> binary().
derive_module_path(File_path) ->
Without_extension = case gleam_stdlib:string_ends_with(
File_path,
<<".gleam"/utf8>>
) of
true ->
gleam@string:slice(
File_path,
0,
string:length(File_path) - string:length(<<".gleam"/utf8>>)
);
false ->
File_path
end,
case gleam@string:split(Without_extension, <<"/src/"/utf8>>) of
[_] ->
Without_extension;
Parts ->
_pipe = gleam@list:last(Parts),
gleam@result:unwrap(_pipe, Without_extension)
end.
-file("src/libero/scanner.gleam", 200).
-spec duplicate_fn_name_errors(list(handler_endpoint())) -> list(libero@gen_error:gen_error()).
duplicate_fn_name_errors(Endpoints) ->
By_name = gleam@list:fold(
Endpoints,
maps:new(),
fun(Acc, Ep) ->
Existing = begin
_pipe = gleam_stdlib:map_get(Acc, erlang:element(3, Ep)),
gleam@result:unwrap(_pipe, [])
end,
gleam@dict:insert(
Acc,
erlang:element(3, Ep),
[erlang:element(2, Ep) | Existing]
)
end
),
_pipe@1 = By_name,
_pipe@2 = maps:to_list(_pipe@1),
_pipe@3 = gleam@list:sort(
_pipe@2,
fun(A, B) ->
gleam@string:compare(erlang:element(1, A), erlang:element(1, B))
end
),
gleam@list:filter_map(
_pipe@3,
fun(Pair) ->
{Fn_name, Modules_rev} = Pair,
case Modules_rev of
[_, _ | _] ->
{ok,
{duplicate_endpoint,
Fn_name,
begin
_pipe@4 = lists:reverse(Modules_rev),
gleam@list:unique(_pipe@4)
end}};
_ ->
{error, nil}
end
end
).
-file("src/libero/scanner.gleam", 503).
-spec labelled_fields_to_params(
list(glance:variant_field()),
fun((glance:type()) -> libero@field_type:field_type())
) -> {ok, list({binary(), libero@field_type:field_type()})} | {error, nil}.
labelled_fields_to_params(Fields, To_ft) ->
gleam@list:try_map(Fields, fun(Field) -> case Field of
{labelled_variant_field, Item, Label} ->
{ok, {Label, To_ft(Item)}};
_ ->
{error, nil}
end end).
-file("src/libero/scanner.gleam", 489).
-spec one_variant_fields(
binary(),
list(glance:definition(glance:custom_type()))
) -> {ok, list(glance:variant_field())} | {error, nil}.
one_variant_fields(Type_name, Custom_types) ->
case gleam@list:find(
Custom_types,
fun(Def) -> erlang:element(3, erlang:element(3, Def)) =:= Type_name end
) of
{ok, Def@1} ->
case erlang:element(7, erlang:element(3, Def@1)) of
[Single_variant] ->
{ok, erlang:element(3, Single_variant)};
_ ->
{error, nil}
end;
{error, nil} ->
{error, nil}
end.
-file("src/libero/scanner.gleam", 463).
-spec module_type_resolver(list(glance:definition(glance:import())), binary()) -> {ok,
fun((glance:type()) -> libero@field_type:field_type())} |
{error, nil}.
module_type_resolver(Imports, Current_module) ->
case libero@glance_type_resolver:resolver_from_imports(Imports) of
{ok, Resolver} ->
{ok,
fun(T) ->
case libero@glance_type_resolver:type_to_field_type(
T,
Resolver,
Current_module,
preserve_unsupported
) of
{ok, Ft} ->
Ft;
{error, _} ->
{type_var, <<"_unresolved"/utf8>>}
end
end};
{error, _} ->
{error, nil}
end.
-file("src/libero/scanner.gleam", 226).
?DOC(
" Read a `.gleam` file and parse it via `glance`, surfacing both I/O and\n"
" parser failures as `GenError` variants tagged with the file path.\n"
).
-spec parse_module(binary()) -> {ok, glance:module_()} |
{error, libero@gen_error:gen_error()}.
parse_module(File_path) ->
gleam@result:'try'(
begin
_pipe = simplifile:read(File_path),
gleam@result:map_error(
_pipe,
fun(Cause) -> {cannot_read_file, File_path, Cause} end
)
end,
fun(Content) -> _pipe@1 = glance:module(Content),
gleam@result:map_error(
_pipe@1,
fun(Cause@1) -> {parse_failed, File_path, Cause@1} end
) end
).
-file("src/libero/scanner.gleam", 428).
-spec resolve_msg_type_fields(
binary(),
binary(),
binary(),
list(glance:definition(glance:custom_type())),
gleam@dict:dict(binary(), binary()),
fun((glance:type()) -> libero@field_type:field_type())
) -> {ok, list({binary(), libero@field_type:field_type()})} | {error, nil}.
resolve_msg_type_fields(
Module_path,
Type_name,
Current_module,
Current_custom_types,
Module_files,
Current_to_ft
) ->
case Module_path =:= Current_module of
true ->
gleam@result:'try'(
one_variant_fields(Type_name, Current_custom_types),
fun(Fields) ->
labelled_fields_to_params(Fields, Current_to_ft)
end
);
false ->
gleam@result:'try'(
begin
_pipe = gleam_stdlib:map_get(Module_files, Module_path),
gleam@result:replace_error(_pipe, nil)
end,
fun(File_path) ->
gleam@result:'try'(
begin
_pipe@1 = parse_module(File_path),
gleam@result:replace_error(_pipe@1, nil)
end,
fun(Parsed) ->
gleam@result:'try'(
module_type_resolver(
erlang:element(2, Parsed),
Module_path
),
fun(To_ft) ->
gleam@result:'try'(
one_variant_fields(
Type_name,
erlang:element(3, Parsed)
),
fun(Fields@1) ->
labelled_fields_to_params(
Fields@1,
To_ft
)
end
)
end
)
end
)
end
)
end.
-file("src/libero/scanner.gleam", 420).
-spec try_msg_param_type(
glance:function_parameter(),
JSP,
fun((glance:type()) -> JSP)
) -> JSP.
try_msg_param_type(Param, Fallback, Next) ->
_pipe = erlang:element(4, Param),
_pipe@1 = gleam@option:map(_pipe, Next),
gleam@option:unwrap(_pipe@1, Fallback).
-file("src/libero/scanner.gleam", 515).
-spec resolve_individual_params(
list(glance:function_parameter()),
fun((glance:type()) -> libero@field_type:field_type())
) -> list({binary(), libero@field_type:field_type()}).
resolve_individual_params(Params, To_ft) ->
gleam@list:filter_map(
Params,
fun(P) -> case {erlang:element(2, P), erlang:element(4, P)} of
{{some, Label}, {some, Type_}} ->
{ok, {Label, To_ft(Type_)}};
{_, _} ->
{error, nil}
end end
).
-file("src/libero/scanner.gleam", 385).
-spec try_resolve_msg_type(
list(glance:function_parameter()),
binary(),
list(glance:definition(glance:custom_type())),
gleam@dict:dict(binary(), binary()),
fun((glance:type()) -> libero@field_type:field_type())
) -> {list({binary(), libero@field_type:field_type()}),
gleam@option:option({binary(), binary()})}.
try_resolve_msg_type(
Payload_params,
Current_module,
Custom_types,
Module_files,
To_ft
) ->
Fallback = {resolve_individual_params(Payload_params, To_ft), none},
case Payload_params of
[Param] ->
try_msg_param_type(
Param,
Fallback,
fun(Type_) -> case To_ft(Type_) of
{user_type, Module_path, Type_name, []} ->
_pipe = resolve_msg_type_fields(
Module_path,
Type_name,
Current_module,
Custom_types,
Module_files,
To_ft
),
_pipe@1 = gleam@result:map(
_pipe,
fun(Fields) ->
{Fields, {some, {Module_path, Type_name}}}
end
),
gleam@result:unwrap(_pipe@1, Fallback);
_ ->
Fallback
end end
);
_ ->
Fallback
end.
-file("src/libero/scanner.gleam", 581).
-spec is_context_type(glance:type(), binary()) -> boolean().
is_context_type(T, Context_type_name) ->
case T of
{named_type, _, Name, none, _} ->
Name =:= Context_type_name;
_ ->
false
end.
-file("src/libero/scanner.gleam", 589).
-spec extract_result_args(glance:type()) -> {ok, {glance:type(), glance:type()}} |
{error, nil}.
extract_result_args(T) ->
case T of
{named_type, _, <<"Result"/utf8>>, _, [Ok, Err]} ->
{ok, {Ok, Err}};
_ ->
{error, nil}
end.
-file("src/libero/scanner.gleam", 566).
-spec extract_response_type(glance:type(), binary()) -> {ok,
{glance:type(), boolean()}} |
{error, nil}.
extract_response_type(T, Context_type_name) ->
case T of
{tuple_type, _, [Response, State]} ->
case is_context_type(State, Context_type_name) of
true ->
{ok, {Response, true}};
false ->
{error, nil}
end;
{named_type, _, <<"Result"/utf8>>, _, _} ->
{ok, {T, false}};
_ ->
{error, nil}
end.
-file("src/libero/scanner.gleam", 527).
-spec validate_handler_signature(glance:function_(), binary()) -> {ok,
{glance:type(),
glance:type(),
list(glance:function_parameter()),
boolean()}} |
{error, nil}.
validate_handler_signature(Func, Context_type_name) ->
Params = erlang:element(5, Func),
gleam@bool:guard(
gleam@list:is_empty(Params),
{error, nil},
fun() ->
Has_context = gleam@list:any(
Params,
fun(P) -> case erlang:element(4, P) of
{some, T} ->
is_context_type(T, Context_type_name);
none ->
false
end end
),
gleam@bool:guard(
not Has_context,
{error, nil},
fun() ->
gleam@result:'try'(
gleam@option:to_result(erlang:element(6, Func), nil),
fun(Return_type) ->
gleam@result:'try'(
extract_response_type(
Return_type,
Context_type_name
),
fun(_use0) ->
{Response_type, Mutates_context} = _use0,
gleam@result:'try'(
extract_result_args(Response_type),
fun(_use0@1) ->
{Ok_type, Err_type} = _use0@1,
Payload_params = gleam@list:filter(
Params,
fun(P@1) ->
case erlang:element(4, P@1) of
{some, T@1} ->
not is_context_type(
T@1,
Context_type_name
);
none ->
true
end
end
),
{ok,
{Ok_type,
Err_type,
Payload_params,
Mutates_context}}
end
)
end
)
end
)
end
)
end
).
-file("src/libero/scanner.gleam", 319).
-spec parse_single_endpoint(
glance:function_(),
binary(),
libero@glance_type_resolver:type_resolver(),
binary(),
list(glance:definition(glance:custom_type())),
gleam@dict:dict(binary(), binary()),
list({binary(), binary()})
) -> {ok, handler_endpoint()} | {error, nil}.
parse_single_endpoint(
Func,
Module_path,
Resolver,
Context_type_name,
Custom_types,
Module_files,
Exclude_param_types
) ->
gleam@bool:guard(
not gleam_stdlib:string_starts_with(
erlang:element(3, Func),
<<"server_"/utf8>>
),
{error, nil},
fun() ->
Wire_name = gleam@string:drop_start(
erlang:element(3, Func),
string:length(<<"server_"/utf8>>)
),
gleam@result:'try'(
validate_handler_signature(Func, Context_type_name),
fun(_use0) ->
{Ok_type, Err_type, Payload_params, Mutates_context} = _use0,
To_ft = fun(T) ->
Ft@1 = case libero@glance_type_resolver:type_to_field_type(
T,
Resolver,
Module_path,
preserve_unsupported
) of
{ok, Ft} -> Ft;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"libero/scanner"/utf8>>,
function => <<"parse_single_endpoint"/utf8>>,
line => 339,
value => _assert_fail,
start => 11581,
'end' => 11766,
pattern_start => 11592,
pattern_end => 11598})
end,
Ft@1
end,
Payload_params@1 = case Exclude_param_types of
[] ->
Payload_params;
_ ->
gleam@list:filter(
Payload_params,
fun(P) -> case erlang:element(4, P) of
{some, T@1} ->
case To_ft(T@1) of
{user_type, Mp, Tn, _} ->
not gleam@list:contains(
Exclude_param_types,
{Mp, Tn}
);
_ ->
true
end;
none ->
true
end end
)
end,
{Params_typed, Msg_type} = try_resolve_msg_type(
Payload_params@1,
Module_path,
Custom_types,
Module_files,
To_ft
),
{ok,
{handler_endpoint,
Module_path,
Wire_name,
To_ft(Ok_type),
To_ft(Err_type),
Params_typed,
Mutates_context,
Msg_type}}
end
)
end
).
-file("src/libero/scanner.gleam", 237).
-spec parse_endpoints(
binary(),
binary(),
gleam@dict:dict(binary(), binary()),
list({binary(), binary()})
) -> {ok, list(handler_endpoint())} | {error, libero@gen_error:gen_error()}.
parse_endpoints(File_path, Context_type_name, Module_files, Exclude_param_types) ->
gleam@result:'try'(
parse_module(File_path),
fun(Parsed) ->
Module_path = derive_module_path(File_path),
gleam@result:'try'(
begin
_pipe = libero@glance_type_resolver:resolver_from_imports(
erlang:element(2, Parsed)
),
gleam@result:map_error(
_pipe,
fun(Message) ->
{type_resolution_failed, File_path, Message}
end
)
end,
fun(Resolver) ->
{ok,
gleam@list:filter_map(
erlang:element(6, Parsed),
fun(Def) ->
{definition, _, Func} = Def,
case erlang:element(4, Func) =:= public of
false ->
{error, nil};
true ->
parse_single_endpoint(
Func,
Module_path,
Resolver,
Context_type_name,
erlang:element(3, Parsed),
Module_files,
Exclude_param_types
)
end
end
)}
end
)
end
).
-file("src/libero/scanner.gleam", 138).
?DOC(
" Like `scan`, but filters out handler params whose resolved type matches\n"
" any #(module_path, type_name) in exclude_param_types. Filtered params are\n"
" removed before message-type resolution, so a handler with one message\n"
" param plus an excluded param still resolves the message type correctly.\n"
).
-spec scan_excluding(binary(), binary(), list({binary(), binary()})) -> {ok,
list(handler_endpoint())} |
{error, list(libero@gen_error:gen_error())}.
scan_excluding(Src_dir, Context_type_name, Exclude_param_types) ->
gleam@result:'try'(
begin
_pipe = walk_directory(Src_dir),
gleam@result:map_error(_pipe, fun(Cause) -> [Cause] end)
end,
fun(Files) ->
Module_files = gleam@list:fold(
Files,
maps:new(),
fun(Acc, File_path) ->
gleam@dict:insert(
Acc,
derive_module_path(File_path),
File_path
)
end
),
{Endpoints_rev, Errors_rev} = gleam@list:fold(
Files,
{[], []},
fun(Acc@1, File_path@1) ->
{Eps_acc, Errs_acc} = Acc@1,
case parse_endpoints(
File_path@1,
Context_type_name,
Module_files,
Exclude_param_types
) of
{ok, Eps} ->
{lists:append(lists:reverse(Eps), Eps_acc),
Errs_acc};
{error, Err} ->
{Eps_acc, [Err | Errs_acc]}
end
end
),
case Errors_rev of
[] ->
Endpoints = lists:reverse(Endpoints_rev),
case duplicate_fn_name_errors(Endpoints) of
[] ->
{ok, Endpoints};
Dup_errors ->
{error, Dup_errors}
end;
_ ->
{error, lists:reverse(Errors_rev)}
end
end
).
-file("src/libero/scanner.gleam", 127).
?DOC(
" Scan a source directory for handler functions.\n"
" Looks for: pub fn server_*(args..., context: T) -> Result(ok, err)\n"
" Returns the endpoint list or structured errors.\n"
).
-spec scan(binary(), binary()) -> {ok, list(handler_endpoint())} |
{error, list(libero@gen_error:gen_error())}.
scan(Src_dir, Context_type_name) ->
scan_excluding(Src_dir, Context_type_name, []).
-file("src/libero/scanner.gleam", 183).
?DOC(
" Collect type seeds from scanned endpoints for the walker.\n"
" Returns #(module_path, type_name) pairs from all param and return types.\n"
).
-spec collect_seeds(list(handler_endpoint())) -> list({binary(), binary()}).
collect_seeds(Endpoints) ->
_pipe = gleam@list:flat_map(
Endpoints,
fun(E) ->
From_params = gleam@list:flat_map(
erlang:element(6, E),
fun(P) ->
libero@field_type:collect_user_types(erlang:element(2, P))
end
),
From_ok = libero@field_type:collect_user_types(erlang:element(4, E)),
From_err = libero@field_type:collect_user_types(
erlang:element(5, E)
),
From_msg_type = case erlang:element(8, E) of
{some, {Module_path, Type_name}} ->
[{Module_path, Type_name}];
none ->
[]
end,
lists:append([From_params, From_ok, From_err, From_msg_type])
end
),
gleam@list:unique(_pipe).
-file("src/libero/scanner.gleam", 272).
?DOC(" Build a map from unqualified type names to the full module path of their import.\n").
-spec build_type_import_map(list(glance:definition(glance:import()))) -> gleam@dict:dict(binary(), binary()).
build_type_import_map(Imports) ->
gleam@list:fold(
Imports,
maps:new(),
fun(Acc, Def) ->
{definition, _, Imp} = Def,
gleam@list:fold(
erlang:element(5, Imp),
Acc,
fun(Inner_acc, Uq) ->
Key = case erlang:element(3, Uq) of
{some, Alias} ->
Alias;
none ->
erlang:element(2, Uq)
end,
gleam@dict:insert(Inner_acc, Key, erlang:element(3, Imp))
end
)
end
).
-file("src/libero/scanner.gleam", 289).
?DOC(
" Map locally-bound type names back to their original names from the\n"
" source module. Only populated when an import uses `type X as Y`.\n"
).
-spec build_type_alias_originals(list(glance:definition(glance:import()))) -> gleam@dict:dict(binary(), binary()).
build_type_alias_originals(Imports) ->
gleam@list:fold(
Imports,
maps:new(),
fun(Acc, Def) ->
{definition, _, Imp} = Def,
gleam@list:fold(
erlang:element(5, Imp),
Acc,
fun(Inner_acc, Uq) -> case erlang:element(3, Uq) of
{some, Alias} ->
gleam@dict:insert(
Inner_acc,
Alias,
erlang:element(2, Uq)
);
none ->
Inner_acc
end end
)
end
).
-file("src/libero/scanner.gleam", 305).
?DOC(
" Build a map from import aliases (and bare module names) to the full\n"
" module path.\n"
).
-spec build_alias_resolution_map(list(glance:definition(glance:import()))) -> gleam@dict:dict(binary(), binary()).
build_alias_resolution_map(Imports) ->
gleam@list:fold(
Imports,
maps:new(),
fun(Acc, Def) ->
{definition, _, Imp} = Def,
Last_seg = libero@field_type:last_segment(erlang:element(3, Imp)),
Alias = case erlang:element(4, Imp) of
{some, {named, Name}} ->
Name;
_ ->
Last_seg
end,
gleam@dict:insert(Acc, Alias, erlang:element(3, Imp))
end
).