Current section

Files

Jump to
libero src libero@codegen.erl
Raw

src/libero@codegen.erl

-module(libero@codegen).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/libero/codegen.gleam").
-export([extract_dir/1, write_dispatch/3, write_send_functions/2, write_push_wrappers/2, write_websocket/1, write_config/1, write_atoms/2, module_to_mjs_path/1, write_register/2]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-file("src/libero/codegen.gleam", 176).
?DOC(
" Convert a module path to a safe Gleam import alias.\n"
" e.g. \"server/store\" -> \"server_store_handler\"\n"
).
-spec handler_alias(binary()) -> binary().
handler_alias(Module_path) ->
<<(gleam@string:replace(Module_path, <<"/"/utf8>>, <<"_"/utf8>>))/binary,
"_handler"/utf8>>.
-file("src/libero/codegen.gleam", 182).
?DOC(
" Convert a message module path to a safe Gleam import alias.\n"
" e.g. \"shared/messages/admin\" -> \"messages_admin\"\n"
).
-spec message_alias(binary()) -> binary().
message_alias(Module_path) ->
gleam@string:replace(Module_path, <<"/"/utf8>>, <<"_"/utf8>>).
-file("src/libero/codegen.gleam", 755).
?DOC(" Write content to a file, logging the path on success.\n").
-spec write_file(binary(), binary()) -> {ok, nil} |
{error, libero@gen_error:gen_error()}.
write_file(Path, Content) ->
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/codegen.gleam", 670).
?DOC(" Write the rpc_config_ffi.mjs resolver file when --ws-path is active.\n").
-spec write_config_ffi(libero@config:config()) -> {ok, nil} |
{error, libero@gen_error:gen_error()}.
write_config_ffi(Config) ->
case erlang:element(2, Config) of
{ws_full_url, _} ->
{ok, nil};
{ws_path_only, _} ->
Ffi_path = gleam@string:replace(
erlang:element(7, Config),
<<".gleam"/utf8>>,
<<"_ffi.mjs"/utf8>>
),
Ffi_content = <<"// Code generated by libero. DO NOT EDIT.
//
// Resolves a WebSocket URL from the browser's current location + path.
// Used by the generated rpc_config.gleam when --ws-path is active.
export function resolveWsUrl(path) {
const protocol = globalThis.location?.protocol === \"https:\" ? \"wss:\" : \"ws:\";
const host = globalThis.location?.host ?? \"localhost\";
return protocol + \"//\" + host + path;
}
"/utf8>>,
write_file(Ffi_path, Ffi_content)
end.
-file("src/libero/codegen.gleam", 770).
?DOC(
" Derive a Gleam module path from a generated file path.\n"
" Uses /src/ split when available, falls back to stripping .gleam extension.\n"
).
-spec generated_module_path(binary()) -> binary().
generated_module_path(Path) ->
case gleam@string:split_once(Path, <<"/src/"/utf8>>) of
{ok, {_, After_src}} ->
case gleam_stdlib:string_ends_with(After_src, <<".gleam"/utf8>>) of
true ->
gleam@string:slice(
After_src,
0,
string:length(After_src) - 6
);
false ->
After_src
end;
{error, nil} ->
case gleam_stdlib:string_ends_with(Path, <<".gleam"/utf8>>) of
true ->
gleam@string:slice(Path, 0, string:length(Path) - 6);
false ->
Path
end
end.
-file("src/libero/codegen.gleam", 799).
-spec extract_dir(binary()) -> binary().
extract_dir(Path) ->
case begin
_pipe = gleam@string:split(Path, <<"/"/utf8>>),
lists:reverse(_pipe)
end of
[_ | Rest_rev] ->
case Rest_rev of
[] ->
<<"."/utf8>>;
_ ->
gleam@string:join(lists:reverse(Rest_rev), <<"/"/utf8>>)
end;
[] ->
<<"."/utf8>>
end.
-file("src/libero/codegen.gleam", 794).
?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/codegen.gleam", 19).
?DOC(
" Generate the server dispatch module at `server_generated/dispatch.gleam`.\n"
" The dispatch module decodes incoming wire calls and routes them by module\n"
" name to the appropriate handler.\n"
).
-spec write_dispatch(list(libero@scanner:message_module()), binary(), binary()) -> {ok,
nil} |
{error, libero@gen_error:gen_error()}.
write_dispatch(Message_modules, Server_generated, Atoms_module) ->
Msg_from_client_modules = gleam@list:filter(
Message_modules,
fun(M) -> erlang:element(4, M) end
),
Handler_imports = gleam@list:flat_map(
Msg_from_client_modules,
fun(M@1) ->
gleam@list:map(
erlang:element(6, M@1),
fun(Handler_mod) ->
Alias = handler_alias(Handler_mod),
<<<<<<"import "/utf8, Handler_mod/binary>>/binary,
" as "/utf8>>/binary,
Alias/binary>>
end
)
end
),
Message_type_imports = gleam@list:filter_map(
Msg_from_client_modules,
fun(M@2) -> case erlang:element(6, M@2) of
[_, _ | _] ->
Alias@1 = message_alias(erlang:element(2, M@2)),
{ok,
<<<<<<"import "/utf8, (erlang:element(2, M@2))/binary>>/binary,
" as "/utf8>>/binary,
Alias@1/binary>>};
_ ->
{error, nil}
end end
),
Case_arms = gleam@list:filter_map(
Msg_from_client_modules,
fun(M@3) -> case erlang:element(6, M@3) of
[] ->
{error, nil};
[Single_handler] ->
Alias@2 = handler_alias(Single_handler),
{ok,
<<<<<<<<" Ok(#(\""/utf8,
(erlang:element(2, M@3))/binary>>/binary,
"\", msg)) ->\n dispatch(state, fn() { "/utf8>>/binary,
Alias@2/binary>>/binary,
".update_from_client(msg: wire.coerce(msg), state:) })"/utf8>>};
[First_handler | Rest_handlers] ->
Msg_alias = message_alias(erlang:element(2, M@3)),
First_alias = handler_alias(First_handler),
Typed_msg_line = <<<<" let typed_msg: "/utf8,
Msg_alias/binary>>/binary,
".MsgFromClient = wire.coerce(msg)"/utf8>>,
First_call = <<<<" let result = "/utf8,
First_alias/binary>>/binary,
".update_from_client(msg: typed_msg, state:)"/utf8>>,
Chain_calls = gleam@list:map(
Rest_handlers,
fun(Handler_mod@1) ->
Alias@3 = handler_alias(Handler_mod@1),
<<<<" let result = case result {\n Error(UnhandledMessage) -> "/utf8,
Alias@3/binary>>/binary,
".update_from_client(msg: typed_msg, state:)\n other -> other\n }"/utf8>>
end
),
Body = gleam@string:join(
[Typed_msg_line, First_call | Chain_calls],
<<"\n"/utf8>>
),
{ok,
<<<<<<<<<<" Ok(#(\""/utf8,
(erlang:element(2, M@3))/binary>>/binary,
"\", msg)) -> {\n"/utf8>>/binary,
Body/binary>>/binary,
"\n dispatch(state, fn() { result })\n"/utf8>>/binary,
" }"/utf8>>}
end end
),
Ok_unknown_arm = <<" Ok(#(name, _)) ->\n #(wire.tag_response(wire.encode(Error(UnknownFunction(name)))), None, state)"/utf8>>,
Error_arm = <<" Error(_) ->\n #(wire.tag_response(wire.encode(Error(MalformedRequest))), None, state)"/utf8>>,
All_arms = lists:append([Case_arms, [Ok_unknown_arm, Error_arm]]),
Needs_unhandled_import = gleam@list:any(
Msg_from_client_modules,
fun(M@4) -> case erlang:element(6, M@4) of
[_, _ | _] ->
true;
_ ->
false
end end
),
All_imports = lists:append([Message_type_imports, Handler_imports]),
Content = <<<<<<<<<<<<<<<<"//// Code generated by libero. DO NOT EDIT.
import gleam/option.{type Option, None, Some}
import libero/error.{type PanicInfo, InternalError, MalformedRequest, UnknownFunction}
import libero/trace
import libero/wire
import server/app_error.{type AppError"/utf8,
(case Needs_unhandled_import of
true ->
<<", UnhandledMessage"/utf8>>;
false ->
<<""/utf8>>
end)/binary>>/binary,
"}
import server/shared_state.{type SharedState}
"/utf8>>/binary,
(gleam@string:join(All_imports, <<"\n"/utf8>>))/binary>>/binary,
"
@external(erlang, \""/utf8>>/binary,
Atoms_module/binary>>/binary,
"\", \"ensure\")
fn ensure_atoms() -> Nil
pub fn handle(
state state: SharedState,
data data: BitArray,
) -> #(BitArray, Option(PanicInfo), SharedState) {
let Nil = ensure_atoms()
case wire.decode_call(data) {
"/utf8>>/binary,
(gleam@string:join(All_arms, <<"\n"/utf8>>))/binary>>/binary,
"
}
}
fn dispatch(
state state: SharedState,
call call: fn() -> Result(#(a, SharedState), AppError),
) -> #(BitArray, Option(PanicInfo), SharedState) {
case trace.try_call(call) {
Ok(Ok(#(value, new_state))) ->
// Strip the MsgFromServer envelope - the FIFO response/request
// pairing on the client makes the variant tag redundant. The wire
// ships the payload directly as Result(payload, RpcError).
#(wire.tag_response(wire.encode(Ok(wire.unwrap_response(value)))), None, new_state)
Ok(Error(app_err)) ->
#(wire.tag_response(wire.encode(Error(error.AppError(app_err)))), None, state)
Error(reason) -> {
let trace_id = trace.new_trace_id()
#(
wire.tag_response(wire.encode(Error(InternalError(trace_id, \"Internal server error\")))),
Some(error.PanicInfo(trace_id:, fn_name: \"dispatch\", reason:)),
state,
)
}
}
}
"/utf8>>,
Output = <<Server_generated/binary, "/dispatch.gleam"/utf8>>,
ensure_parent_dir(Output),
write_file(Output, Content).
-file("src/libero/codegen.gleam", 191).
?DOC(
" Generate per-module send function files under `client_generated/`.\n"
" For each message module with `has_msg_from_client == True`, generates a\n"
" `<module_name>.gleam` file with a `send_to_server` function that wraps `rpc.send`.\n"
).
-spec write_send_functions(list(libero@scanner:message_module()), binary()) -> {ok,
nil} |
{error, list(libero@gen_error:gen_error())}.
write_send_functions(Message_modules, Client_generated) ->
Msg_from_client_modules = gleam@list:filter(
Message_modules,
fun(M) -> erlang:element(4, M) end
),
Errors = gleam@list:fold(
Msg_from_client_modules,
[],
fun(Errs, M@1) ->
Segment = libero@scanner:last_module_segment(erlang:element(2, M@1)),
Content = <<<<<<<<<<<<<<<<<<<<"//// Code generated by libero. DO NOT EDIT.
import gleam/dynamic.{type Dynamic}
import "/utf8,
(erlang:element(2, M@1))/binary>>/binary,
".{type MsgFromClient}
import libero/rpc
import "/utf8>>/binary,
(generated_module_path(
<<Client_generated/binary,
"/rpc_config.gleam"/utf8>>
))/binary>>/binary,
"
import "/utf8>>/binary,
(generated_module_path(
<<Client_generated/binary,
"/rpc_register.gleam"/utf8>>
))/binary>>/binary,
"
import lustre/effect.{type Effect}
pub fn send_to_server(
msg msg: MsgFromClient,
on_response on_response: fn(Dynamic) -> msg,
) -> Effect(msg) {
rpc_register.register_all()
rpc.send(
url: rpc_config.ws_url(),
module: \""/utf8>>/binary,
(erlang:element(2, M@1))/binary>>/binary,
"\",
msg: msg,
on_response: on_response,
)
}
pub fn update_from_server(
handler handler: fn(Dynamic) -> msg,
) -> Effect(msg) {
rpc_register.register_all()
rpc.update_from_server(
module: \""/utf8>>/binary,
(erlang:element(2, M@1))/binary>>/binary,
"\",
handler: handler,
)
}
"/utf8>>,
Output = <<<<<<Client_generated/binary, "/"/utf8>>/binary,
Segment/binary>>/binary,
".gleam"/utf8>>,
ensure_parent_dir(Output),
case simplifile:write(Output, Content) of
{ok, _} ->
gleam_stdlib:println(<<" wrote "/utf8, Output/binary>>),
Errs;
{error, Cause} ->
[{cannot_write_file, Output, Cause} | Errs]
end
end
),
case Errors of
[] ->
{ok, nil};
_ ->
{error, lists:reverse(Errors)}
end.
-file("src/libero/codegen.gleam", 256).
?DOC(
" Generate per-module push wrapper files under `server_generated/`.\n"
" For each message module with `has_msg_from_server == True`, generates a\n"
" `<module_name>.gleam` file with `send_to_client` and `send_to_clients`\n"
" functions that bake in the module string.\n"
).
-spec write_push_wrappers(list(libero@scanner:message_module()), binary()) -> {ok,
nil} |
{error, list(libero@gen_error:gen_error())}.
write_push_wrappers(Message_modules, Server_generated) ->
Msg_from_server_modules = gleam@list:filter(
Message_modules,
fun(M) -> erlang:element(5, M) end
),
Errors = gleam@list:fold(
Msg_from_server_modules,
[],
fun(Errs, M@1) ->
Segment = libero@scanner:last_module_segment(erlang:element(2, M@1)),
Content = <<<<<<<<<<<<"//// Code generated by libero. DO NOT EDIT.
import libero/push
import "/utf8,
(erlang:element(2, M@1))/binary>>/binary,
".{type MsgFromServer}
pub fn send_to_client(
client_id client_id: String,
msg msg: MsgFromServer,
) -> Nil {
push.send_to_client(client_id:, module: \""/utf8>>/binary,
(erlang:element(2, M@1))/binary>>/binary,
"\", msg:)
}
pub fn send_to_clients(
topic topic: String,
msg msg: MsgFromServer,
) -> Nil {
push.send_to_clients(topic:, module: \""/utf8>>/binary,
(erlang:element(2, M@1))/binary>>/binary,
"\", msg:)
}
"/utf8>>,
Output = <<<<<<Server_generated/binary, "/"/utf8>>/binary,
Segment/binary>>/binary,
".gleam"/utf8>>,
ensure_parent_dir(Output),
case simplifile:write(Output, Content) of
{ok, _} ->
gleam_stdlib:println(<<" wrote "/utf8, Output/binary>>),
Errs;
{error, Cause} ->
[{cannot_write_file, Output, Cause} | Errs]
end
end
),
case Errors of
[] ->
{ok, nil};
_ ->
{error, lists:reverse(Errors)}
end.
-file("src/libero/codegen.gleam", 306).
?DOC(
" Generate the server websocket handler at `server_generated/websocket.gleam`.\n"
" Also writes the Erlang FFI for decoding push messages.\n"
).
-spec write_websocket(binary()) -> {ok, nil} |
{error, libero@gen_error:gen_error()}.
write_websocket(Server_generated) ->
Gleam_output = <<Server_generated/binary, "/websocket.gleam"/utf8>>,
Module_path = case gleam@string:split_once(
Server_generated,
<<"src/"/utf8>>
) of
{ok, {_, After}} ->
After;
{error, nil} ->
Server_generated
end,
Ffi_module = <<(gleam@string:replace(
Module_path,
<<"/"/utf8>>,
<<"@"/utf8>>
))/binary,
"@websocket"/utf8>>,
Ffi_output = <<<<"src/"/utf8, Ffi_module/binary>>/binary, "_ffi.erl"/utf8>>,
Content = <<<<<<<<"//// Code generated by libero. DO NOT EDIT.
////
//// WebSocket handler for mist. Handles dispatch, push frame
//// forwarding, and topic cleanup on disconnect.
import gleam/dynamic.{type Dynamic}
import gleam/erlang/process
import gleam/http/request.{type Request}
import gleam/list
import gleam/option.{type Option, Some}
import gleam/string
import libero/push
import libero/ws_logger.{type Logger}
import mist.{type Connection}
import "/utf8,
Module_path/binary>>/binary,
"/dispatch
import server/shared_state.{type SharedState}
pub type ConnState {
ConnState(state: SharedState, topics: List(String), logger: Logger)
}
type PushMsg {
PushFrame(BitArray)
Ignored
}
/// Wire up a WebSocket upgrade with dispatch, push, and topic management.
/// The `logger` argument receives connect/disconnect, panic, and send
/// failure messages. Use `ws_logger.default_logger()` for stdout output
/// or pass your own structured logger.
pub fn upgrade(
request req: Request(Connection),
state state: SharedState,
topics topics: List(String),
logger logger: Logger,
) {
mist.websocket(
request: req,
handler: handler,
on_init: on_init(state, topics, logger),
on_close: fn(state) {
list.each(state.topics, fn(t) { push.leave(topic: t) })
logger.debug(\"WebSocket: disconnected\")
},
)
}
fn on_init(state: SharedState, topics: List(String), logger: Logger) {
fn(_conn: mist.WebsocketConnection) -> #(ConnState, Option(process.Selector(PushMsg))) {
logger.debug(\"WebSocket: connected\")
list.each(topics, fn(t) { push.join(topic: t) })
let selector =
process.new_selector()
|> process.select_other(fn(msg: Dynamic) {
case decode_push_msg(msg) {
Ok(frame) -> PushFrame(frame)
Error(Nil) -> Ignored
}
})
#(ConnState(state:, topics:, logger:), Some(selector))
}
}
fn handler(
state: ConnState,
message: mist.WebsocketMessage(PushMsg),
conn: mist.WebsocketConnection,
) {
case message {
mist.Binary(data) -> {
let #(response_bytes, maybe_panic, new_state) =
dispatch.handle(state: state.state, data:)
case maybe_panic {
Some(info) ->
state.logger.error(
\"RPC panic: \"
<> info.fn_name
<> \" (trace \"
<> info.trace_id
<> \"): \"
<> info.reason,
)
_ -> Nil
}
case mist.send_binary_frame(conn, response_bytes) {
Ok(_) -> Nil
Error(reason) ->
state.logger.warning(
\"Failed to send WebSocket frame: \" <> string.inspect(reason),
)
}
mist.continue(ConnState(..state, state: new_state))
}
mist.Custom(PushFrame(frame)) -> {
case mist.send_binary_frame(conn, frame) {
Ok(_) -> Nil
Error(reason) ->
state.logger.warning(
\"Failed to send WebSocket push frame: \" <> string.inspect(reason),
)
}
mist.continue(state)
}
mist.Custom(Ignored) -> mist.continue(state)
mist.Closed | mist.Shutdown -> mist.stop()
mist.Text(_) -> mist.continue(state)
}
}
@external(erlang, \""/utf8>>/binary,
Ffi_module/binary>>/binary,
"_ffi\", \"decode_push_msg\")
fn decode_push_msg(msg: Dynamic) -> Result(BitArray, Nil)
"/utf8>>,
Ffi_content = <<<<<<"-module('"/utf8, Ffi_module/binary>>/binary,
"_ffi').\n"/utf8>>/binary,
"-export([decode_push_msg/1]).
decode_push_msg({libero_push, Frame}) when is_binary(Frame) ->
{ok, Frame};
decode_push_msg(_) ->
{error, nil}.
"/utf8>>,
ensure_parent_dir(Gleam_output),
gleam@result:'try'(
write_file(Gleam_output, Content),
fun(_) ->
ensure_parent_dir(Ffi_output),
write_file(Ffi_output, Ffi_content)
end
).
-file("src/libero/codegen.gleam", 467).
?DOC(
" Write the tiny Gleam wrapper for type registration.\n"
" Called automatically by generated send functions; safe to call\n"
" multiple times (idempotent on the JS side).\n"
).
-spec write_register_gleam(libero@config:config()) -> {ok, nil} |
{error, libero@gen_error:gen_error()}.
write_register_gleam(Config) ->
Content = <<"//// Code generated by libero. DO NOT EDIT.
////
//// Registers every consumer custom type that might cross the wire
//// so libero's client-side rebuild function can reconstruct tagged
//// ETF terms into Gleam class instances.
////
//// Called automatically by generated send functions. Safe to call
//// manually if needed (idempotent).
pub fn register_all() -> Nil {
do_register_all()
}
@external(javascript, \"./rpc_register_ffi.mjs\", \"registerAll\")
fn do_register_all() -> Nil
"/utf8>>,
Output = erlang:element(8, Config),
ensure_parent_dir(Output),
write_file(Output, Content).
-file("src/libero/codegen.gleam", 634).
-spec write_config(libero@config:config()) -> {ok, nil} |
{error, libero@gen_error:gen_error()}.
write_config(Config) ->
Content = case erlang:element(2, Config) of
{ws_full_url, Url} ->
<<<<"//// Code generated by libero. DO NOT EDIT.
////
//// WebSocket endpoint the client connects to at runtime.
//// Set via --ws-url at generation time. Rerun the libero codegen
//// to regenerate if the URL changes.
pub fn ws_url() -> String {
\""/utf8,
Url/binary>>/binary,
"\"
}
"/utf8>>;
{ws_path_only, Path} ->
<<<<"//// Code generated by libero. DO NOT EDIT.
////
//// WebSocket endpoint resolved at runtime from the browser's location.
//// Set via --ws-path at generation time. The scheme (ws/wss) and host
//// are inferred from window.location so one compiled bundle works
//// across all subdomains.
pub fn ws_url() -> String {
resolve_ws_url(\""/utf8,
Path/binary>>/binary,
"\")
}
@external(javascript, \"./rpc_config_ffi.mjs\", \"resolveWsUrl\")
fn resolve_ws_url(_path: String) -> String {
panic as \"resolve_ws_url requires a browser environment (window.location)\"
}
"/utf8>>
end,
Output = erlang:element(7, Config),
ensure_parent_dir(Output),
gleam@result:'try'(
write_file(Output, Content),
fun(_) -> write_config_ffi(Config) end
).
-file("src/libero/codegen.gleam", 699).
?DOC(
" Generate an Erlang FFI file that pre-registers all constructor atoms\n"
" discovered by the type graph walker, plus framework atoms used by\n"
" libero's wire protocol. Calling `ensure/0` from this module creates\n"
" the atoms in the BEAM atom table so that `binary_to_term([safe])`\n"
" can decode client ETF payloads without rejecting unknown atoms.\n"
).
-spec write_atoms(
libero@config:config(),
list(libero@walker:discovered_variant())
) -> {ok, nil} | {error, libero@gen_error:gen_error()}.
write_atoms(Config, Discovered) ->
Framework_atoms = [<<"ok"/utf8>>,
<<"error"/utf8>>,
<<"some"/utf8>>,
<<"none"/utf8>>,
<<"nil"/utf8>>,
<<"true"/utf8>>,
<<"false"/utf8>>,
<<"app_error"/utf8>>,
<<"malformed_request"/utf8>>,
<<"unknown_function"/utf8>>,
<<"internal_error"/utf8>>,
<<"decode_error"/utf8>>],
Discovered_atoms = gleam@list:map(
Discovered,
fun(V) -> erlang:element(4, V) end
),
All_atoms = begin
_pipe = lists:append(Framework_atoms, Discovered_atoms),
_pipe@1 = gleam@list:unique(_pipe),
gleam@list:sort(_pipe@1, fun gleam@string:compare/2)
end,
Atom_list = begin
_pipe@2 = gleam@list:map(
All_atoms,
fun(Atom) ->
<<<<" <<\""/utf8, Atom/binary>>/binary, "\">>"/utf8>>
end
),
gleam@string:join(_pipe@2, <<",\n"/utf8>>)
end,
Content = <<<<<<<<"%% Code generated by libero. DO NOT EDIT.
%%
%% Pre-registers all constructor atoms that may appear in client ETF
%% payloads, so binary_to_term([safe]) can decode them without
%% rejecting unknown atoms.
%%
%% ensure/0 uses persistent_term as a one-shot guard so the
%% binary_to_atom calls only run once per VM lifetime.
%%
%% lists:foreach + fun is used instead of bare binary_to_atom calls
%% because the Erlang compiler optimizes away pure BIF calls whose
%% results are discarded.
-module("/utf8,
(erlang:element(6, Config))/binary>>/binary,
").
-export([ensure/0]).
ensure() ->
case persistent_term:get({?MODULE, done}, false) of
true -> nil;
false -> do_ensure()
end.
do_ensure() ->
lists:foreach(fun(B) -> binary_to_atom(B) end, [
"/utf8>>/binary,
Atom_list/binary>>/binary,
"
]),
persistent_term:put({?MODULE, done}, true),
nil.
"/utf8>>,
Output = erlang:element(5, Config),
ensure_parent_dir(Output),
write_file(Output, Content).
-file("src/libero/codegen.gleam", 814).
?DOC(
" Convert a Gleam module path like \"shared/discount\" to its compiled\n"
" .mjs bundle path \"shared/shared/discount.mjs\". The first segment\n"
" is the package name (Gleam convention) and is repeated because\n"
" the bundle layout is `<package>/<module_path>.mjs`.\n"
).
-spec module_to_mjs_path(binary()) -> binary().
module_to_mjs_path(Module_path) ->
case gleam@string:split_once(Module_path, <<"/"/utf8>>) of
{error, nil} ->
<<<<<<Module_path/binary, "/"/utf8>>/binary, Module_path/binary>>/binary,
".mjs"/utf8>>;
{ok, {Package, _}} ->
<<<<<<Package/binary, "/"/utf8>>/binary, Module_path/binary>>/binary,
".mjs"/utf8>>
end.
-file("src/libero/codegen.gleam", 492).
?DOC(
" Write the FFI .mjs file with explicit imports and registerConstructor\n"
" calls for every variant discovered by the type graph walker.\n"
).
-spec write_register_ffi(
libero@config:config(),
list(libero@walker:discovered_variant())
) -> {ok, nil} | {error, libero@gen_error:gen_error()}.
write_register_ffi(Config, Discovered) ->
Prefix = erlang:element(10, Config),
Distinct_modules = begin
_pipe = gleam@list:fold(
Discovered,
{[], gleam@set:new()},
fun(Acc, V) ->
{Modules_acc, Seen} = Acc,
case gleam@set:contains(Seen, erlang:element(2, V)) of
true ->
Acc;
false ->
{lists:append(Modules_acc, [erlang:element(2, V)]),
gleam@set:insert(Seen, erlang:element(2, V))}
end
end
),
(fun(Pair) -> erlang:element(1, Pair) end)(_pipe)
end,
Module_aliases = gleam@list:index_fold(
Distinct_modules,
maps:new(),
fun(Acc@1, Module_path, I) ->
gleam@dict:insert(
Acc@1,
Module_path,
<<"_m"/utf8, (erlang:integer_to_binary(I))/binary>>
)
end
),
Libero_import = <<<<"import { registerConstructor, registerFloatFields, setListCtors, setDictFromList, setGleamCustomType } from \""/utf8,
Prefix/binary>>/binary,
"libero/libero/rpc_ffi.mjs\";"/utf8>>,
Framework_imports = [<<<<"import { Ok, Error, CustomType, Empty, NonEmpty } from \""/utf8,
Prefix/binary>>/binary,
"gleam_stdlib/gleam.mjs\";"/utf8>>,
<<<<"import { DecodeError } from \""/utf8, Prefix/binary>>/binary,
"libero/libero/wire.mjs\";"/utf8>>,
<<<<"import { AppError, MalformedRequest, UnknownFunction, InternalError } from \""/utf8,
Prefix/binary>>/binary,
"libero/libero/error.mjs\";"/utf8>>,
<<<<"import { Some, None } from \""/utf8, Prefix/binary>>/binary,
"gleam_stdlib/gleam/option.mjs\";"/utf8>>,
<<<<"import { from_list as dictFromList } from \""/utf8, Prefix/binary>>/binary,
"gleam_stdlib/gleam/dict.mjs\";"/utf8>>],
Module_imports = gleam@list:map(
Distinct_modules,
fun(Module_path@1) ->
Alias = begin
_pipe@1 = gleam_stdlib:map_get(Module_aliases, Module_path@1),
gleam@result:unwrap(_pipe@1, <<"_m0"/utf8>>)
end,
<<<<<<<<<<"import * as "/utf8, Alias/binary>>/binary,
" from \""/utf8>>/binary,
Prefix/binary>>/binary,
(module_to_mjs_path(Module_path@1))/binary>>/binary,
"\";"/utf8>>
end
),
Register_calls = gleam@list:map(
Discovered,
fun(V@1) ->
Alias@1 = begin
_pipe@2 = gleam_stdlib:map_get(
Module_aliases,
erlang:element(2, V@1)
),
gleam@result:unwrap(_pipe@2, <<"_m0"/utf8>>)
end,
<<<<<<<<<<<<<<<<<<<<" if ("/utf8, Alias@1/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(3, V@1))/binary>>/binary,
") registerConstructor(\""/utf8>>/binary,
(erlang:element(4, V@1))/binary>>/binary,
"\", "/utf8>>/binary,
Alias@1/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(3, V@1))/binary>>/binary,
");"/utf8>>
end
),
Float_field_calls = gleam@list:filter_map(
Discovered,
fun(V@2) -> case erlang:element(5, V@2) of
[] ->
{error, nil};
Indices ->
Indices_str = begin
_pipe@3 = gleam@list:map(
Indices,
fun erlang:integer_to_binary/1
),
gleam@string:join(_pipe@3, <<", "/utf8>>)
end,
{ok,
<<<<<<<<" registerFloatFields(\""/utf8,
(erlang:element(4, V@2))/binary>>/binary,
"\", ["/utf8>>/binary,
Indices_str/binary>>/binary,
"]);"/utf8>>}
end end
),
Framework_register_calls = [<<" // Framework types"/utf8>>,
<<" setGleamCustomType(CustomType);"/utf8>>,
<<" setListCtors(Empty, NonEmpty);"/utf8>>,
<<" setDictFromList(dictFromList);"/utf8>>,
<<" registerConstructor(\"ok\", Ok);"/utf8>>,
<<" registerConstructor(\"error\", Error);"/utf8>>,
<<" registerConstructor(\"some\", Some);"/utf8>>,
<<" registerConstructor(\"none\", None);"/utf8>>,
<<" registerConstructor(\"decode_error\", DecodeError);"/utf8>>,
<<" registerConstructor(\"app_error\", AppError);"/utf8>>,
<<" registerConstructor(\"malformed_request\", MalformedRequest);"/utf8>>,
<<" registerConstructor(\"unknown_function\", UnknownFunction);"/utf8>>,
<<" registerConstructor(\"internal_error\", InternalError);"/utf8>>],
All_calls = lists:append(
[Framework_register_calls, Register_calls, Float_field_calls]
),
Content = <<<<<<<<<<<<<<<<"// Code generated by libero. DO NOT EDIT.
//
// Registers framework and application custom types for wire codec
// reconstruction. Framework types (Ok, Error, Some, None, etc.) are
// always included. Application types are discovered by walking the
// MsgFromClient/MsgFromServer type graphs at generation time.
//
// Called automatically by generated send functions. Idempotent -
// safe to call multiple times (only runs registration once).
"/utf8,
Libero_import/binary>>/binary,
"\n"/utf8>>/binary,
(gleam@string:join(Framework_imports, <<"\n"/utf8>>))/binary>>/binary,
"\n"/utf8>>/binary,
(gleam@string:join(Module_imports, <<"\n"/utf8>>))/binary>>/binary,
"
let registered = false;
export function registerAll() {
if (registered) return;
try {
registered = true;
"/utf8>>/binary,
(gleam@string:join(All_calls, <<"\n"/utf8>>))/binary>>/binary,
"
} catch (e) {
registered = false;
const msg = \"libero: type registration failed. \"
+ \"This usually means the generated code is stale or the project \"
+ \"has not been compiled yet. Re-run: gleam run -m libero -- ...\\n\"
+ \"Original error: \" + (e.message || e);
throw new globalThis.Error(msg);
}
}
"/utf8>>,
Output = erlang:element(9, Config),
ensure_parent_dir(Output),
write_file(Output, Content).
-file("src/libero/codegen.gleam", 451).
?DOC(
" Write the client-side type registration files (gleam wrapper + mjs FFI).\n"
" Uses the pre-discovered variant list instead of walking from function signatures.\n"
).
-spec write_register(
libero@config:config(),
list(libero@walker:discovered_variant())
) -> {ok, nil} | {error, list(libero@gen_error:gen_error())}.
write_register(Config, Discovered) ->
gleam@result:'try'(
begin
_pipe = write_register_gleam(Config),
gleam@result:map_error(_pipe, fun(E) -> [E] end)
end,
fun(_) ->
gleam@result:'try'(
begin
_pipe@1 = write_register_ffi(Config, Discovered),
gleam@result:map_error(_pipe@1, fun(E@1) -> [E@1] end)
end,
fun(_) -> {ok, nil} end
)
end
).