-module(libero@codegen). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/libero/codegen.gleam"). -export([check_segment_collisions/1, 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", 216). ?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", 223). ?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", 234). ?DOC( " Detect modules whose last path segment would collide in generated output.\n" " E.g. `shared/admin/messages` and `shared/user/messages` both produce\n" " `messages.gleam`, and the second silently overwrites the first.\n" " Returns an error listing all collisions, or Ok(Nil) if none.\n" " nolint: stringly_typed_error -- CLI-facing error, String is appropriate\n" ). -spec check_segment_collisions(list(libero@scanner:message_module())) -> {ok, nil} | {error, binary()}. check_segment_collisions(Message_modules) -> Segments = gleam@list:fold( Message_modules, maps:new(), fun(Acc, M) -> Segment = libero@scanner:last_module_segment(erlang:element(2, M)), Existing = begin _pipe = gleam_stdlib:map_get(Acc, Segment), gleam@result:unwrap(_pipe, []) end, gleam@dict:insert(Acc, Segment, [erlang:element(2, M) | Existing]) end ), Collisions = gleam@dict:fold( Segments, [], fun(Acc@1, Segment@1, Paths) -> case Paths of [_, _ | _] -> [<<<<<<<<"modules "/utf8, (gleam@string:join( lists:reverse(Paths), <<", "/utf8>> ))/binary>>/binary, " share the output filename \""/utf8>>/binary, Segment@1/binary>>/binary, ".gleam\""/utf8>> | Acc@1]; _ -> Acc@1 end end ), case Collisions of [] -> {ok, nil}; _ -> {error, <<<<"error: Module name collision \x{2502} \x{2502} "/utf8, (gleam@string:join( lists:reverse(Collisions), <<"\n \x{2502} "/utf8>> ))/binary>>/binary, " \x{2502} hint: Rename one of the modules so they produce different output filenames"/utf8>>} end. -file("src/libero/codegen.gleam", 639). ?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", 645). ?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", 653). ?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", 677). ?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", 758). -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} -> Ok_param = <<"t"/utf8, (erlang:integer_to_binary(Next))/binary>>, Err_depth = Next + 1, Err_param = <<"t"/utf8, (erlang:integer_to_binary(Err_depth))/binary>>, <<<<<<<<<<<<<<<<<<<<"decode_result_of(("/utf8, Ok_param/binary>>/binary, ") => "/utf8>>/binary, (field_decoder_call_depth( Ok, Ok_param, Next + 1 ))/binary>>/binary, ", ("/utf8>>/binary, Err_param/binary>>/binary, ") => "/utf8>>/binary, (field_decoder_call_depth( Err, Err_param, Err_depth + 1 ))/binary>>/binary, ", "/utf8>>/binary, Term_expr/binary>>/binary, ")"/utf8>>; {dict_of, K, V} -> Key_param = <<"t"/utf8, (erlang:integer_to_binary(Next))/binary>>, Val_depth = Next + 1, Val_param = <<"t"/utf8, (erlang:integer_to_binary(Val_depth))/binary>>, <<<<<<<<<<<<<<<<<<<<"decode_dict_of(("/utf8, Key_param/binary>>/binary, ") => "/utf8>>/binary, (field_decoder_call_depth( K, Key_param, Next + 1 ))/binary>>/binary, ", ("/utf8>>/binary, Val_param/binary>>/binary, ") => "/utf8>>/binary, (field_decoder_call_depth( V, Val_param, Val_depth + 1 ))/binary>>/binary, ", "/utf8>>/binary, Term_expr/binary>>/binary, ")"/utf8>>; {tuple_of, Elems} -> Decoders = gleam@list:index_map( Elems, fun(E, I) -> Elem_depth = Next + I, Elem_param = <<"t"/utf8, (erlang:integer_to_binary(Elem_depth))/binary>>, <<<<<<"("/utf8, Elem_param/binary>>/binary, ") => "/utf8>>/binary, (field_decoder_call_depth(E, Elem_param, Elem_depth + 1))/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", 753). ?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", 693). ?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", 710). ?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", 658). ?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", 853). ?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" "\n" " DESIGN NOTE: Only the first MsgFromServer type is wired into the decoder\n" " entry point. Multiple MsgFromServer types across different modules are not\n" " supported — define a single MsgFromServer in one shared module.\n" " If multiple are found, a build-time warning is printed.\n" ). -spec emit_msg_from_server_decoder(list(libero@walker:discovered_type())) -> binary(). emit_msg_from_server_decoder(Discovered) -> Msg_from_server_types = gleam@list:filter( Discovered, fun(T) -> erlang:element(3, T) =:= <<"MsgFromServer"/utf8>> end ), case Msg_from_server_types of [] -> <<""/utf8>>; [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>>; [First | Rest] -> Other_modules = begin _pipe = gleam@list:map( Rest, fun(T@2) -> erlang:element(2, T@2) end ), gleam@string:join(_pipe, <<", "/utf8>>) end, gleam_stdlib:println_error( <<<<<<"warning: multiple MsgFromServer types found. Only "/utf8, (erlang:element(2, First))/binary>>/binary, " will be wired into the client decoder. Ignored: "/utf8>>/binary, Other_modules/binary>> ), Fn_name@1 = decoder_fn_name( erlang:element(2, First), erlang:element(3, First) ), <<<<<<<<"export function decode_msg_from_server(term) {\n"/utf8, " return "/utf8>>/binary, Fn_name@1/binary>>/binary, "(term);\n"/utf8>>/binary, "}"/utf8>> end. -file("src/libero/codegen.gleam", 538). ?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", 1243). ?DOC( " Write content to a file, logging the path on success.\n" " Gleam files are run through `gleam format` before writing.\n" ). -spec write_file(binary(), binary()) -> {ok, nil} | {error, libero@gen_error:gen_error()}. write_file(Path, Content) -> Formatted = case gleam_stdlib:string_ends_with(Path, <<".gleam"/utf8>>) of true -> libero@format:format_gleam(Content); false -> Content end, case simplifile:write(Path, Formatted) 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", 923). ?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", 1226). ?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", 1262). ?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", 1291). -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", 1286). ?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", 30). ?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 = <>, ensure_parent_dir(Output), write_file(Output, Content). -file("src/libero/codegen.gleam", 272). ?DOC( " Generate per-module send function files under `client_generated/`.\n" " For each message module with `has_msg_from_client == True`, generates a\n" " `.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( <> ))/binary>>/binary, " import "/utf8>>/binary, (generated_module_path( <> ))/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 = <<<<<>/binary, Segment/binary>>/binary, ".gleam"/utf8>>, ensure_parent_dir(Output), case write_file(Output, Content) of {ok, _} -> Errs; {error, Err} -> [Err | Errs] end end ), case Errors of [] -> {ok, nil}; _ -> {error, lists:reverse(Errors)} end. -file("src/libero/codegen.gleam", 335). ?DOC( " Generate per-module push wrapper files under `server_generated/`.\n" " For each message module with `has_msg_from_server == True`, generates a\n" " `.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 = <<<<<>/binary, Segment/binary>>/binary, ".gleam"/utf8>>, ensure_parent_dir(Output), case write_file(Output, Content) of {ok, _} -> Errs; {error, Err} -> [Err | Errs] end end ), case Errors of [] -> {ok, nil}; _ -> {error, lists:reverse(Errors)} end. -file("src/libero/codegen.gleam", 382). ?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 = <>, 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", 516). ?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", 887). -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", 952). ?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", 1012). ?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 = <>, Ffi_path = <>, 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", 1053). ?DOC(" Generate the server entry point at `src/.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 = <>, {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(list.filter(path, fn(s) { s != \"..\" && s != \"\" }), \"/\"), )"/utf8>> end ), _pipe@1 = gleam@string:join(_pipe, <<"\n"/utf8>>), (fun(R) -> <<"\n"/utf8, R/binary>> end)(_pipe@1) end, Index = <<<<<<<<" _, _ -> serve_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", 1306). ?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 `/.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} -> <<<<<>/binary, Module_path/binary>>/binary, ".mjs"/utf8>>; {ok, {Package, _}} -> <<<<<>/binary, Module_path/binary>>/binary, ".mjs"/utf8>> end. -file("src/libero/codegen.gleam", 587). ?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", 547). ?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).