Current section
Files
Jump to
Current section
Files
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, build_type_alias_originals/1, build_alias_resolution_map/1, build_type_import_map/1, parse_module/1, scan_handler_endpoints/2]).
-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 the server source tree to discover handler functions whose\n"
" signatures define the wire contract: each public function with last\n"
" parameter `HandlerContext`, return type `#(_, HandlerContext)`, and\n"
" all other types from shared/.\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()}.
-file("src/libero/scanner.gleam", 112).
-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", 102).
-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", 81).
?DOC(
" Classify a single directory entry and fold it into the accumulator.\n"
" Stat failures (permissions, races) fall through as \"not a directory\",\n"
" which means the entry is evaluated as a file and filtered out unless\n"
" its name happens to end in `.gleam`. Missing files can't match, so\n"
" silently skipping is safe.\n"
).
-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", 63).
?DOC(
" Recursively walk a directory, returning every `.gleam` file found.\n"
" Skips any subdirectory named `generated`, since libero never reads its\n"
" own output, and leaving this convention in place means consumers\n"
" don't need to configure scan_excludes as their projects grow.\n"
"\n"
" Results are sorted alphabetically so codegen output is deterministic\n"
" across runs and across machines (filesystem order is not stable).\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", 129).
?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. Splitting on the LAST `/src/` (not the first)\n"
" matters for vendored paths like `vendor/x/src/lib/src/types.gleam`,\n"
" where the leftmost match would yield the wrong module path.\n"
" E.g. `examples/checklist/shared/src/shared/items.gleam` -> `shared/items`.\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", 190).
?DOC(
" Detect handler functions sharing a name across modules. Each duplicate\n"
" would compile into the same ClientMsg variant constructor and the same\n"
" dispatch case arm, so codegen would emit two definitions of each and the\n"
" generated module would fail to compile. Surface as a libero-level error\n"
" before that happens.\n"
).
-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", 542).
?DOC(
" Resolve a non-module-qualified named type. Builtins return their\n"
" FieldType directly. Other names are looked up in `imports` to\n"
" produce a `UserType` with the import's full module path.\n"
"\n"
" Invariant: callers reach this function only after\n"
" `all_types_shared` has accepted the name, meaning it's either a\n"
" builtin or a shared-tree type. If the name is in shared but not in\n"
" the file's imports, the handler is shadowing a shared type with a\n"
" local definition (only seen in test fixtures); we fall back to the\n"
" bare name as module_path. Production handlers always import their\n"
" shared types, so this fallback never fires for compiled endpoints.\n"
).
-spec builtin_or_user(
binary(),
list(glance:type()),
gleam@dict:dict(binary(), binary()),
gleam@dict:dict(binary(), binary()),
gleam@dict:dict(binary(), binary())
) -> libero@field_type:field_type().
builtin_or_user(Name, Parameters, Imports, Aliases, Type_alias_originals) ->
Recurse = fun(T) ->
glance_type_to_field_type(T, Imports, Aliases, Type_alias_originals)
end,
case libero@field_type:builtin_field_type(Name, Parameters, Recurse) of
{ok, Ft} ->
Ft;
{error, nil} ->
Module_path = begin
_pipe = gleam_stdlib:map_get(Imports, Name),
gleam@result:unwrap(_pipe, Name)
end,
Type_name = begin
_pipe@1 = gleam_stdlib:map_get(Type_alias_originals, Name),
gleam@result:unwrap(_pipe@1, Name)
end,
{user_type,
Module_path,
Type_name,
gleam@list:map(Parameters, Recurse)}
end.
-file("src/libero/scanner.gleam", 487).
?DOC(
" Convert a glance.Type AST into a structured FieldType, resolving\n"
" module-qualified types to their full paths via the supplied maps.\n"
"\n"
" `imports` and `aliases` map unqualified names and module aliases\n"
" (respectively) to FULL module paths (e.g. \"shared/types\"). See\n"
" `build_type_import_map` and `build_alias_resolution_map`.\n"
" `type_alias_originals` maps locally-bound aliased type names to the\n"
" name they have in the source module, e.g. {\"MyItem\": \"Item\"}.\n"
).
-spec glance_type_to_field_type(
glance:type(),
gleam@dict:dict(binary(), binary()),
gleam@dict:dict(binary(), binary()),
gleam@dict:dict(binary(), binary())
) -> libero@field_type:field_type().
glance_type_to_field_type(T, Imports, Aliases, Type_alias_originals) ->
Recurse_named = fun(Name, Params) ->
builtin_or_user(Name, Params, Imports, Aliases, Type_alias_originals)
end,
Recurse = fun(T@1) ->
glance_type_to_field_type(T@1, Imports, Aliases, Type_alias_originals)
end,
case T of
{named_type, _, Name@1, none, []} ->
Recurse_named(Name@1, []);
{named_type, _, Name@2, none, Params@1} ->
Recurse_named(Name@2, Params@1);
{named_type, _, Name@3, {some, M}, Params@2} ->
Module_path = begin
_pipe = gleam_stdlib:map_get(Aliases, M),
gleam@result:unwrap(_pipe, M)
end,
{user_type, Module_path, Name@3, gleam@list:map(Params@2, Recurse)};
{tuple_type, _, Elements} ->
{tuple_of, gleam@list:map(Elements, Recurse)};
{variable_type, _, Name@4} ->
{type_var, Name@4};
{function_type, _, _, _} ->
{type_var, <<"_fn"/utf8>>};
{hole_type, _, _} ->
{type_var, <<"_"/utf8>>}
end.
-file("src/libero/scanner.gleam", 595).
?DOC(
" Check if a single type (and all its parameters) are shared or builtins.\n"
" `field_type.is_builtin` is the single source of truth for builtin\n"
" names, shared with the walker.\n"
"\n"
" Resolves locally-bound aliased type names back to their original\n"
" source names before checking against `shared_types`, so\n"
" `import shared/items.{type Item as MyItem}` lets the handler use\n"
" `MyItem` and still pass the shared check.\n"
).
-spec is_type_shared(
glance:type(),
gleam@set:set(binary()),
gleam@dict:dict(binary(), binary())
) -> boolean().
is_type_shared(T, Shared_types, Type_alias_originals) ->
case T of
{named_type, _, Name, _, Parameters} ->
Resolved = begin
_pipe = gleam_stdlib:map_get(Type_alias_originals, Name),
gleam@result:unwrap(_pipe, Name)
end,
(libero@field_type:is_builtin(Resolved) orelse gleam@set:contains(
Shared_types,
Resolved
))
andalso gleam@list:all(
Parameters,
fun(P) ->
is_type_shared(P, Shared_types, Type_alias_originals)
end
);
{tuple_type, _, Elements} ->
gleam@list:all(
Elements,
fun(E) ->
is_type_shared(E, Shared_types, Type_alias_originals)
end
);
{variable_type, _, _} ->
true;
{function_type, _, Parameters@1, Return} ->
gleam@list:all(
Parameters@1,
fun(P@1) ->
is_type_shared(P@1, Shared_types, Type_alias_originals)
end
)
andalso is_type_shared(Return, Shared_types, Type_alias_originals);
{hole_type, _, _} ->
true
end.
-file("src/libero/scanner.gleam", 577).
?DOC(
" Check that all types in a list are shared types or builtins.\n"
" Walks type parameters recursively (e.g. List(Todo) checks Todo).\n"
).
-spec all_types_shared(
list(glance:type()),
gleam@set:set(binary()),
gleam@dict:dict(binary(), binary())
) -> boolean().
all_types_shared(Types, Shared_types, Type_alias_originals) ->
gleam@list:all(
Types,
fun(T) -> is_type_shared(T, Shared_types, Type_alias_originals) end
).
-file("src/libero/scanner.gleam", 469).
?DOC(" Pull `ok` and `err` out of a `Result(ok, err)` type annotation.\n").
-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", 461).
?DOC(
" Check if a type annotation is the project's HandlerContext, brought\n"
" into local scope by an unqualified import. We deliberately reject\n"
" module-qualified `pkg.HandlerContext` references so that types named\n"
" `HandlerContext` from unrelated modules aren't treated as handler\n"
" endpoints.\n"
).
-spec is_handler_context_type(glance:type()) -> boolean().
is_handler_context_type(T) ->
case T of
{named_type, _, <<"HandlerContext"/utf8>>, none, _} ->
true;
_ ->
false
end.
-file("src/libero/scanner.gleam", 444).
?DOC(
" Pull the response slot out of the function's return type.\n"
" `#(R, HandlerContext)` returns `(R, mutates_context: True)`.\n"
" `R` (already a Result) returns `(R, mutates_context: False)`.\n"
" Any other shape rejects the function as a non-handler.\n"
).
-spec extract_response_type(glance:type()) -> {ok, {glance:type(), boolean()}} |
{error, nil}.
extract_response_type(T) ->
case T of
{tuple_type, _, [Response, State]} ->
case is_handler_context_type(State) of
true ->
{ok, {Response, true}};
false ->
{error, nil}
end;
{named_type, _, <<"Result"/utf8>>, _, _} ->
{ok, {T, false}};
_ ->
{error, nil}
end.
-file("src/libero/scanner.gleam", 413).
?DOC(
" Verify the function's signature matches the handler-as-contract shape.\n"
" Two return shapes are accepted:\n"
" - `#(Result(ok, err), HandlerContext)` — handler may emit a new\n"
" context value (e.g. login swapping the session).\n"
" - `Result(ok, err)` — read-only; codegen threads the inbound context\n"
" through unchanged.\n"
" In both cases the last parameter must be typed `HandlerContext`.\n"
"\n"
" Returns the two halves of the `Result`, the payload parameter list\n"
" (everything before the trailing HandlerContext), and a flag indicating\n"
" which return shape was used.\n"
"\n"
" The wire envelope, dispatch, and client codecs all assume Result-shaped\n"
" responses; a bare value would compile but produce broken serialization\n"
" at runtime. Filter at scan time so codegen never sees a non-Result.\n"
).
-spec validate_handler_signature(glance:function_()) -> {ok,
{glance:type(),
glance:type(),
list(glance:function_parameter()),
boolean()}} |
{error, nil}.
validate_handler_signature(Func) ->
Params = erlang:element(5, Func),
gleam@bool:guard(
gleam@list:is_empty(Params),
{error, nil},
fun() ->
gleam@result:'try'(
gleam@list:last(Params),
fun(Last_param) ->
gleam@result:'try'(
gleam@option:to_result(
erlang:element(4, Last_param),
nil
),
fun(Last_type) ->
gleam@bool:guard(
not is_handler_context_type(Last_type),
{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
),
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:take(
Params,
erlang:length(
Params
)
- 1
),
{ok,
{Ok_type,
Err_type,
Payload_params,
Mutates_context}}
end
)
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/libero/scanner.gleam", 347).
-spec parse_single_endpoint(
glance:function_(),
binary(),
gleam@dict:dict(binary(), binary()),
gleam@dict:dict(binary(), binary()),
gleam@dict:dict(binary(), binary()),
gleam@set:set(binary())
) -> {ok, handler_endpoint()} | {error, nil}.
parse_single_endpoint(
Func,
Module_path,
Type_imports,
Alias_map,
Type_alias_originals,
Shared_types
) ->
gleam@result:'try'(
validate_handler_signature(Func),
fun(_use0) ->
{Ok_type, Err_type, Payload_params, Mutates_context} = _use0,
All_param_types = gleam@list:filter_map(
Payload_params,
fun(P) -> gleam@option:to_result(erlang:element(4, P), nil) end
),
All_types = [Ok_type, Err_type | All_param_types],
gleam@bool:guard(
not all_types_shared(
All_types,
Shared_types,
Type_alias_originals
),
{error, nil},
fun() ->
To_ft = fun(T) ->
glance_type_to_field_type(
T,
Type_imports,
Alias_map,
Type_alias_originals
)
end,
Params_typed = gleam@list:filter_map(
Payload_params,
fun(P@1) ->
case {erlang:element(2, P@1),
erlang:element(4, P@1)} of
{{some, Label}, {some, Type_}} ->
{ok, {Label, To_ft(Type_)}};
{_, _} ->
{error, nil}
end
end
),
{ok,
{handler_endpoint,
Module_path,
erlang:element(3, Func),
To_ft(Ok_type),
To_ft(Err_type),
Params_typed,
Mutates_context}}
end
)
end
).
-file("src/libero/scanner.gleam", 315).
?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"
" e.g. `import shared/items.{type Item as MyItem}` produces\n"
" {\"MyItem\": \"Item\"}. Used by `all_types_shared` so an aliased type\n"
" resolves against `shared_types` (which holds original names).\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", 333).
?DOC(
" Build a map from import aliases (and bare module names) to the full\n"
" module path. e.g. `import shared/line_items_report as wire` produces\n"
" {\"wire\": \"shared/line_items_report\"}. Unaliased imports produce\n"
" identity entries keyed by the last segment.\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
).
-file("src/libero/scanner.gleam", 295).
?DOC(
" Build a map from unqualified type names (using the alias if present)\n"
" to the FULL module path of their import.\n"
" e.g. `import shared/items.{type Item}` produces {\"Item\": \"shared/items\"}.\n"
" e.g. `import shared/items.{type Item as MyItem}` produces\n"
" {\"MyItem\": \"shared/items\"}.\n"
" Used by structured type resolution where downstream codegen needs the\n"
" full path for decoder function naming.\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", 240).
?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"
" Shared by the handler scanner and the type walker so every codegen\n"
" stage produces consistent errors for the same file.\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", 262).
-spec parse_endpoints(binary(), gleam@set:set(binary())) -> {ok,
list(handler_endpoint())} |
{error, libero@gen_error:gen_error()}.
parse_endpoints(File_path, Shared_types) ->
gleam@result:map(
parse_module(File_path),
fun(Parsed) ->
Module_path = derive_module_path(File_path),
Type_imports = build_type_import_map(erlang:element(2, Parsed)),
Alias_map = build_alias_resolution_map(erlang:element(2, Parsed)),
Type_alias_originals = build_type_alias_originals(
erlang:element(2, Parsed)
),
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,
Type_imports,
Alias_map,
Type_alias_originals,
Shared_types
)
end
end
)
end
).
-file("src/libero/scanner.gleam", 251).
-spec read_public_type_names(binary()) -> {ok, list(binary())} |
{error, libero@gen_error:gen_error()}.
read_public_type_names(File_path) ->
gleam@result:map(
parse_module(File_path),
fun(Parsed) ->
gleam@list:fold(
erlang:element(3, Parsed),
[],
fun(Acc, Ct) ->
{definition, _, T} = Ct,
case erlang:element(4, T) =:= public of
true ->
[erlang:element(3, T) | Acc];
false ->
Acc
end
end
)
end
).
-file("src/libero/scanner.gleam", 215).
?DOC(" Scan shared source directory and collect all exported type names.\n").
-spec scan_shared_type_names(binary()) -> {ok, gleam@set:set(binary())} |
{error, list(libero@gen_error:gen_error())}.
scan_shared_type_names(Shared_src) ->
gleam@result:'try'(
begin
_pipe = walk_directory(Shared_src),
gleam@result:map_error(_pipe, fun(Cause) -> [Cause] end)
end,
fun(Files) ->
{Names_set, Errors_rev} = gleam@list:fold(
Files,
{gleam@set:new(), []},
fun(Acc, File_path) ->
{Set_acc, Errs_acc} = Acc,
case read_public_type_names(File_path) of
{ok, Names} ->
{gleam@list:fold(
Names,
Set_acc,
fun gleam@set:insert/2
),
Errs_acc};
{error, Err} ->
{Set_acc, [Err | Errs_acc]}
end
end
),
case Errors_rev of
[] ->
{ok, Names_set};
_ ->
{error, lists:reverse(Errors_rev)}
end
end
).
-file("src/libero/scanner.gleam", 151).
?DOC(
" Scan server source for handler endpoint functions.\n"
" A handler endpoint is any public function whose last parameter\n"
" is typed as HandlerContext, return type is #(something, HandlerContext),\n"
" and all types in params/return are from shared/ or builtins.\n"
).
-spec scan_handler_endpoints(binary(), binary()) -> {ok,
list(handler_endpoint())} |
{error, list(libero@gen_error:gen_error())}.
scan_handler_endpoints(Server_src, Shared_src) ->
gleam@result:'try'(
scan_shared_type_names(Shared_src),
fun(Shared_types) ->
gleam@result:'try'(
begin
_pipe = walk_directory(Server_src),
gleam@result:map_error(_pipe, fun(Cause) -> [Cause] end)
end,
fun(Files) ->
{Endpoints_rev, Errors_rev} = gleam@list:fold(
Files,
{[], []},
fun(Acc, File_path) ->
{Eps_acc, Errs_acc} = Acc,
case parse_endpoints(File_path, Shared_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
)
end
).