Current section
Files
Jump to
Current section
Files
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([emit_typed_decoders/1, write_if_missing/2, extract_dir/1, write_dispatch/5, write_send_functions/2, write_push_wrappers/2, write_websocket/2, write_decoders_gleam/1, write_config/1, write_atoms/2, write_ssr_flags/1, write_main/5, module_to_mjs_path/1, write_decoders_ffi/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", 202).
?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", 209).
?DOC(
" Convert a message module path to a safe Gleam import alias.\n"
" Uses a \"_msg\" suffix to avoid collision with handler_alias.\n"
" e.g. \"shared/messages/admin\" -> \"shared_messages_admin_msg\"\n"
).
-spec message_alias(binary()) -> binary().
message_alias(Module_path) ->
<<(gleam@string:replace(Module_path, <<"/"/utf8>>, <<"_"/utf8>>))/binary,
"_msg"/utf8>>.
-file("src/libero/codegen.gleam", 601).
?DOC(
" Convert a module path to a flat JS identifier for use as a namespace alias.\n"
" e.g. \"shared/line_item\" -> \"shared_line_item\"\n"
).
-spec module_alias(binary()) -> binary().
module_alias(Module_path) ->
gleam@string:replace(Module_path, <<"/"/utf8>>, <<"_"/utf8>>).
-file("src/libero/codegen.gleam", 607).
?DOC(
" Derive the JS decoder function name for a discovered type.\n"
" e.g. (\"shared/line_item\", \"Status\") -> \"decode_shared_line_item_status\"\n"
).
-spec decoder_fn_name(binary(), binary()) -> binary().
decoder_fn_name(Module_path, Type_name) ->
<<<<<<"decode_"/utf8, (module_alias(Module_path))/binary>>/binary,
"_"/utf8>>/binary,
(libero@walker:to_snake_case(Type_name))/binary>>.
-file("src/libero/codegen.gleam", 615).
?DOC(" True when every variant in the list has zero fields (pure enum).\n").
-spec all_variants_zero_arity(list(libero@walker:discovered_variant())) -> boolean().
all_variants_zero_arity(Variants) ->
gleam@list:all(
Variants,
fun(V) -> gleam@list:is_empty(erlang:element(6, V)) end
).
-file("src/libero/codegen.gleam", 639).
?DOC(" Emit the body of a pure-enum decoder (no-field variants only).\n").
-spec emit_enum_decoder(list(libero@walker:discovered_variant())) -> binary().
emit_enum_decoder(Variants) ->
Cases = gleam@list:map(
Variants,
fun(V) ->
<<<<<<<<<<<<" if (term === \""/utf8,
(erlang:element(4, V))/binary>>/binary,
"\") return new _m_"/utf8>>/binary,
(module_alias(erlang:element(2, V)))/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(3, V))/binary>>/binary,
"();"/utf8>>
end
),
<<(gleam@string:join(Cases, <<"\n"/utf8>>))/binary,
"\n throw new DecodeError(\"unknown variant: \" + String(term));"/utf8>>.
-file("src/libero/codegen.gleam", 726).
-spec field_decoder_call_depth(libero@walker:field_type(), binary(), integer()) -> binary().
field_decoder_call_depth(Ft, Term_expr, Depth) ->
Param = <<"t"/utf8, (erlang:integer_to_binary(Depth))/binary>>,
Next = Depth + 1,
case Ft of
int_field ->
<<<<"decode_int("/utf8, Term_expr/binary>>/binary, ")"/utf8>>;
float_field ->
<<<<"decode_float("/utf8, Term_expr/binary>>/binary, ")"/utf8>>;
string_field ->
<<<<"decode_string("/utf8, Term_expr/binary>>/binary, ")"/utf8>>;
bool_field ->
<<<<"decode_bool("/utf8, Term_expr/binary>>/binary, ")"/utf8>>;
bit_array_field ->
<<<<"decode_bit_array("/utf8, Term_expr/binary>>/binary, ")"/utf8>>;
nil_field ->
<<<<"decode_nil("/utf8, Term_expr/binary>>/binary, ")"/utf8>>;
{list_of, Inner} ->
<<<<<<<<<<<<"decode_list_of(("/utf8, Param/binary>>/binary,
") => "/utf8>>/binary,
(field_decoder_call_depth(Inner, Param, Next))/binary>>/binary,
", "/utf8>>/binary,
Term_expr/binary>>/binary,
")"/utf8>>;
{option_of, Inner@1} ->
<<<<<<<<<<<<"decode_option_of(("/utf8, Param/binary>>/binary,
") => "/utf8>>/binary,
(field_decoder_call_depth(Inner@1, Param, Next))/binary>>/binary,
", "/utf8>>/binary,
Term_expr/binary>>/binary,
")"/utf8>>;
{result_of, Ok, Err} ->
<<<<<<<<<<<<<<<<<<<<"decode_result_of(("/utf8, Param/binary>>/binary,
") => "/utf8>>/binary,
(field_decoder_call_depth(
Ok,
Param,
Next
))/binary>>/binary,
", ("/utf8>>/binary,
Param/binary>>/binary,
") => "/utf8>>/binary,
(field_decoder_call_depth(Err, Param, Next))/binary>>/binary,
", "/utf8>>/binary,
Term_expr/binary>>/binary,
")"/utf8>>;
{dict_of, K, V} ->
<<<<<<<<<<<<<<<<<<<<"decode_dict_of(("/utf8, Param/binary>>/binary,
") => "/utf8>>/binary,
(field_decoder_call_depth(
K,
Param,
Next
))/binary>>/binary,
", ("/utf8>>/binary,
Param/binary>>/binary,
") => "/utf8>>/binary,
(field_decoder_call_depth(V, Param, Next))/binary>>/binary,
", "/utf8>>/binary,
Term_expr/binary>>/binary,
")"/utf8>>;
{tuple_of, Elems} ->
Decoders = gleam@list:map(
Elems,
fun(E) ->
<<<<<<"("/utf8, Param/binary>>/binary, ") => "/utf8>>/binary,
(field_decoder_call_depth(E, Param, Next))/binary>>
end
),
<<<<<<<<"decode_tuple_of(["/utf8,
(gleam@string:join(Decoders, <<", "/utf8>>))/binary>>/binary,
"], "/utf8>>/binary,
Term_expr/binary>>/binary,
")"/utf8>>;
{type_var, Name} ->
<<<<"(() => { throw new DecodeError(\"TypeVar<"/utf8, Name/binary>>/binary,
"> not supported at runtime\"); })()"/utf8>>;
{user_type, Module_path, Type_name, _} ->
<<<<<<(decoder_fn_name(Module_path, Type_name))/binary, "("/utf8>>/binary,
Term_expr/binary>>/binary,
")"/utf8>>
end.
-file("src/libero/codegen.gleam", 718).
?DOC(
" Produce the JS expression that decodes `term_expr` according to `ft`.\n"
" `depth` tracks nesting to generate unique lambda param names (t0, t1, ...).\n"
).
-spec field_decoder_call(libero@walker:field_type(), binary()) -> binary().
field_decoder_call(Ft, Term_expr) ->
field_decoder_call_depth(Ft, Term_expr, 0).
-file("src/libero/codegen.gleam", 655).
?DOC(" Emit the body of a single-variant record decoder.\n").
-spec emit_record_decoder(libero@walker:discovered_variant()) -> binary().
emit_record_decoder(Variant) ->
Field_lines = gleam@list:index_map(
erlang:element(6, Variant),
fun(Ft, I) ->
<<" "/utf8,
(field_decoder_call(
Ft,
<<<<"term["/utf8, (erlang:integer_to_binary(I + 1))/binary>>/binary,
"]"/utf8>>
))/binary>>
end
),
<<<<<<<<<<<<" return new _m_"/utf8,
(module_alias(erlang:element(2, Variant)))/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(3, Variant))/binary>>/binary,
"(\n"/utf8>>/binary,
(gleam@string:join(Field_lines, <<",\n"/utf8>>))/binary>>/binary,
"\n );"/utf8>>.
-file("src/libero/codegen.gleam", 674).
?DOC(
" Emit the body of a multi-variant tagged union decoder.\n"
" `error_label` is used in the default throw message (e.g. \"variant\" or\n"
" \"MsgFromServer variant\").\n"
).
-spec emit_tagged_union_decoder(
list(libero@walker:discovered_variant()),
binary()
) -> binary().
emit_tagged_union_decoder(Variants, Error_label) ->
Arms = gleam@list:map(Variants, fun(V) -> case erlang:element(6, V) of
[] ->
<<<<<<<<<<<<" case \""/utf8,
(erlang:element(4, V))/binary>>/binary,
"\":\n return new _m_"/utf8>>/binary,
(module_alias(erlang:element(2, V)))/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(3, V))/binary>>/binary,
"();"/utf8>>;
Fields ->
Field_args = gleam@list:index_map(
Fields,
fun(Ft, I) ->
field_decoder_call(
Ft,
<<<<"term["/utf8,
(erlang:integer_to_binary(I + 1))/binary>>/binary,
"]"/utf8>>
)
end
),
<<<<<<<<<<<<<<<<" case \""/utf8,
(erlang:element(4, V))/binary>>/binary,
"\":\n return new _m_"/utf8>>/binary,
(module_alias(erlang:element(2, V)))/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(3, V))/binary>>/binary,
"("/utf8>>/binary,
(gleam@string:join(Field_args, <<", "/utf8>>))/binary>>/binary,
");"/utf8>>
end end),
<<<<<<<<<<<<" const tag = Array.isArray(term) ? term[0] : term;\n"/utf8,
" switch (tag) {\n"/utf8>>/binary,
(gleam@string:join(Arms, <<"\n"/utf8>>))/binary>>/binary,
"\n default:\n throw new DecodeError(\"unknown "/utf8>>/binary,
Error_label/binary>>/binary,
": \" + String(tag));\n"/utf8>>/binary,
" }"/utf8>>.
-file("src/libero/codegen.gleam", 620).
?DOC(" Emit one JS decoder function for a discovered type.\n").
-spec emit_type_decoder(libero@walker:discovered_type()) -> binary().
emit_type_decoder(T) ->
Fn_name = decoder_fn_name(erlang:element(2, T), erlang:element(3, T)),
Body = case erlang:element(5, T) of
[] ->
<<" throw new DecodeError(\"empty type\");"/utf8>>;
[Single] ->
case erlang:element(6, Single) of
[] ->
emit_enum_decoder(erlang:element(5, T));
_ ->
emit_record_decoder(Single)
end;
Variants ->
case all_variants_zero_arity(Variants) of
true ->
emit_enum_decoder(Variants);
false ->
emit_tagged_union_decoder(Variants, <<"variant"/utf8>>)
end
end,
<<<<<<<<"export function "/utf8, Fn_name/binary>>/binary,
"(term) {\n"/utf8>>/binary,
Body/binary>>/binary,
"\n}"/utf8>>.
-file("src/libero/codegen.gleam", 792).
?DOC(
" Emit the `decode_msg_from_server` entry point function.\n"
" Delegates to the per-type decoder to avoid duplicating the switch body.\n"
" If no MsgFromServer type is found in the discovered list, returns \"\".\n"
).
-spec emit_msg_from_server_decoder(list(libero@walker:discovered_type())) -> binary().
emit_msg_from_server_decoder(Discovered) ->
case gleam@list:find(
Discovered,
fun(T) -> erlang:element(3, T) =:= <<"MsgFromServer"/utf8>> end
) of
{error, _} ->
<<""/utf8>>;
{ok, T@1} ->
Fn_name = decoder_fn_name(
erlang:element(2, T@1),
erlang:element(3, T@1)
),
<<<<<<<<"export function decode_msg_from_server(term) {\n"/utf8,
" return "/utf8>>/binary,
Fn_name/binary>>/binary,
"(term);\n"/utf8>>/binary,
"}"/utf8>>
end.
-file("src/libero/codegen.gleam", 492).
?DOC(
" Emit a JS string with one decoder function per discovered type and a\n"
" `decode_msg_from_server` entry point. Does not write to disk - exposed\n"
" so tests can assert on the output without filesystem I/O.\n"
).
-spec emit_typed_decoders(list(libero@walker:discovered_type())) -> binary().
emit_typed_decoders(Discovered) ->
Type_decoders = gleam@list:map(
Discovered,
fun(T) -> emit_type_decoder(T) end
),
Entry = emit_msg_from_server_decoder(Discovered),
Parts = gleam@list:filter(
[gleam@string:join(Type_decoders, <<"\n\n"/utf8>>), Entry],
fun(S) -> S /= <<""/utf8>> end
),
gleam@string:join(Parts, <<"\n\n"/utf8>>).
-file("src/libero/codegen.gleam", 1172).
?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", 842).
?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", 1156).
?DOC(
" Write content to a file, skipping if the file already exists.\n"
" Used for scaffolded files that users may customize after generation.\n"
).
-spec write_if_missing(binary(), binary()) -> {ok, nil} |
{error, libero@gen_error:gen_error()}.
write_if_missing(Path, Content) ->
case begin
_pipe = simplifile_erl:is_file(Path),
gleam@result:unwrap(_pipe, false)
end of
true ->
gleam_stdlib:println(
<<<<" kept "/utf8, Path/binary>>/binary,
" (exists, not overwriting)"/utf8>>
),
{ok, nil};
false ->
write_file(Path, Content)
end.
-file("src/libero/codegen.gleam", 1187).
?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", 1216).
-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", 1211).
?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", 18).
?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(),
binary(),
binary()
) -> {ok, nil} | {error, libero@gen_error:gen_error()}.
write_dispatch(
Message_modules,
Server_generated,
Atoms_module,
Shared_state_module,
App_error_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 dispatch(state, fn() {\n"/utf8>>/binary,
Body/binary>>/binary,
"\n result\n })"/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 "/utf8,
App_error_module/binary>>/binary,
".{type AppError"/utf8>>/binary,
(case Needs_unhandled_import of
true ->
<<", UnhandledMessage"/utf8>>;
false ->
<<""/utf8>>
end)/binary>>/binary,
"}
import "/utf8>>/binary,
Shared_state_module/binary>>/binary,
".{type SharedState}
"/utf8>>/binary,
(gleam@string:join(All_imports, <<"\n"/utf8>>))/binary>>/binary,
"
@external(erlang, \""/utf8>>/binary,
Atoms_module/binary>>/binary,
"\", \"ensure\")
pub fn ensure_atoms() -> Nil
pub fn handle(
state state: SharedState,
data data: BitArray,
) -> #(BitArray, Option(PanicInfo), SharedState) {
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))) ->
// Ship the full MsgFromServer envelope on the response so the client's
// typed decoder handles both push and response frames uniformly.
// Encoding runs under try_call so any serialization panic (e.g.
// a value containing an unencodable term) is captured and surfaced
// as InternalError instead of crashing the websocket handler.
safe_encode(fn() { wire.encode(Ok(value)) }, new_state, \"dispatch_encode_ok\")
Ok(Error(app_err)) ->
safe_encode(fn() { wire.encode(Error(error.AppError(app_err))) }, state, \"dispatch_encode_app_err\")
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,
)
}
}
}
/// Run an encoder thunk under try_call protection. If encoding panics
/// (e.g. a response value contains an unencodable term), return an
/// InternalError response with the panic reason so the websocket handler
/// can log it and stay alive.
fn safe_encode(
encoder: fn() -> BitArray,
state: SharedState,
fn_name: String,
) -> #(BitArray, Option(PanicInfo), SharedState) {
case trace.try_call(encoder) {
Ok(bytes) -> #(wire.tag_response(bytes), None, state)
Error(reason) -> {
let trace_id = trace.new_trace_id()
#(
wire.tag_response(wire.encode(Error(InternalError(trace_id, \"Response encoding failed\")))),
Some(error.PanicInfo(trace_id:, fn_name:, reason:)),
state,
)
}
}
}
"/utf8>>,
Output = <<Server_generated/binary, "/dispatch.gleam"/utf8>>,
ensure_parent_dir(Output),
write_file(Output, Content).
-file("src/libero/codegen.gleam", 218).
?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_decoders.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) {
// Reference rpc_decoders to ensure its side-effecting module-level code
// (constructor setters, decoder registration) runs before any RPC call.
let _ = rpc_decoders.decode_msg_from_server
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.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", 284).
?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", 334).
?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(), binary()) -> {ok, nil} |
{error, libero@gen_error:gen_error()}.
write_websocket(Server_generated, Shared_state_module) ->
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,
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/decode
import gleam/erlang/atom
import gleam/erlang/process
import gleam/http/request.{type Request}
import gleam/list
import gleam/option.{type Option, Some}
import gleam/result
import gleam/string
import libero/push
import libero/ws_logger.{type Logger}
import mist.{type Connection}
import "/utf8,
Module_path/binary>>/binary,
"/dispatch
import "/utf8>>/binary,
Shared_state_module/binary>>/binary,
".{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_record(
tag: atom.create(\"libero_push\"),
fields: 1,
mapping: fn(record) {
{ use frame <- decode.field(1, decode.bit_array)
decode.success(PushFrame(frame))
}
|> decode.run(record, _)
|> result.unwrap(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)
}
}
"/utf8>>,
ensure_parent_dir(Gleam_output),
write_file(Gleam_output, Content).
-file("src/libero/codegen.gleam", 470).
?DOC(
" Write the Gleam wrapper for the typed decoder FFI.\n"
" Surfaces `decode_msg_from_server` from the generated JS FFI file to\n"
" Gleam callers. JS-only - no Erlang target fallback.\n"
).
-spec write_decoders_gleam(libero@config:config()) -> {ok, nil} |
{error, libero@gen_error:gen_error()}.
write_decoders_gleam(Config) ->
Content = <<"//// Code generated by libero. DO NOT EDIT.
////
//// Gleam wrapper for the typed decoder FFI. Exposes
//// decode_msg_from_server to Gleam callers on the JavaScript target.
import gleam/dynamic.{type Dynamic}
@external(javascript, \"./rpc_decoders_ffi.mjs\", \"decode_msg_from_server\")
pub fn decode_msg_from_server(raw: Dynamic) -> Dynamic
"/utf8>>,
Output = erlang:element(10, Config),
ensure_parent_dir(Output),
write_file(Output, Content).
-file("src/libero/codegen.gleam", 806).
-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", 871).
?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_type())) -> {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 = begin
_pipe = gleam@list:flat_map(
Discovered,
fun(T) -> erlang:element(5, T) end
),
gleam@list:map(_pipe, fun(V) -> erlang:element(4, V) end)
end,
All_atoms = begin
_pipe@1 = lists:append(Framework_atoms, Discovered_atoms),
_pipe@2 = gleam@list:unique(_pipe@1),
gleam@list:sort(_pipe@2, fun gleam@string:compare/2)
end,
Atom_list = begin
_pipe@3 = gleam@list:map(
All_atoms,
fun(Atom) ->
<<<<" <<\""/utf8, Atom/binary>>/binary, "\">>"/utf8>>
end
),
gleam@string:join(_pipe@3, <<",\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", 931).
?DOC(
" Generate the client-side SSR flags reader (gleam + mjs).\n"
" Produces a read_flags() function that reads window.__LIBERO_FLAGS__\n"
" and returns it as a Dynamic value for ssr.decode_flags().\n"
).
-spec write_ssr_flags(binary()) -> {ok, nil} |
{error, libero@gen_error:gen_error()}.
write_ssr_flags(Client_generated) ->
Gleam_path = <<Client_generated/binary, "/ssr.gleam"/utf8>>,
Ffi_path = <<Client_generated/binary, "/ssr_ffi.mjs"/utf8>>,
Gleam_content = <<"//// Code generated by libero. DO NOT EDIT.
////
//// SSR flags reader for hydration. Reads the base64-encoded ETF
//// flags embedded by the server in window.__LIBERO_FLAGS__.
import gleam/dynamic.{type Dynamic}
/// Read the SSR flags embedded in the page by the server.
/// Returns a Dynamic value suitable for passing to ssr.decode_flags().
@external(javascript, \"./ssr_ffi.mjs\", \"readFlags\")
pub fn read_flags() -> Dynamic {
// Unreachable on the Erlang target - read_flags is client-only.
panic as \"ssr.read_flags requires a browser environment\"
}
"/utf8>>,
Ffi_content = <<"// Code generated by libero. DO NOT EDIT.
//
// Reads SSR flags embedded by the server for client hydration.
export function readFlags() {
return globalThis.__LIBERO_FLAGS__ ?? \"\";
}
"/utf8>>,
ensure_parent_dir(Gleam_path),
gleam@result:'try'(
write_file(Gleam_path, Gleam_content),
fun(_) -> write_file(Ffi_path, Ffi_content) end
).
-file("src/libero/codegen.gleam", 972).
?DOC(" Generate the server entry point at `src/<app_name>.gleam`.\n").
-spec write_main(binary(), integer(), binary(), binary(), list(binary())) -> {ok,
nil} |
{error, libero@gen_error:gen_error()}.
write_main(
App_name,
Port,
Server_generated,
Shared_state_module,
Js_client_names
) ->
Output = <<<<"src/"/utf8,
(gleam@string:replace(App_name, <<"-"/utf8>>, <<"_"/utf8>>))/binary>>/binary,
".gleam"/utf8>>,
Dispatch_module = case gleam@string:split_once(
Server_generated,
<<"src/"/utf8>>
) of
{ok, {_, After}} ->
After;
{error, nil} ->
Server_generated
end,
Ws_module = <<Dispatch_module/binary, "/websocket"/utf8>>,
{Js_routes, Index_route} = case Js_client_names of
[] ->
{<<""/utf8>>,
<<"
_, _ ->
response.new(404)
|> response.set_body(mist.Bytes(bytes_tree.from_string(\"Not found\")))"/utf8>>};
[First_js | _] ->
Routes = begin
_pipe = gleam@list:map(
Js_client_names,
fun(Name) ->
<<<<<<<<" _, [\""/utf8, Name/binary>>/binary,
"\", ..path] ->
serve_file(
\"clients/"/utf8>>/binary,
Name/binary>>/binary,
"/build/dev/javascript/\" <> string.join(path, \"/\"),
)"/utf8>>
end
),
_pipe@1 = gleam@string:join(_pipe, <<"\n"/utf8>>),
(fun(R) -> <<"\n"/utf8, R/binary>> end)(_pipe@1)
end,
Index = <<<<<<<<"
_, _ -> serve_html(\"<!DOCTYPE html>
<html>
<head>
<meta charset=\\\"utf-8\\\">
<meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1\\\">
</head>
<body>
<div id=\\\"app\\\"></div>
<script type=\\\"module\\\">
import { main } from \\\"/"/utf8,
First_js/binary>>/binary,
"/"/utf8>>/binary,
First_js/binary>>/binary,
"/app.mjs\\\";
main();
</script>
</body>
</html>\")"/utf8>>,
{Routes, Index}
end,
Content = <<<<<<<<<<<<<<<<<<<<<<"//// Code generated by libero. DO NOT EDIT.
import gleam/bytes_tree
import gleam/erlang/process
import gleam/http
import gleam/http/request.{type Request}
import gleam/http/response
import gleam/list
import gleam/option.{None, Some}
import gleam/string
import libero/push
import libero/ws_logger
import mist.{type Connection}
import "/utf8,
Dispatch_module/binary>>/binary,
"/dispatch
import "/utf8>>/binary,
Ws_module/binary>>/binary,
" as ws
import "/utf8>>/binary,
Shared_state_module/binary>>/binary,
"
pub fn main() {
let _ = push.init()
let _ = dispatch.ensure_atoms()
let state = shared_state.new()
let logger = ws_logger.default_logger()
let assert Ok(_) =
fn(req: Request(Connection)) {
case req.method, request.path_segments(req) {
_, [\"ws\"] ->
ws.upgrade(
request: req,
state:,
topics: [],
logger:,
)
http.Post, [\"rpc\"] -> handle_rpc(req, state, logger)"/utf8>>/binary,
Js_routes/binary>>/binary,
Index_route/binary>>/binary,
"
}
}
|> mist.new
|> mist.port("/utf8>>/binary,
(erlang:integer_to_binary(Port))/binary>>/binary,
")
|> mist.start
process.sleep_forever()
}
fn handle_rpc(
req: Request(Connection),
state: shared_state.SharedState,
logger: ws_logger.Logger,
) -> response.Response(mist.ResponseData) {
// Note: HTTP RPC is stateless — state mutations are not persisted across
// requests. Use WebSocket for stateful interactions.
case mist.read_body(req, 1_000_000) {
Ok(req) -> {
let #(response_bytes, maybe_panic, _new_state) =
dispatch.handle(state:, data: req.body)
case maybe_panic {
Some(info) ->
logger.error(
\"RPC panic: \"
<> info.fn_name
<> \" (trace \"
<> info.trace_id
<> \"): \"
<> info.reason,
)
None -> Nil
}
response.new(200)
|> response.set_header(\"content-type\", \"application/octet-stream\")
|> response.set_body(mist.Bytes(bytes_tree.from_bit_array(response_bytes)))
}
Error(_) ->
response.new(400)
|> response.set_body(mist.Bytes(bytes_tree.from_string(\"Bad request\")))
}
}
fn serve_html(html: String) -> response.Response(mist.ResponseData) {
response.new(200)
|> response.set_header(\"content-type\", \"text/html\")
|> response.set_body(mist.Bytes(bytes_tree.from_string(html)))
}
fn serve_file(
path: String,
) -> response.Response(mist.ResponseData) {
case mist.send_file(path, offset: 0, limit: None) {
Ok(body) ->
response.new(200)
|> response.set_header(\"content-type\", content_type(path))
|> response.set_body(body)
Error(_) ->
response.new(404)
|> response.set_body(mist.Bytes(bytes_tree.from_string(\"Not found\")))
}
}
fn content_type(path: String) -> String {
case string.split(path, \".\") |> list.last {
Ok(\"js\") | Ok(\"mjs\") -> \"application/javascript\"
Ok(\"css\") -> \"text/css\"
Ok(\"html\") -> \"text/html\"
Ok(\"json\") -> \"application/json\"
Ok(\"wasm\") -> \"application/wasm\"
Ok(\"svg\") -> \"image/svg+xml\"
Ok(\"png\") -> \"image/png\"
Ok(\"ico\") -> \"image/x-icon\"
Ok(\"map\") -> \"application/json\"
_ -> \"application/octet-stream\"
}
}
"/utf8>>,
ensure_parent_dir(Output),
write_if_missing(Output, Content).
-file("src/libero/codegen.gleam", 1231).
?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", 551).
?DOC(" Emit a JS import block for the typed decoders file.\n").
-spec emit_decoder_imports(
list(libero@walker:discovered_type()),
libero@config:config()
) -> binary().
emit_decoder_imports(Discovered, Config) ->
Module_paths = begin
_pipe = gleam@list:fold(
Discovered,
{[], gleam@set:new()},
fun(Acc, T) ->
{Paths_acc, Seen} = Acc,
case gleam@set:contains(Seen, erlang:element(2, T)) of
true ->
Acc;
false ->
{lists:append(Paths_acc, [erlang:element(2, T)]),
gleam@set:insert(Seen, erlang:element(2, T))}
end
end
),
(fun(Pair) -> erlang:element(1, Pair) end)(_pipe)
end,
Prelude_import = <<<<<<<<<<<<"import { decode_int, decode_float, decode_string, decode_bool, "/utf8,
"decode_bit_array, decode_nil, decode_list_of, decode_option_of, "/utf8>>/binary,
"decode_result_of, decode_dict_of, decode_tuple_of, DecodeError, "/utf8>>/binary,
"setMsgFromServerDecoder, setResultCtors, setOptionCtors, setListCtors, "/utf8>>/binary,
"setDictFromList } from \""/utf8>>/binary,
(erlang:element(11, Config))/binary>>/binary,
"\";"/utf8>>,
Prefix = erlang:element(8, Config),
Stdlib_imports = <<<<<<<<<<<<<<<<"import { Ok, Error as ResultError, Empty, NonEmpty } from \""/utf8,
Prefix/binary>>/binary,
"gleam_stdlib/gleam.mjs\";\n"/utf8>>/binary,
"import { Some, None } from \""/utf8>>/binary,
Prefix/binary>>/binary,
"gleam_stdlib/gleam/option.mjs\";\n"/utf8>>/binary,
"import { from_list as dictFromList } from \""/utf8>>/binary,
Prefix/binary>>/binary,
"gleam_stdlib/gleam/dict.mjs\";"/utf8>>,
Module_imports = gleam@list:map(
Module_paths,
fun(Mp) ->
<<<<<<<<<<"import * as _m_"/utf8, (module_alias(Mp))/binary>>/binary,
" from \""/utf8>>/binary,
(erlang:element(8, Config))/binary>>/binary,
(module_to_mjs_path(Mp))/binary>>/binary,
"\";"/utf8>>
end
),
gleam@string:join(
[Prelude_import, Stdlib_imports | Module_imports],
<<"\n"/utf8>>
).
-file("src/libero/codegen.gleam", 503).
?DOC(" Write the typed decoders FFI file for the consumer.\n").
-spec write_decoders_ffi(
libero@config:config(),
list(libero@walker:discovered_type())
) -> {ok, nil} | {error, libero@gen_error:gen_error()}.
write_decoders_ffi(Config, Discovered) ->
Imports = emit_decoder_imports(Discovered, Config),
Body = emit_typed_decoders(Discovered),
Ctor_setters = <<<<<<"setResultCtors(Ok, ResultError);\n"/utf8,
"setOptionCtors(Some, None);\n"/utf8>>/binary,
"setListCtors(Empty, NonEmpty);\n"/utf8>>/binary,
"setDictFromList(dictFromList);\n"/utf8>>,
Has_msg_from_server = gleam@list:any(
Discovered,
fun(T) -> erlang:element(3, T) =:= <<"MsgFromServer"/utf8>> end
),
Auto_register = case Has_msg_from_server of
true ->
<<<<"\n// Auto-register the typed decoder so push frames bypass the\n"/utf8,
"// global constructor registry. Called at module load time.\n"/utf8>>/binary,
"setMsgFromServerDecoder(decode_msg_from_server);\n"/utf8>>;
false ->
<<""/utf8>>
end,
Content = <<<<<<<<<<<<<<"// Code generated by libero. DO NOT EDIT.
//
// Per-type decoder functions derived from the DiscoveredType graph.
// Eliminates the global constructor registry - each decoder knows
// exactly which module's constructor to instantiate.
"/utf8,
Imports/binary>>/binary,
"\n\n"/utf8>>/binary,
Ctor_setters/binary>>/binary,
"\n"/utf8>>/binary,
Body/binary>>/binary,
"\n"/utf8>>/binary,
Auto_register/binary>>,
Output = erlang:element(9, Config),
ensure_parent_dir(Output),
write_file(Output, Content).