Current section

Files

Jump to
libero src libero.erl
Raw

src/libero.erl

-module(libero).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/libero.gleam").
-export([main/0]).
-export_type([inject_fn/0, session_info/0, rpc/0, param/0, classified_param/0, return_shape/0, gen_error/0, config/0, type_resolver/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(
" RPC dispatch and client stub generator.\n"
"\n"
" Scans `server/src/server/**/*.gleam` for `pub fn`s marked with a\n"
" `/// @rpc` doc comment and produces:\n"
"\n"
" 1. `client/src/client/rpc/<module>.gleam`, per-module labeled\n"
" stub functions that look like ordinary Gleam functions to the\n"
" client developer. Each stub takes the same labels as the server\n"
" function (minus the first \"session context\" param) plus an\n"
" `on_response` callback, and returns `Effect(msg)`.\n"
"\n"
" 2. `server/src/server/rpc_dispatch_generated.gleam`, a flat case\n"
" expression that receives a decoded call envelope, looks up the\n"
" function by wire name, coerces the positional args, and calls\n"
" the real server function with the session context prepended.\n"
"\n"
" ## Conventions\n"
"\n"
" - The first parameter of an @rpc function is the session context\n"
" (e.g. `sqlight.Connection`) and is injected from the dispatch's\n"
" `db` argument. It is NOT part of the wire args.\n"
"\n"
" - The wire name is derived from the server file path and function\n"
" name: `server/src/server/records.gleam` + `save` → `\"records.save\"`.\n"
" Nested modules work: `server/src/server/admin/items.gleam` + `create`\n"
" → `\"admin.items.create\"`.\n"
"\n"
" - Types in parameter positions and return types should be reachable\n"
" from the generated stub (i.e. from a shared package). The generator\n"
" walks the server file's imports to resolve unqualified type names\n"
" and emits matching imports in the stub.\n"
"\n"
" - Doc comments are discarded by glance, so `/// @rpc` is found by\n"
" preprocessing the source text to build a set of annotated function\n"
" names, then each function's signature is extracted via glance.\n"
).
-type inject_fn() :: {inject_fn, binary(), binary(), binary()}.
-type session_info() :: {session_info,
binary(),
gleam@dict:dict(binary(), gleam@set:set(binary()))}.
-type rpc() :: {rpc,
binary(),
binary(),
binary(),
binary(),
binary(),
list(classified_param()),
list(param()),
return_shape(),
gleam@dict:dict(binary(), gleam@set:set(binary()))}.
-type param() :: {param, binary(), binary()}.
-type classified_param() :: {injected, binary(), inject_fn()} | {wire, param()}.
-type return_shape() :: {bare, binary()} | {wrapped, binary(), binary()}.
-type gen_error() :: {cannot_read_dir, binary(), simplifile:file_error()} |
{cannot_read_file, binary(), simplifile:file_error()} |
{cannot_write_file, binary(), simplifile:file_error()} |
{parse_failed, binary(), glance:error()} |
{no_context_param, binary(), binary()} |
{no_return_type, binary(), binary()} |
{unlabelled_param, binary(), binary(), integer()} |
{unknown_type, binary(), binary(), binary()} |
{duplicate_wire_name, binary()} |
{empty_module_path, binary()}.
-type config() :: {config,
binary(),
gleam@option:option(binary()),
binary(),
binary(),
binary(),
binary(),
binary()}.
-type type_resolver() :: {type_resolver,
gleam@dict:dict(binary(), binary()),
gleam@dict:dict(binary(), binary())}.
-file("src/libero.gleam", 251).
?DOC(
" Derive all paths from --namespace and --client. When namespace is\n"
" None the paths land under generated/libero/ inside the consumer's\n"
" server and client packages. When set, paths are nested one level\n"
" deeper under generated/libero/<namespace>/ to keep multi-SPA output\n"
" physically isolated.\n"
).
-spec build_config(binary(), gleam@option:option(binary()), binary()) -> config().
build_config(Ws_url, Namespace, Client_root) ->
{Scan_root, Dispatch_output, Stub_root, Config_output} = case Namespace of
none ->
{<<"src/server"/utf8>>,
<<"src/server/generated/libero/rpc_dispatch.gleam"/utf8>>,
<<Client_root/binary, "/src/client/generated/libero/rpc"/utf8>>,
<<Client_root/binary,
"/src/client/generated/libero/rpc_config.gleam"/utf8>>};
{some, Ns} ->
{<<"src/server/"/utf8, Ns/binary>>,
<<<<"src/server/generated/libero/"/utf8, Ns/binary>>/binary,
"/rpc_dispatch.gleam"/utf8>>,
<<<<<<Client_root/binary, "/src/client/generated/libero/"/utf8>>/binary,
Ns/binary>>/binary,
"/rpc"/utf8>>,
<<<<<<Client_root/binary, "/src/client/generated/libero/"/utf8>>/binary,
Ns/binary>>/binary,
"/rpc_config.gleam"/utf8>>}
end,
{config,
Ws_url,
Namespace,
Client_root,
Scan_root,
Dispatch_output,
Stub_root,
Config_output}.
-file("src/libero.gleam", 285).
?DOC(" Extract a `--name=value` flag from the argument list.\n").
-spec find_flag(list(binary()), binary()) -> {ok, binary()} | {error, nil}.
find_flag(Args, Name) ->
Prefix = <<Name/binary, "="/utf8>>,
_pipe = Args,
_pipe@1 = gleam@list:find(
_pipe,
fun(Arg) -> gleam_stdlib:string_starts_with(Arg, Prefix) end
),
gleam@result:map(
_pipe@1,
fun(Arg@1) -> gleam@string:drop_start(Arg@1, string:length(Prefix)) end
).
-file("src/libero.gleam", 390).
-spec find_duplicate_wire_names(list(rpc())) -> list(gen_error()).
find_duplicate_wire_names(Rpcs) ->
By_name = gleam@list:fold(
Rpcs,
maps:new(),
fun(Acc, Rpc) ->
Existing = begin
_pipe = gleam_stdlib:map_get(Acc, erlang:element(2, Rpc)),
gleam@result:unwrap(_pipe, [])
end,
gleam@dict:insert(Acc, erlang:element(2, Rpc), [Rpc | Existing])
end
),
gleam@dict:fold(By_name, [], fun(Acc@1, Name, Group) -> case Group of
[_, _ | _] ->
[{duplicate_wire_name, Name} | Acc@1];
_ ->
Acc@1
end end).
-file("src/libero.gleam", 465).
-spec format_file_error(simplifile:file_error()) -> binary().
format_file_error(Err) ->
simplifile:describe_error(Err).
-file("src/libero.gleam", 407).
-spec print_error(gen_error()) -> nil.
print_error(Err) ->
Message = case Err of
{cannot_read_dir, Path, Cause} ->
<<<<<<<<"cannot read directory: "/utf8, Path/binary>>/binary,
" ("/utf8>>/binary,
(format_file_error(Cause))/binary>>/binary,
")"/utf8>>;
{cannot_read_file, Path@1, Cause@1} ->
<<<<<<<<"cannot read file: "/utf8, Path@1/binary>>/binary,
" ("/utf8>>/binary,
(format_file_error(Cause@1))/binary>>/binary,
")"/utf8>>;
{cannot_write_file, Path@2, Cause@2} ->
<<<<<<<<"cannot write file: "/utf8, Path@2/binary>>/binary,
" ("/utf8>>/binary,
(format_file_error(Cause@2))/binary>>/binary,
")"/utf8>>;
{parse_failed, Path@3, _} ->
<<Path@3/binary,
": failed to parse as Gleam source (glance.module error)"/utf8>>;
{empty_module_path, Path@4} ->
<<Path@4/binary,
": could not derive module segments (path produced empty list)"/utf8>>;
{no_context_param, Path@5, Fn_name} ->
<<<<<<<<<<<<Path@5/binary, ": @rpc function `"/utf8>>/binary,
Fn_name/binary>>/binary,
"` has no parameters. The first parameter must be the session context"/utf8>>/binary,
"\n e.g. `pub fn "/utf8>>/binary,
Fn_name/binary>>/binary,
"(conn conn: sqlight.Connection, ...) -> ...`"/utf8>>;
{no_return_type, Path@6, Fn_name@1} ->
<<<<<<<<Path@6/binary, ": @rpc function `"/utf8>>/binary,
Fn_name@1/binary>>/binary,
"` has no return type annotation"/utf8>>/binary,
"\n every @rpc function must explicitly annotate its return type"/utf8>>;
{unlabelled_param, Path@7, Fn_name@2, Position} ->
<<<<<<<<<<<<Path@7/binary, ": @rpc function `"/utf8>>/binary,
Fn_name@2/binary>>/binary,
"` parameter at position "/utf8>>/binary,
(erlang:integer_to_binary(Position))/binary>>/binary,
" has no label"/utf8>>/binary,
"\n @rpc parameters must be labelled: `name name: String`"/utf8>>;
{unknown_type, Path@8, Fn_name@3, Type_name} ->
<<<<<<<<<<<<<<<<<<Path@8/binary, ": @rpc function `"/utf8>>/binary,
Fn_name@3/binary>>/binary,
"` references type `"/utf8>>/binary,
Type_name/binary>>/binary,
"` which isn't imported in this file"/utf8>>/binary,
"\n add an `import shared/<module>.{type "/utf8>>/binary,
Type_name/binary>>/binary,
"}` to "/utf8>>/binary,
Path@8/binary>>;
{duplicate_wire_name, Wire_name} ->
<<<<<<"duplicate wire name: two @rpc functions both resolve to `"/utf8,
Wire_name/binary>>/binary,
"`"/utf8>>/binary,
"\n rename one of them, or use an explicit `/// @rpc other.name` override (not yet implemented)"/utf8>>
end,
gleam_stdlib:println_error(<<"error: "/utf8, Message/binary>>).
-file("src/libero.gleam", 203).
-spec parse_config() -> config().
parse_config() ->
Args = erlang:element(4, argv:load()),
case find_flag(Args, <<"--ws-url"/utf8>>) of
{ok, Ws_url} ->
Namespace = case find_flag(Args, <<"--namespace"/utf8>>) of
{ok, Ns} ->
{some, Ns};
{error, nil} ->
none
end,
Client_root = case find_flag(Args, <<"--client"/utf8>>) of
{ok, Path} ->
Path;
{error, nil} ->
<<"../client"/utf8>>
end,
build_config(Ws_url, Namespace, Client_root);
{error, nil} ->
gleam_stdlib:println_error(<<"error: --ws-url is required"/utf8>>),
gleam_stdlib:println_error(<<""/utf8>>),
gleam_stdlib:println_error(
<<" Libero generates a client rpc_config with this URL baked in at"/utf8>>
),
gleam_stdlib:println_error(
<<" compile time. There is no default. Pass --ws-url=wss://<host>/ws/rpc"/utf8>>
),
gleam_stdlib:println_error(<<" for your environment."/utf8>>),
gleam_stdlib:println_error(<<""/utf8>>),
gleam_stdlib:println_error(<<" Example:"/utf8>>),
gleam_stdlib:println_error(
<<" gleam run -m libero -- --ws-url=wss://example.com/admin/ws/rpc --namespace=admin"/utf8>>
),
_ = erlang:halt(1),
build_config(<<""/utf8>>, none, <<"../client"/utf8>>)
end.
-file("src/libero.gleam", 840).
?DOC(" From \"pub fn save(...\", extract \"save\".\n").
-spec parse_pub_fn_name(binary()) -> binary().
parse_pub_fn_name(Line) ->
After_pub_fn = gleam@string:drop_start(Line, 7),
_pipe = gleam@string:split_once(After_pub_fn, <<"("/utf8>>),
_pipe@1 = gleam@result:map(
_pipe,
fun(Pair) -> gleam@string:trim(erlang:element(1, Pair)) end
),
gleam@result:unwrap(_pipe@1, gleam@string:trim(After_pub_fn)).
-file("src/libero.gleam", 814).
-spec process_line(binary(), binary(), boolean(), gleam@set:set(binary())) -> {boolean(),
gleam@set:set(binary())}.
process_line(Line, Marker, Pending_marker, Acc) ->
Trimmed = gleam@string:trim_start(Line),
Is_marker = gleam_stdlib:string_starts_with(Trimmed, Marker),
Is_doc = gleam_stdlib:string_starts_with(Trimmed, <<"///"/utf8>>),
Is_blank = Trimmed =:= <<""/utf8>>,
case gleam_stdlib:string_starts_with(Trimmed, <<"pub fn "/utf8>>) of
true ->
Next_acc = case Pending_marker of
true ->
gleam@set:insert(Acc, parse_pub_fn_name(Trimmed));
false ->
Acc
end,
{false, Next_acc};
false ->
New_pending = (Pending_marker andalso (Is_doc orelse Is_blank))
orelse Is_marker,
{New_pending, Acc}
end.
-file("src/libero.gleam", 788).
-spec walk_lines(list(binary()), binary(), boolean(), gleam@set:set(binary())) -> gleam@set:set(binary()).
walk_lines(Lines, Marker, Pending_marker, Acc) ->
case Lines of
[] ->
Acc;
[Line | Rest] ->
{Next_pending, Next_acc} = process_line(
Line,
Marker,
Pending_marker,
Acc
),
walk_lines(Rest, Marker, Next_pending, Next_acc)
end.
-file("src/libero.gleam", 775).
?DOC(
" Find every `pub fn NAME` preceded immediately (with optional blank\n"
" comments/whitespace) by a line containing the given marker. Returns\n"
" the set of function names.\n"
).
-spec find_annotated_functions(binary(), binary()) -> gleam@set:set(binary()).
find_annotated_functions(Source, Marker) ->
Lines = gleam@string:split(Source, <<"\n"/utf8>>),
walk_lines(Lines, Marker, false, gleam@set:new()).
-file("src/libero.gleam", 852).
?DOC(
" \"../../server/src/server/records.gleam\" → [\"records\"]\n"
" \"../../server/src/server/admin/items.gleam\" → [\"admin\", \"items\"]\n"
).
-spec path_to_module_segments(binary(), binary()) -> list(binary()).
path_to_module_segments(Path, Scan_root) ->
Prefix = <<Scan_root/binary, "/"/utf8>>,
After = case gleam_stdlib:string_starts_with(Path, Prefix) of
true ->
gleam@string:drop_start(Path, string:length(Prefix));
false ->
Path
end,
Without_ext = case gleam_stdlib:string_ends_with(After, <<".gleam"/utf8>>) of
true ->
gleam@string:drop_end(After, 6);
false ->
After
end,
gleam@string:split(Without_ext, <<"/"/utf8>>).
-file("src/libero.gleam", 878).
?DOC(
" Convert a server-package file path (relative to the server package\n"
" root, e.g. \"src/server/admin/items.gleam\") to the Gleam module path\n"
" used in import statements (\"server/admin/items\"). Strips the leading\n"
" \"src/\" and the \".gleam\" suffix.\n"
"\n"
" Precondition: the path must start with \"src/\" (no leading \"./\",\n"
" no absolute paths). Paths that don't match are returned minus\n"
" the \".gleam\" suffix with no error, which may produce garbage.\n"
" Libero only calls this with paths produced by its own directory\n"
" walker, which satisfies the precondition.\n"
).
-spec server_path_to_module(binary()) -> binary().
server_path_to_module(Path) ->
Without_src = case gleam_stdlib:string_starts_with(Path, <<"src/"/utf8>>) of
true ->
gleam@string:drop_start(Path, 4);
false ->
Path
end,
case gleam_stdlib:string_ends_with(Without_src, <<".gleam"/utf8>>) of
true ->
gleam@string:drop_end(Without_src, 6);
false ->
Without_src
end.
-file("src/libero.gleam", 902).
?DOC(
" Convert a cross-package client file path (relative to the server\n"
" cwd, e.g. \"../client/src/client/generated/libero/admin/rpc_config.gleam\")\n"
" to the Gleam module path used in import statements\n"
" (\"client/generated/libero/admin/rpc_config\"). Strips everything up\n"
" to and including the FIRST occurrence of \"/src/\" and the \".gleam\"\n"
" suffix.\n"
"\n"
" Precondition: the path must contain exactly one \"/src/\" segment\n"
" and end with \".gleam\". Pathological shapes like nested\n"
" \"foo/src/bar/src/baz.gleam\" would split on the first \"/src/\" and\n"
" silently mis-derive the module. Libero only calls this with paths\n"
" it builds from --client + convention, which satisfies the\n"
" precondition.\n"
).
-spec client_path_to_module(binary()) -> binary().
client_path_to_module(Path) ->
After_src = case gleam@string:split_once(Path, <<"/src/"/utf8>>) of
{ok, {_, Rest}} ->
Rest;
{error, nil} ->
Path
end,
case gleam_stdlib:string_ends_with(After_src, <<".gleam"/utf8>>) of
true ->
gleam@string:drop_end(After_src, 6);
false ->
After_src
end.
-file("src/libero.gleam", 961).
-spec default_module_alias(binary()) -> binary().
default_module_alias(Module_path) ->
_pipe = gleam@string:split(Module_path, <<"/"/utf8>>),
_pipe@1 = gleam@list:last(_pipe),
gleam@result:unwrap(_pipe@1, Module_path).
-file("src/libero.gleam", 928).
-spec build_type_resolver(list(glance:definition(glance:import()))) -> type_resolver().
build_type_resolver(Imports) ->
Empty_unq = maps:new(),
Empty_al = maps:new(),
Init = {type_resolver, Empty_unq, Empty_al},
gleam@list:fold(
Imports,
Init,
fun(Acc, Def) ->
Imp = erlang:element(3, Def),
Module_path = erlang:element(3, Imp),
Acc@2 = gleam@list:fold(
erlang:element(5, Imp),
Acc,
fun(Acc@1, Uq) ->
Name = case erlang:element(3, Uq) of
{some, A} ->
A;
none ->
erlang:element(2, Uq)
end,
{type_resolver,
gleam@dict:insert(
erlang:element(2, Acc@1),
Name,
Module_path
),
erlang:element(3, Acc@1)}
end
),
Alias_name = case erlang:element(4, Imp) of
{some, {named, Name@1}} ->
Name@1;
_ ->
default_module_alias(Module_path)
end,
{type_resolver,
erlang:element(2, Acc@2),
gleam@dict:insert(
erlang:element(3, Acc@2),
Alias_name,
Module_path
)}
end
).
-file("src/libero.gleam", 1155).
-spec empty_imports() -> gleam@dict:dict(binary(), gleam@set:set(binary())).
empty_imports() ->
maps:new().
-file("src/libero.gleam", 1159).
-spec merge_imports(
gleam@dict:dict(binary(), gleam@set:set(binary())),
gleam@dict:dict(binary(), gleam@set:set(binary()))
) -> gleam@dict:dict(binary(), gleam@set:set(binary())).
merge_imports(A, B) ->
gleam@dict:fold(
B,
A,
fun(Acc, Module_path, Types) ->
Existing = begin
_pipe = gleam_stdlib:map_get(Acc, Module_path),
gleam@result:unwrap(_pipe, gleam@set:new())
end,
gleam@dict:insert(
Acc,
Module_path,
gleam@set:union(Existing, Types)
)
end
).
-file("src/libero.gleam", 1168).
-spec single_import(binary(), binary()) -> gleam@dict:dict(binary(), gleam@set:set(binary())).
single_import(Module_path, Type_name) ->
gleam@dict:insert(
maps:new(),
Module_path,
gleam@set:insert(gleam@set:new(), Type_name)
).
-file("src/libero.gleam", 1175).
-spec empty_module_import(binary()) -> gleam@dict:dict(binary(), gleam@set:set(binary())).
empty_module_import(Module_path) ->
gleam@dict:insert(maps:new(), Module_path, gleam@set:new()).
-file("src/libero.gleam", 1179).
-spec render_imports(gleam@dict:dict(binary(), gleam@set:set(binary()))) -> list(binary()).
render_imports(Imports) ->
_pipe = Imports,
_pipe@1 = maps:to_list(_pipe),
_pipe@2 = gleam@list:sort(
_pipe@1,
fun(A, B) ->
{Path_a, _} = A,
{Path_b, _} = B,
gleam@string:compare(Path_a, Path_b)
end
),
gleam@list:map(
_pipe@2,
fun(Entry) ->
{Module_path, Types} = Entry,
Type_list = begin
_pipe@3 = gleam@set:to_list(Types),
gleam@list:sort(_pipe@3, fun gleam@string:compare/2)
end,
case Type_list of
[] ->
<<"import "/utf8, Module_path/binary>>;
_ ->
Type_clause = begin
_pipe@4 = Type_list,
_pipe@5 = gleam@list:map(
_pipe@4,
fun(T) -> <<"type "/utf8, T/binary>> end
),
gleam@string:join(_pipe@5, <<", "/utf8>>)
end,
<<<<<<<<"import "/utf8, Module_path/binary>>/binary,
".{"/utf8>>/binary,
Type_clause/binary>>/binary,
"}"/utf8>>
end
end
).
-file("src/libero.gleam", 1300).
-spec is_primitive_or_builtin(binary()) -> boolean().
is_primitive_or_builtin(Name) ->
gleam@list:contains(
[<<"Int"/utf8>>,
<<"Float"/utf8>>,
<<"String"/utf8>>,
<<"Bool"/utf8>>,
<<"Nil"/utf8>>,
<<"BitArray"/utf8>>,
<<"List"/utf8>>],
Name
).
-file("src/libero.gleam", 1277).
?DOC(
" Decide whether a type name needs an import and return an\n"
" ImportMap entry.\n"
).
-spec resolve_import(binary(), gleam@option:option(binary()), type_resolver()) -> gleam@dict:dict(binary(), gleam@set:set(binary())).
resolve_import(Name, Module, Resolver) ->
case {is_primitive_or_builtin(Name), Module} of
{true, _} ->
empty_imports();
{false, {some, Alias}} ->
_pipe = gleam_stdlib:map_get(erlang:element(3, Resolver), Alias),
_pipe@1 = gleam@result:map(_pipe, fun empty_module_import/1),
gleam@result:unwrap(_pipe@1, empty_imports());
{false, none} ->
_pipe@2 = gleam_stdlib:map_get(erlang:element(2, Resolver), Name),
_pipe@3 = gleam@result:map(
_pipe@2,
fun(Module_path) -> single_import(Module_path, Name) end
),
gleam@result:unwrap(_pipe@3, empty_imports())
end.
-file("src/libero.gleam", 1235).
-spec render_type(glance:type(), type_resolver()) -> {binary(),
gleam@dict:dict(binary(), gleam@set:set(binary()))}.
render_type(T, Resolver) ->
Empty = empty_imports(),
case T of
{named_type, _, Name, Module, Parameters} ->
{Param_strs, Param_imports} = gleam@list:fold(
Parameters,
{[], Empty},
fun(Acc, P) ->
{Strs, Imps} = Acc,
{S, I} = render_type(P, Resolver),
{lists:append(Strs, [S]), merge_imports(Imps, I)}
end
),
Base = case Module of
{some, M} ->
<<<<M/binary, "."/utf8>>/binary, Name/binary>>;
none ->
Name
end,
Rendered = case Param_strs of
[] ->
Base;
_ ->
<<<<<<Base/binary, "("/utf8>>/binary,
(gleam@string:join(Param_strs, <<", "/utf8>>))/binary>>/binary,
")"/utf8>>
end,
Imports_for_this = resolve_import(Name, Module, Resolver),
{Rendered, merge_imports(Imports_for_this, Param_imports)};
{tuple_type, _, Elements} ->
{Strs@2, Imps@2} = gleam@list:fold(
Elements,
{[], Empty},
fun(Acc@1, P@1) ->
{Strs@1, Imps@1} = Acc@1,
{S@1, I@1} = render_type(P@1, Resolver),
{lists:append(Strs@1, [S@1]), merge_imports(Imps@1, I@1)}
end
),
{<<<<"#("/utf8, (gleam@string:join(Strs@2, <<", "/utf8>>))/binary>>/binary,
")"/utf8>>,
Imps@2};
{function_type, _, _, _} ->
{<<"fn(...)"/utf8>>, Empty};
{variable_type, _, Name@1} ->
{Name@1, Empty};
{hole_type, _, _} ->
{<<"_"/utf8>>, Empty}
end.
-file("src/libero.gleam", 665).
-spec build_inject_entry(
glance:function_(),
binary(),
binary(),
type_resolver()
) -> {ok, {binary(), inject_fn(), session_info()}} | {error, nil}.
build_inject_entry(Func, Module_path, Module_alias, Resolver) ->
case erlang:element(5, Func) of
[First | _] ->
case First of
{function_parameter, _, _, {some, Session_t}} ->
{Rendered, Imports} = render_type(Session_t, Resolver),
Inject_fn = {inject_fn,
erlang:element(3, Func),
Module_path,
Module_alias},
Session = {session_info, Rendered, Imports},
{ok, {erlang:element(3, Func), Inject_fn, Session}};
_ ->
{error, nil}
end;
[] ->
{error, nil}
end.
-file("src/libero.gleam", 629).
-spec extract_injects_body(
binary(),
glance:module_(),
gleam@set:set(binary()),
config()
) -> {ok, list({binary(), inject_fn(), session_info()})} |
{error, list(gen_error())}.
extract_injects_body(Path, Module_ast, Annotated, Config) ->
Type_resolver = build_type_resolver(erlang:element(2, Module_ast)),
Module_segments = path_to_module_segments(Path, erlang:element(5, Config)),
case begin
_pipe = gleam@list:last(Module_segments),
gleam@option:from_result(_pipe)
end of
none ->
{error, [{empty_module_path, Path}]};
{some, Module_alias} ->
Module_path = server_path_to_module(Path),
Entries = begin
_pipe@1 = erlang:element(6, Module_ast),
gleam@list:filter_map(
_pipe@1,
fun(Def) ->
Func = erlang:element(3, Def),
case (erlang:element(4, Func) =:= public) andalso gleam@set:contains(
Annotated,
erlang:element(3, Func)
) of
false ->
{error, nil};
true ->
build_inject_entry(
Func,
Module_path,
Module_alias,
Type_resolver
)
end
end
)
end,
{ok, Entries}
end.
-file("src/libero.gleam", 602).
-spec extract_injects_from_file(binary(), config()) -> {ok,
list({binary(), inject_fn(), session_info()})} |
{error, list(gen_error())}.
extract_injects_from_file(Path, Config) ->
gleam@result:'try'(
begin
_pipe = simplifile:read(Path),
gleam@result:map_error(
_pipe,
fun(Cause) -> [{cannot_read_file, Path, Cause}] end
)
end,
fun(Source) ->
gleam@result:'try'(
begin
_pipe@1 = glance:module(Source),
gleam@result:map_error(
_pipe@1,
fun(Cause@1) -> [{parse_failed, Path, Cause@1}] end
)
end,
fun(Module_ast) ->
Annotated = find_annotated_functions(
Source,
<<"/// @inject"/utf8>>
),
case gleam@set:is_empty(Annotated) of
true ->
{ok, []};
false ->
extract_injects_body(
Path,
Module_ast,
Annotated,
Config
)
end
end
)
end
).
-file("src/libero.gleam", 571).
?DOC(
" Scan every source file for /// @inject functions. Returns the full\n"
" injection map keyed by function name, plus the inferred session\n"
" type info (or a sentinel empty SessionInfo if no @inject fns exist).\n"
).
-spec build_inject_map(list(binary()), config()) -> {ok,
{gleam@dict:dict(binary(), inject_fn()), session_info()}} |
{error, list(gen_error())}.
build_inject_map(Source_files, Config) ->
{Entries@1, Errors} = gleam@list:fold(
Source_files,
{[], []},
fun(Acc, Path) ->
{So_far, Errs} = Acc,
case extract_injects_from_file(Path, Config) of
{ok, Entries} ->
{lists:append(So_far, Entries), Errs};
{error, E} ->
{So_far, lists:append(Errs, E)}
end
end
),
case Errors of
[_ | _] ->
{error, Errors};
[] ->
Map = gleam@list:fold(
Entries@1,
maps:new(),
fun(Acc@1, Entry) ->
{Fn_name, Inject_fn, _} = Entry,
gleam@dict:insert(Acc@1, Fn_name, Inject_fn)
end
),
Session = case Entries@1 of
[] ->
{session_info, <<"Nil"/utf8>>, empty_imports()};
[{_, _, S} | _] ->
S
end,
{ok, {Map, Session}}
end.
-file("src/libero.gleam", 1064).
-spec classify_labelled_parameter(
list(classified_param()),
gleam@dict:dict(binary(), gleam@set:set(binary())),
list(gen_error()),
binary(),
glance:type(),
gleam@dict:dict(binary(), inject_fn()),
type_resolver()
) -> {list(classified_param()),
gleam@dict:dict(binary(), gleam@set:set(binary())),
list(gen_error())}.
classify_labelled_parameter(
Classified_so_far,
Imports_so_far,
Errors_so_far,
Label,
Type_,
Inject_map,
Resolver
) ->
case begin
_pipe = gleam_stdlib:map_get(Inject_map, Label),
gleam@option:from_result(_pipe)
end of
{some, Inject_fn} ->
{lists:append(Classified_so_far, [{injected, Label, Inject_fn}]),
Imports_so_far,
Errors_so_far};
none ->
{Rendered, New_imports} = render_type(Type_, Resolver),
Merged_imports = merge_imports(Imports_so_far, New_imports),
{lists:append(Classified_so_far, [{wire, {param, Label, Rendered}}]),
Merged_imports,
Errors_so_far}
end.
-file("src/libero.gleam", 1035).
-spec classify_parameter(
{list(classified_param()),
gleam@dict:dict(binary(), gleam@set:set(binary())),
list(gen_error())},
glance:function_parameter(),
integer(),
gleam@dict:dict(binary(), inject_fn()),
type_resolver(),
binary(),
binary()
) -> {list(classified_param()),
gleam@dict:dict(binary(), gleam@set:set(binary())),
list(gen_error())}.
classify_parameter(Acc, Param, I, Inject_map, Resolver, Path, Fn_name) ->
{Classified_so_far, Imports_so_far, Errors_so_far} = Acc,
case Param of
{function_parameter, {some, Label}, _, {some, Type_}} ->
classify_labelled_parameter(
Classified_so_far,
Imports_so_far,
Errors_so_far,
Label,
Type_,
Inject_map,
Resolver
);
_ ->
{Classified_so_far,
Imports_so_far,
lists:append(
Errors_so_far,
[{unlabelled_param, Path, Fn_name, I + 1}]
)}
end.
-file("src/libero.gleam", 1207).
?DOC(
" Detect whether a server fn's return type is `Result(T, E)` (Wrapped)\n"
" or a bare `T` (Bare). Render both halves and collect their imports.\n"
).
-spec render_return(glance:type(), type_resolver()) -> {return_shape(),
gleam@dict:dict(binary(), gleam@set:set(binary()))}.
render_return(T, Resolver) ->
case T of
{named_type, _, <<"Result"/utf8>>, none, [Ok_t, Err_t]} ->
{Ok_rendered, Ok_imports} = render_type(Ok_t, Resolver),
{Err_rendered, Err_imports} = render_type(Err_t, Resolver),
{{wrapped, Ok_rendered, Err_rendered},
merge_imports(Ok_imports, Err_imports)};
_ ->
{Rendered, Imports} = render_type(T, Resolver),
{{bare, Rendered}, Imports}
end.
-file("src/libero.gleam", 1095).
-spec assemble_rpc(
binary(),
binary(),
list(binary()),
binary(),
list(classified_param()),
list(param()),
gleam@dict:dict(binary(), gleam@set:set(binary())),
glance:type(),
type_resolver(),
config()
) -> rpc().
assemble_rpc(
Fn_name,
Path,
Module_segments,
Server_module_alias,
Classified,
Wire_params,
Stub_imports,
Return_type,
Resolver,
Config
) ->
{Return_shape, Return_imports} = render_return(Return_type, Resolver),
Libero_types = case Return_shape of
{bare, _} ->
[<<"Never"/utf8>>, <<"RpcError"/utf8>>];
{wrapped, _, _} ->
[<<"RpcError"/utf8>>]
end,
Libero_import = gleam@list:fold(
Libero_types,
maps:new(),
fun(Acc, T) ->
merge_imports(Acc, single_import(<<"libero/error"/utf8>>, T))
end
),
Merged_stub_imports = begin
_pipe = Stub_imports,
_pipe@1 = merge_imports(_pipe, Return_imports),
merge_imports(_pipe@1, Libero_import)
end,
Wire_name = case erlang:element(3, Config) of
{some, Ns} ->
gleam@string:join(
lists:append([Ns | Module_segments], [Fn_name]),
<<"."/utf8>>
);
none ->
gleam@string:join(
lists:append(Module_segments, [Fn_name]),
<<"."/utf8>>
)
end,
Server_module = server_path_to_module(Path),
Client_stub_path = <<<<<<(erlang:element(7, Config))/binary, "/"/utf8>>/binary,
(gleam@string:join(Module_segments, <<"/"/utf8>>))/binary>>/binary,
".gleam"/utf8>>,
{rpc,
Wire_name,
Fn_name,
Server_module,
Server_module_alias,
Client_stub_path,
Classified,
Wire_params,
Return_shape,
Merged_stub_imports}.
-file("src/libero.gleam", 969).
-spec build_rpc(
glance:function_(),
binary(),
list(binary()),
type_resolver(),
gleam@dict:dict(binary(), inject_fn()),
config()
) -> {ok, rpc()} | {error, list(gen_error())}.
build_rpc(Func, Path, Module_segments, Resolver, Inject_map, Config) ->
Fn_name = erlang:element(3, Func),
gleam@result:'try'(case erlang:element(6, Func) of
{some, Rt} ->
{ok, Rt};
none ->
{error, [{no_return_type, Path, Fn_name}]}
end, fun(Return_type) ->
gleam@result:'try'(
begin
_pipe = gleam@list:last(Module_segments),
gleam@result:replace_error(
_pipe,
[{empty_module_path, Path}]
)
end,
fun(Server_module_alias) ->
{Classified, Stub_imports, Param_errors} = gleam@list:index_fold(
erlang:element(5, Func),
{[], empty_imports(), []},
fun(Acc, Param, I) ->
classify_parameter(
Acc,
Param,
I,
Inject_map,
Resolver,
Path,
Fn_name
)
end
),
Wire_params = gleam@list:filter_map(
Classified,
fun(C) -> case C of
{wire, P} ->
{ok, P};
{injected, _, _} ->
{error, nil}
end end
),
case Param_errors of
[_ | _] ->
{error, Param_errors};
[] ->
{ok,
assemble_rpc(
Fn_name,
Path,
Module_segments,
Server_module_alias,
Classified,
Wire_params,
Stub_imports,
Return_type,
Resolver,
Config
)}
end
end
)
end).
-file("src/libero.gleam", 737).
-spec accumulate_rpc(
{list(rpc()), list(gen_error())},
glance:definition(glance:function_()),
gleam@set:set(binary()),
binary(),
list(binary()),
type_resolver(),
gleam@dict:dict(binary(), inject_fn()),
config()
) -> {list(rpc()), list(gen_error())}.
accumulate_rpc(
Acc,
Def,
Annotated_fn_names,
Path,
Module_segments,
Type_resolver,
Inject_map,
Config
) ->
{Rpcs_so_far, Errors_so_far} = Acc,
Func = erlang:element(3, Def),
Is_rpc = (erlang:element(4, Func) =:= public) andalso gleam@set:contains(
Annotated_fn_names,
erlang:element(3, Func)
),
case Is_rpc of
false ->
Acc;
true ->
Result = build_rpc(
Func,
Path,
Module_segments,
Type_resolver,
Inject_map,
Config
),
case Result of
{ok, Rpc} ->
{lists:append(Rpcs_so_far, [Rpc]), Errors_so_far};
{error, Errs} ->
{Rpcs_so_far, lists:append(Errors_so_far, Errs)}
end
end.
-file("src/libero.gleam", 695).
-spec extract_rpcs_from_file(
binary(),
gleam@dict:dict(binary(), inject_fn()),
config()
) -> {ok, list(rpc())} | {error, list(gen_error())}.
extract_rpcs_from_file(Path, Inject_map, Config) ->
gleam@result:'try'(
begin
_pipe = simplifile:read(Path),
gleam@result:map_error(
_pipe,
fun(Cause) -> [{cannot_read_file, Path, Cause}] end
)
end,
fun(Source) ->
gleam@result:'try'(
begin
_pipe@1 = glance:module(Source),
gleam@result:map_error(
_pipe@1,
fun(Cause@1) -> [{parse_failed, Path, Cause@1}] end
)
end,
fun(Module_ast) ->
Annotated_fn_names = find_annotated_functions(
Source,
<<"/// @rpc"/utf8>>
),
Type_resolver = build_type_resolver(
erlang:element(2, Module_ast)
),
Module_segments = path_to_module_segments(
Path,
erlang:element(5, Config)
),
{Rpcs, Errors} = gleam@list:fold(
erlang:element(6, Module_ast),
{[], []},
fun(Acc, Def) ->
accumulate_rpc(
Acc,
Def,
Annotated_fn_names,
Path,
Module_segments,
Type_resolver,
Inject_map,
Config
)
end
),
case Errors of
[] ->
{ok, Rpcs};
_ ->
{error, Errors}
end
end
)
end
).
-file("src/libero.gleam", 1338).
-spec extract_dir(binary()) -> binary().
extract_dir(Path) ->
case begin
_pipe = gleam@string:split(Path, <<"/"/utf8>>),
lists:reverse(_pipe)
end of
[_ | Rest_rev] ->
gleam@string:join(lists:reverse(Rest_rev), <<"/"/utf8>>);
[] ->
<<"."/utf8>>
end.
-file("src/libero.gleam", 1345).
-spec group_by_path(list(rpc())) -> gleam@dict:dict(binary(), list(rpc())).
group_by_path(Rpcs) ->
Empty = maps:new(),
gleam@list:fold(
Rpcs,
Empty,
fun(Acc, Rpc) ->
Existing = begin
_pipe = gleam_stdlib:map_get(Acc, erlang:element(6, Rpc)),
gleam@result:unwrap(_pipe, [])
end,
gleam@dict:insert(
Acc,
erlang:element(6, Rpc),
lists:append(Existing, [Rpc])
)
end
).
-file("src/libero.gleam", 1433).
-spec render_args_tuple(list(param())) -> binary().
render_args_tuple(Params) ->
case Params of
[] ->
<<"Nil"/utf8>>;
[Only] ->
erlang:element(2, Only);
_ ->
Names = gleam@list:map(Params, fun(P) -> erlang:element(2, P) end),
<<<<"#("/utf8, (gleam@string:join(Names, <<", "/utf8>>))/binary>>/binary,
")"/utf8>>
end.
-file("src/libero.gleam", 1393).
-spec render_stub_fn(rpc()) -> binary().
render_stub_fn(Rpc) ->
Labelled_params = begin
_pipe = erlang:element(8, Rpc),
_pipe@1 = gleam@list:map(
_pipe,
fun(P) ->
<<<<<<<<<<<<" "/utf8, (erlang:element(2, P))/binary>>/binary,
" "/utf8>>/binary,
(erlang:element(2, P))/binary>>/binary,
": "/utf8>>/binary,
(erlang:element(3, P))/binary>>/binary,
","/utf8>>
end
),
gleam@string:join(_pipe@1, <<"\n"/utf8>>)
end,
Labelled_params_block = case Labelled_params of
<<""/utf8>> ->
<<""/utf8>>;
_ ->
<<Labelled_params/binary, "\n"/utf8>>
end,
Args_tuple = render_args_tuple(erlang:element(8, Rpc)),
Response_type = case erlang:element(9, Rpc) of
{bare, Ok} ->
<<<<"Result("/utf8, Ok/binary>>/binary, ", RpcError(Never))"/utf8>>;
{wrapped, Ok@1, Err} ->
<<<<<<<<"Result("/utf8, Ok@1/binary>>/binary, ", RpcError("/utf8>>/binary,
Err/binary>>/binary,
"))"/utf8>>
end,
<<<<<<<<<<<<<<<<<<<<<<"pub fn "/utf8, (erlang:element(3, Rpc))/binary>>/binary,
"(\n"/utf8>>/binary,
Labelled_params_block/binary>>/binary,
" on_response on_response: fn("/utf8>>/binary,
Response_type/binary>>/binary,
") -> msg,\n) -> Effect(msg) {\n"/utf8>>/binary,
" rpc.call_by_name(\n url: rpc_config.ws_url,\n name: \""/utf8>>/binary,
(erlang:element(2, Rpc))/binary>>/binary,
"\",\n args: "/utf8>>/binary,
Args_tuple/binary>>/binary,
",\n wrap: on_response,\n )\n}"/utf8>>.
-file("src/libero.gleam", 1355).
-spec render_stub_file(list(rpc()), config()) -> binary().
render_stub_file(Rpcs, Config) ->
All_imports = begin
_pipe = Rpcs,
_pipe@1 = gleam@list:fold(
_pipe,
empty_imports(),
fun(Acc, Rpc) -> merge_imports(Acc, erlang:element(10, Rpc)) end
),
render_imports(_pipe@1)
end,
Rpc_config_module = client_path_to_module(erlang:element(8, Config)),
Standard_imports = <<<<<<<<"import "/utf8, Rpc_config_module/binary>>/binary,
"\n"/utf8>>/binary,
"import libero/rpc\n"/utf8>>/binary,
"import lustre/effect.{type Effect}"/utf8>>,
Extra_imports = case All_imports of
[] ->
<<""/utf8>>;
_ ->
<<"\n"/utf8,
(gleam@string:join(All_imports, <<"\n"/utf8>>))/binary>>
end,
Stubs = begin
_pipe@2 = Rpcs,
_pipe@3 = gleam@list:map(_pipe@2, fun render_stub_fn/1),
gleam@string:join(_pipe@3, <<"\n\n"/utf8>>)
end,
<<<<<<<<<<"//// Code generated by libero. DO NOT EDIT.
////
//// Regenerated from the /// @rpc-annotated server functions whenever
//// those source files change. Edit the server function signature (add
//// or remove labels, rename parameters) and re-run bin/dev.
"/utf8,
Standard_imports/binary>>/binary,
Extra_imports/binary>>/binary,
"\n\n"/utf8>>/binary,
Stubs/binary>>/binary,
"\n"/utf8>>.
-file("src/libero.gleam", 1450).
?DOC(
" Create the parent directory for the given file path, ignoring any\n"
" error. create_directory_all is idempotent (no error if the dir\n"
" already exists) and any real write failure surfaces on the\n"
" subsequent simplifile.write call.\n"
).
-spec ensure_parent_dir(binary()) -> nil.
ensure_parent_dir(Path) ->
_ = simplifile:create_directory_all(extract_dir(Path)),
nil.
-file("src/libero.gleam", 474).
-spec write_config(config()) -> {ok, nil} | {error, gen_error()}.
write_config(Config) ->
Content = <<<<"//// Code generated by libero. DO NOT EDIT.
////
//// WebSocket URL passed to libero at generation time via --ws-url.
//// Rerun bin/dev to regenerate if the URL changes.
pub const ws_url: String = \""/utf8,
(erlang:element(2, Config))/binary>>/binary,
"\"
"/utf8>>,
Output = erlang:element(8, Config),
ensure_parent_dir(Output),
case simplifile:write(Output, Content) of
{ok, _} ->
gleam_stdlib:println(<<" wrote "/utf8, Output/binary>>),
{ok, nil};
{error, Cause} ->
{error, {cannot_write_file, Output, Cause}}
end.
-file("src/libero.gleam", 1322).
-spec write_stub_file(binary(), list(rpc()), config()) -> {ok, nil} |
{error, gen_error()}.
write_stub_file(Path, Rpcs_in_file, Config) ->
Content = render_stub_file(Rpcs_in_file, Config),
ensure_parent_dir(Path),
case simplifile:write(Path, Content) of
{ok, _} ->
gleam_stdlib:println(<<" wrote "/utf8, Path/binary>>),
{ok, nil};
{error, Cause} ->
{error, {cannot_write_file, Path, Cause}}
end.
-file("src/libero.gleam", 1309).
-spec write_stub_files(list(rpc()), config()) -> {ok, nil} |
{error, gen_error()}.
write_stub_files(Rpcs, Config) ->
Grouped = group_by_path(Rpcs),
gleam@dict:fold(
Grouped,
{ok, nil},
fun(Acc, Path, Rpcs_in_file) ->
gleam@result:'try'(
Acc,
fun(_) -> write_stub_file(Path, Rpcs_in_file, Config) end
)
end
).
-file("src/libero.gleam", 1668).
-spec render_dispatch_call(rpc()) -> binary().
render_dispatch_call(Rpc) ->
{Labelled_args, _} = gleam@list:fold(
erlang:element(7, Rpc),
{[], 1},
fun(Acc, Cp) ->
{Args_so_far, Wire_idx} = Acc,
case Cp of
{injected, Label, Inject} ->
Arg = <<<<<<<<<<Label/binary, ": "/utf8>>/binary,
(erlang:element(4, Inject))/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(2, Inject))/binary>>/binary,
"(session)"/utf8>>,
{lists:append(Args_so_far, [Arg]), Wire_idx};
{wire, Param} ->
Arg@1 = <<<<<<(erlang:element(2, Param))/binary,
": coerce(a"/utf8>>/binary,
(erlang:integer_to_binary(Wire_idx))/binary>>/binary,
")"/utf8>>,
{lists:append(Args_so_far, [Arg@1]), Wire_idx + 1}
end
end
),
<<<<<<<<<<(erlang:element(5, Rpc))/binary, "."/utf8>>/binary,
(erlang:element(3, Rpc))/binary>>/binary,
"("/utf8>>/binary,
(gleam@string:join(Labelled_args, <<", "/utf8>>))/binary>>/binary,
")"/utf8>>.
-file("src/libero.gleam", 1705).
-spec build_indexes(integer(), list(integer())) -> list(integer()).
build_indexes(N, Acc) ->
case N of
0 ->
Acc;
_ ->
build_indexes(N - 1, [N | Acc])
end.
-file("src/libero.gleam", 1701).
-spec indexes(integer()) -> list(integer()).
indexes(N) ->
build_indexes(N, []).
-file("src/libero.gleam", 1658).
-spec render_pattern(integer()) -> binary().
render_pattern(Arity) ->
case Arity of
0 ->
<<"[]"/utf8>>;
_ ->
Names = begin
_pipe = indexes(Arity),
gleam@list:map(
_pipe,
fun(I) ->
<<"a"/utf8, (erlang:integer_to_binary(I))/binary>>
end
)
end,
<<<<"["/utf8, (gleam@string:join(Names, <<", "/utf8>>))/binary>>/binary,
"]"/utf8>>
end.
-file("src/libero.gleam", 1615).
-spec render_dispatch_case(rpc()) -> binary().
render_dispatch_case(Rpc) ->
Arity = erlang:length(erlang:element(8, Rpc)),
Pattern = render_pattern(Arity),
Call = render_dispatch_call(Rpc),
Panic_arm = <<<<<<<<<<<<<<<<<<<<<<<<" Error(reason) -> {\n"/utf8,
" let trace_id = trace.new_trace_id()\n"/utf8>>/binary,
" #(\n"/utf8>>/binary,
" wire.encode(Error(InternalError(trace_id: trace_id))),\n"/utf8>>/binary,
" Some(PanicInfo(\n"/utf8>>/binary,
" trace_id: trace_id,\n"/utf8>>/binary,
" fn_name: \""/utf8>>/binary,
(erlang:element(2, Rpc))/binary>>/binary,
"\",\n"/utf8>>/binary,
" reason: reason,\n"/utf8>>/binary,
" )),\n"/utf8>>/binary,
" )\n"/utf8>>/binary,
" }\n"/utf8>>,
Try_prefix = <<<<"case trace.try_call(fn() {\n "/utf8, Call/binary>>/binary,
"\n }) {\n"/utf8>>,
Body = case erlang:element(9, Rpc) of
{bare, _} ->
<<<<<<Try_prefix/binary,
" Ok(value) -> #(wire.encode(Ok(value)), None)\n"/utf8>>/binary,
Panic_arm/binary>>/binary,
" }"/utf8>>;
{wrapped, _, _} ->
<<<<<<<<Try_prefix/binary,
" Ok(Ok(value)) -> #(wire.encode(Ok(value)), None)\n"/utf8>>/binary,
" Ok(Error(app_err)) -> #(wire.encode(Error(AppError(app_err))), None)\n"/utf8>>/binary,
Panic_arm/binary>>/binary,
" }"/utf8>>
end,
<<<<<<<<<<" \""/utf8, (erlang:element(2, Rpc))/binary>>/binary,
"\", "/utf8>>/binary,
Pattern/binary>>/binary,
" ->\n "/utf8>>/binary,
Body/binary>>.
-file("src/libero.gleam", 1479).
-spec render_dispatch(
list(rpc()),
gleam@dict:dict(binary(), inject_fn()),
session_info(),
config()
) -> binary().
render_dispatch(Rpcs, Inject_map, Session, Config) ->
Handle_name = case erlang:element(3, Config) of
{some, Ns} ->
<<"handle_"/utf8, Ns/binary>>;
none ->
<<"handle"/utf8>>
end,
Has_wrapped = gleam@list:any(
Rpcs,
fun(Rpc) -> case erlang:element(9, Rpc) of
{wrapped, _, _} ->
true;
{bare, _} ->
false
end end
),
Error_import = case Has_wrapped of
true ->
<<"import libero/error.{
type PanicInfo, AppError, InternalError, MalformedRequest, PanicInfo,
UnknownFunction,
}"/utf8>>;
false ->
<<"import libero/error.{
type PanicInfo, InternalError, MalformedRequest, PanicInfo, UnknownFunction,
}"/utf8>>
end,
Session_is_used = not gleam@dict:is_empty(Inject_map),
Outer_session_binding = case Session_is_used of
true ->
<<"session"/utf8>>;
false ->
<<"_session"/utf8>>
end,
Dispatch_session_param = case Session_is_used of
true ->
<<<<" session session: "/utf8,
(erlang:element(2, Session))/binary>>/binary,
",\n"/utf8>>;
false ->
<<""/utf8>>
end,
Dispatch_call_session = case Session_is_used of
true ->
<<"session: session, "/utf8>>;
false ->
<<""/utf8>>
end,
Server_imports = begin
_pipe = Rpcs,
_pipe@1 = gleam@list:map(
_pipe,
fun(Rpc@1) -> erlang:element(4, Rpc@1) end
),
_pipe@2 = gleam@list:unique(_pipe@1),
_pipe@3 = gleam@list:sort(_pipe@2, fun gleam@string:compare/2),
_pipe@4 = gleam@list:map(
_pipe@3,
fun(M) -> <<"import "/utf8, M/binary>> end
),
gleam@string:join(_pipe@4, <<"\n"/utf8>>)
end,
Inject_imports = begin
_pipe@5 = maps:values(Inject_map),
_pipe@6 = gleam@list:map(_pipe@5, fun(F) -> erlang:element(3, F) end),
_pipe@7 = gleam@list:unique(_pipe@6),
_pipe@8 = gleam@list:sort(_pipe@7, fun gleam@string:compare/2),
_pipe@9 = gleam@list:map(
_pipe@8,
fun(M@1) -> <<"import "/utf8, M@1/binary>> end
),
gleam@string:join(_pipe@9, <<"\n"/utf8>>)
end,
Session_imports = begin
_pipe@10 = render_imports(erlang:element(3, Session)),
gleam@string:join(_pipe@10, <<"\n"/utf8>>)
end,
Cases = begin
_pipe@11 = Rpcs,
_pipe@12 = gleam@list:map(_pipe@11, fun render_dispatch_case/1),
gleam@string:join(_pipe@12, <<"\n\n"/utf8>>)
end,
Combined_imports = begin
_pipe@13 = [Server_imports, Inject_imports, Session_imports],
_pipe@14 = gleam@list:filter(_pipe@13, fun(S) -> S /= <<""/utf8>> end),
gleam@string:join(_pipe@14, <<"\n"/utf8>>)
end,
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"//// Code generated by libero. DO NOT EDIT.
////
//// Regenerated from the /// @rpc-annotated server functions whenever
//// those source files change.
import gleam/dynamic.{type Dynamic}
import gleam/option.{type Option, None, Some}
"/utf8,
Error_import/binary>>/binary,
"
import libero/trace
import libero/wire
"/utf8>>/binary,
Combined_imports/binary>>/binary,
"
/// Handle an incoming RPC call envelope. Returns a tuple of:
/// - the JSON-encoded response string (to send back to the client)
/// - `Option(PanicInfo)`: `Some` if a server function panicked,
/// letting the caller log, report, or escalate however they
/// prefer. Libero has no logging dependency of its own.
///
/// Malformed requests, unknown functions, and panics all flow back
/// as typed error envelopes. The WebSocket connection is never
/// dropped on bad input.
pub fn "/utf8>>/binary,
Handle_name/binary>>/binary,
"(
session "/utf8>>/binary,
Outer_session_binding/binary>>/binary,
": "/utf8>>/binary,
(erlang:element(2, Session))/binary>>/binary,
",
text text: String,
) -> #(String, Option(PanicInfo)) {
case wire.decode_call(text) {
Ok(#(name, args)) -> dispatch("/utf8>>/binary,
Dispatch_call_session/binary>>/binary,
"name: name, args: args)
Error(wire.DecodeError(message: _msg, cause: _cause)) ->
// The typed error is bound by field so neither the message nor
// the underlying json.DecodeError is silently discarded; the
// client only needs to know the envelope was malformed.
#(wire.encode(Error(MalformedRequest)), None)
}
}
fn dispatch(
"/utf8>>/binary,
Dispatch_session_param/binary>>/binary,
" name name: String,
args args: List(Dynamic),
) -> #(String, Option(PanicInfo)) {
case name, args {
"/utf8>>/binary,
Cases/binary>>/binary,
"
_, _ -> #(wire.encode(Error(UnknownFunction(name: name))), None)
}
}
@external(erlang, \"gleam_stdlib\", \"identity\")
fn coerce(value: Dynamic) -> a
"/utf8>>.
-file("src/libero.gleam", 1455).
-spec write_dispatch(
list(rpc()),
gleam@dict:dict(binary(), inject_fn()),
session_info(),
config()
) -> {ok, nil} | {error, gen_error()}.
write_dispatch(Rpcs, Inject_map, Session, Config) ->
Content = render_dispatch(Rpcs, Inject_map, Session, Config),
Dispatch_output = erlang:element(6, Config),
ensure_parent_dir(Dispatch_output),
case simplifile:write(Dispatch_output, Content) of
{ok, _} ->
gleam_stdlib:println(<<" wrote "/utf8, Dispatch_output/binary>>),
{ok, nil};
{error, Cause} ->
{error, {cannot_write_file, Dispatch_output, Cause}}
end.
-file("src/libero.gleam", 553).
-spec visit_file(list(binary()), binary(), binary()) -> list(binary()).
visit_file(Acc, Entry, Child) ->
Keep = gleam_stdlib:string_ends_with(Entry, <<".gleam"/utf8>>) andalso not gleam@list:contains(
[<<"sql.gleam"/utf8>>],
Entry
),
case Keep of
true ->
lists:append(Acc, [Child]);
false ->
Acc
end.
-file("src/libero.gleam", 543).
-spec visit_subdirectory(list(binary()), binary(), binary()) -> {ok,
list(binary())} |
{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(Acc, Nested)} end
)
end
).
-file("src/libero.gleam", 506).
?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"
).
-spec walk_directory(binary()) -> {ok, list(binary())} | {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@list:try_fold(
Entries,
[],
fun(Acc, Entry) -> visit_entry(Acc, Path, Entry) end
)
end
).
-file("src/libero.gleam", 521).
?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, gen_error()}.
visit_entry(Acc, Parent, Entry) ->
Child = <<<<Parent/binary, "/"/utf8>>/binary, Entry/binary>>,
gleam@bool:guard(
begin
_pipe = simplifile_erl:is_symlink(Child),
gleam@result:unwrap(_pipe, false)
end,
{ok, Acc},
fun() ->
Is_dir = gleam@result:unwrap(
simplifile_erl:is_directory(Child),
false
),
case Is_dir of
true ->
visit_subdirectory(Acc, Entry, Child);
false ->
{ok, visit_file(Acc, Entry, Child)}
end
end
).
-file("src/libero.gleam", 496).
-spec discover_source_files(config()) -> {ok, list(binary())} |
{error, gen_error()}.
discover_source_files(Config) ->
walk_directory(erlang:element(5, Config)).
-file("src/libero.gleam", 317).
?DOC(
" Runs the whole pipeline. Returns Ok(count) on success with the\n"
" number of RPCs written, or Error(errors) with every problem we\n"
" found, not just the first, so users see all issues in one run.\n"
).
-spec run(config()) -> {ok, integer()} | {error, list(gen_error())}.
run(Config) ->
gleam@result:'try'(
begin
_pipe = discover_source_files(Config),
gleam@result:map_error(_pipe, fun(E) -> [E] end)
end,
fun(Source_files) ->
gleam_stdlib:println(
<<<<"libero: found "/utf8,
(erlang:integer_to_binary(erlang:length(Source_files)))/binary>>/binary,
" server source files"/utf8>>
),
gleam@result:'try'(
build_inject_map(Source_files, Config),
fun(_use0) ->
{Inject_map, Session} = _use0,
gleam_stdlib:println(
<<<<"libero: found "/utf8,
(erlang:integer_to_binary(maps:size(Inject_map)))/binary>>/binary,
" @inject functions"/utf8>>
),
{All_rpcs, Extraction_errors} = gleam@list:fold(
Source_files,
{[], []},
fun(Acc, Path) ->
{Rpcs_so_far, Errors_so_far} = Acc,
case extract_rpcs_from_file(
Path,
Inject_map,
Config
) of
{ok, Rpcs} ->
{lists:append(Rpcs_so_far, Rpcs),
Errors_so_far};
{error, Errors} ->
{Rpcs_so_far,
lists:append(Errors_so_far, Errors)}
end
end
),
Dup_errors = find_duplicate_wire_names(All_rpcs),
All_errors = lists:append(Extraction_errors, Dup_errors),
case All_errors of
[] ->
gleam_stdlib:println(
<<<<"libero: extracted "/utf8,
(erlang:integer_to_binary(
erlang:length(All_rpcs)
))/binary>>/binary,
" @rpc functions"/utf8>>
),
gleam@result:'try'(
begin
_pipe@1 = write_stub_files(All_rpcs, Config),
gleam@result:map_error(
_pipe@1,
fun(E@1) -> [E@1] end
)
end,
fun(_) ->
gleam@result:'try'(
begin
_pipe@2 = write_dispatch(
All_rpcs,
Inject_map,
Session,
Config
),
gleam@result:map_error(
_pipe@2,
fun(E@2) -> [E@2] end
)
end,
fun(_) ->
gleam@result:'try'(
begin
_pipe@3 = write_config(
Config
),
gleam@result:map_error(
_pipe@3,
fun(E@3) -> [E@3] end
)
end,
fun(_) ->
{ok,
erlang:length(All_rpcs)}
end
)
end
)
end
);
_ ->
{error, All_errors}
end
end
)
end
).
-file("src/libero.gleam", 294).
-spec main() -> nil.
main() ->
Config = parse_config(),
gleam_stdlib:println(
<<"libero: scanning "/utf8, (erlang:element(5, Config))/binary>>
),
case run(Config) of
{ok, Count} ->
gleam_stdlib:println(
<<<<"libero: done. generated "/utf8,
(erlang:integer_to_binary(Count))/binary>>/binary,
" RPC stubs"/utf8>>
);
{error, Errors} ->
gleam@list:each(Errors, fun print_error/1),
Count@1 = erlang:integer_to_binary(erlang:length(Errors)),
gleam_stdlib:println_error(<<""/utf8>>),
gleam_stdlib:println_error(
<<<<"libero: "/utf8, Count@1/binary>>/binary,
" error(s), no files generated"/utf8>>
),
_ = erlang:halt(1)
end.