Current section

Files

Jump to
libero src libero@codegen_dispatch.erl
Raw

src/libero@codegen_dispatch.erl

-module(libero@codegen_dispatch).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/libero/codegen_dispatch.gleam").
-export([generate_with_extra_params/7, generate/6, generate_atoms_erl/4]).
-export_type([extra_param/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Server-side dispatch generator.\n"
"\n"
" Produces dispatch source from a list of handler endpoints. The generated\n"
" module pattern-matches incoming wire envelopes onto handler function\n"
" calls and produces ETF-encoded responses.\n"
"\n"
" Each handler call is wrapped in `trace.try_call` so a panicked handler\n"
" produces a structured InternalError response (with trace ID for log\n"
" correlation) instead of crashing the caller's process.\n"
).
-type extra_param() :: {extra_param, binary(), binary(), binary()}.
-file("src/libero/codegen_dispatch.gleam", 321).
-spec handler_alias(binary()) -> binary().
handler_alias(Module_path) ->
<<(libero@codegen:module_to_underscored(Module_path))/binary,
"_handler"/utf8>>.
-file("src/libero/codegen_dispatch.gleam", 258).
-spec emit_case_arm(
libero@scanner:handler_endpoint(),
gleam@option:option(binary()),
binary()
) -> binary().
emit_case_arm(E, Wire_module, Extra_args) ->
Variant_name = libero@codegen:to_pascal_case(
<<"server_"/utf8, (erlang:element(3, E))/binary>>
),
Alias = handler_alias(erlang:element(2, E)),
Param_destructure = libero@codegen:variant_pattern(
Variant_name,
erlang:element(6, E)
),
Handler_args = case erlang:element(8, E) of
{some, _} ->
<<"wire.coerce(typed_msg), server_context"/utf8, Extra_args/binary>>;
none ->
Positional = gleam@list:map(
erlang:element(6, E),
fun(P) -> erlang:element(1, P) end
),
gleam@string:join(
lists:append(
Positional,
[<<"server_context"/utf8, Extra_args/binary>>]
),
<<", "/utf8>>
)
end,
Raw_call = <<<<<<<<<<<<Alias/binary, "."/utf8>>/binary, "server_"/utf8>>/binary,
(erlang:element(3, E))/binary>>/binary,
"("/utf8>>/binary,
Handler_args/binary>>/binary,
")"/utf8>>,
Ok_destructure = case erlang:element(7, E) of
true ->
<<"#(result, new_ctx)"/utf8>>;
false ->
<<"result"/utf8>>
end,
Ok_ctx = case erlang:element(7, E) of
true ->
<<"new_ctx"/utf8>>;
false ->
<<"server_context"/utf8>>
end,
Encode_line = case Wire_module of
{some, _} ->
<<<<" let result = wire_encode_response_"/utf8,
(erlang:element(3, E))/binary>>/binary,
"(result)\n"/utf8>>;
none ->
<<""/utf8>>
end,
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" "/utf8,
Param_destructure/binary>>/binary,
" -> {\n"/utf8>>/binary,
" case trace.try_call(fn() { "/utf8>>/binary,
Raw_call/binary>>/binary,
" }) {\n"/utf8>>/binary,
" Ok("/utf8>>/binary,
Ok_destructure/binary>>/binary,
") -> {\n"/utf8>>/binary,
Encode_line/binary>>/binary,
" #(wire.encode_response(request_id:, value:Ok(result)), "/utf8>>/binary,
Ok_ctx/binary>>/binary,
")\n"/utf8>>/binary,
" }\n"/utf8>>/binary,
" Error(reason) -> {\n"/utf8>>/binary,
" let trace_id = trace.new_trace_id()\n"/utf8>>/binary,
" io.println_error(\"[libero] \" <> trace_id <> \" "/utf8>>/binary,
(erlang:element(3, E))/binary>>/binary,
": \" <> reason)\n"/utf8>>/binary,
" #(wire.encode_response(request_id:, value:Error(InternalError(trace_id:, message: \"Something went wrong\"))), server_context)\n"/utf8>>/binary,
" }\n"/utf8>>/binary,
" }\n"/utf8>>/binary,
" }"/utf8>>.
-file("src/libero/codegen_dispatch.gleam", 47).
?DOC(
" Generate dispatch with additional pass-through parameters on handle()\n"
" and every handler call. Each ExtraParam adds an import, a function\n"
" parameter, and a labeled argument threaded to handlers.\n"
).
-spec generate_with_extra_params(
list(libero@scanner:handler_endpoint()),
binary(),
binary(),
binary(),
gleam@option:option(binary()),
gleam@option:option(binary()),
list(extra_param())
) -> binary().
generate_with_extra_params(
Endpoints,
Context_module,
Context_type_name,
Wire_module_tag,
Atoms_module,
Wire_module,
Extra_params
) ->
Extra_import_lines = begin
_pipe = Extra_params,
_pipe@1 = gleam@list:filter(
_pipe,
fun(P) -> erlang:element(4, P) /= <<""/utf8>> end
),
_pipe@2 = gleam@list:map(
_pipe@1,
fun(P@1) -> erlang:element(4, P@1) end
),
gleam@list:unique(_pipe@2)
end,
Extra_handle_params = begin
_pipe@3 = Extra_params,
_pipe@4 = gleam@list:map(
_pipe@3,
fun(P@2) ->
<<<<<<<<<<<<"\n "/utf8, (erlang:element(2, P@2))/binary>>/binary,
" "/utf8>>/binary,
(erlang:element(2, P@2))/binary>>/binary,
": "/utf8>>/binary,
(erlang:element(3, P@2))/binary>>/binary,
","/utf8>>
end
),
gleam@string:join(_pipe@4, <<""/utf8>>)
end,
Extra_args = begin
_pipe@5 = Extra_params,
_pipe@6 = gleam@list:map(
_pipe@5,
fun(P@3) -> <<", "/utf8, (erlang:element(2, P@3))/binary>> end
),
gleam@string:join(_pipe@6, <<""/utf8>>)
end,
Handler_modules = begin
_pipe@7 = Endpoints,
_pipe@8 = gleam@list:map(_pipe@7, fun(E) -> erlang:element(2, E) end),
gleam@list:unique(_pipe@8)
end,
Handler_imports = gleam@list:map(
Handler_modules,
fun(Mod) ->
Alias = handler_alias(Mod),
<<<<<<"import "/utf8, Mod/binary>>/binary, " as "/utf8>>/binary,
Alias/binary>>
end
),
Base_resolve = libero@codegen:build_alias_resolver(Endpoints),
Resolve_alias = fun(Module_path) ->
case gleam@list:contains(Handler_modules, Module_path) of
true ->
handler_alias(Module_path);
false ->
Base_resolve(Module_path)
end
end,
Shared_type_imports = begin
_pipe@9 = libero@codegen:collect_endpoint_type_modules(Endpoints, false),
_pipe@10 = gleam@list:filter(
_pipe@9,
fun(Module_path@1) ->
not gleam@list:contains(Handler_modules, Module_path@1)
end
),
gleam@list:map(
_pipe@10,
fun(Module_path@2) ->
Alias@1 = Resolve_alias(Module_path@2),
case Alias@1 =:= libero@field_type:last_segment(Module_path@2) of
true ->
<<"import "/utf8, Module_path@2/binary>>;
false ->
<<<<<<"import "/utf8, Module_path@2/binary>>/binary,
" as "/utf8>>/binary,
Alias@1/binary>>
end
end
)
end,
Dict_import = libero@codegen:import_if(
Endpoints,
fun libero@codegen:is_dict/1,
<<"import gleam/dict.{type Dict}"/utf8>>
),
Option_import = libero@codegen:import_if(
Endpoints,
fun libero@codegen:is_option/1,
<<"import gleam/option.{type Option}"/utf8>>
),
Client_msg_variants = libero@codegen:emit_client_msg_variants(
Endpoints,
Resolve_alias
),
Known_tag_arms = begin
_pipe@11 = Endpoints,
_pipe@12 = gleam@list:map(
_pipe@11,
fun(E@1) ->
<<<<<<<<<<" Ok(\"server_"/utf8,
(erlang:element(3, E@1))/binary>>/binary,
"\") ->\n"/utf8>>/binary,
" dispatch_known(msg, request_id, server_context"/utf8>>/binary,
Extra_args/binary>>/binary,
")"/utf8>>
end
),
gleam@string:join(_pipe@12, <<"\n"/utf8>>)
end,
Case_arms = gleam@list:map(
Endpoints,
fun(_capture) -> emit_case_arm(_capture, Wire_module, Extra_args) end
),
Atoms_external = case Atoms_module of
{some, Mod@1} ->
<<<<"\n/// Pre-register all constructor atoms that may appear in client ETF
/// payloads, so binary_to_term([safe]) can decode them. Called once
/// on the first RPC call; subsequent calls are a no-op (persistent_term
/// lookup).
@external(erlang, \""/utf8,
Mod@1/binary>>/binary,
"\", \"ensure\")
fn ensure_atoms() -> Nil
"/utf8>>;
none ->
<<""/utf8>>
end,
Ensure_call = case Atoms_module of
{some, _} ->
<<" ensure_atoms()\n "/utf8>>;
none ->
<<""/utf8>>
end,
Inner_case = case Endpoints of
[] ->
<<<<<<<<<<" Ok(_) ->\n"/utf8,
" #(wire.encode_response(request_id:, value:Error(UnknownFunction(\""/utf8>>/binary,
Wire_module_tag/binary>>/binary,
"\"))), server_context)\n"/utf8>>/binary,
" Error(_) ->\n"/utf8>>/binary,
" #(wire.encode_response(request_id:, value:Error(MalformedRequest)), server_context)"/utf8>>;
_ ->
<<<<<<<<<<<<<<Known_tag_arms/binary, "\n"/utf8>>/binary,
" Ok(tag) ->\n"/utf8>>/binary,
" #(wire.encode_response(request_id:, value:Error(UnknownFunction(\""/utf8>>/binary,
Wire_module_tag/binary>>/binary,
".\" <> tag))), server_context)\n"/utf8>>/binary,
" Error(_) ->\n"/utf8>>/binary,
" #(wire.encode_response(request_id:, value:Error(MalformedRequest)), server_context)"/utf8>>
end,
Wire_externals = case {Wire_module, Endpoints} of
{{some, Mod@2}, [_ | _]} ->
Decode_ext = <<<<"\n@external(erlang, \""/utf8, Mod@2/binary>>/binary,
"\", \"decode_client_msg\")\nfn wire_decode_client_msg(msg: a) -> b\n"/utf8>>,
Response_exts = begin
_pipe@13 = gleam@list:map(
Endpoints,
fun(E@2) ->
<<<<<<<<<<<<"@external(erlang, \""/utf8, Mod@2/binary>>/binary,
"\", \"encode_response_"/utf8>>/binary,
(erlang:element(3, E@2))/binary>>/binary,
"\")\nfn wire_encode_response_"/utf8>>/binary,
(erlang:element(3, E@2))/binary>>/binary,
"(result: a) -> b\n"/utf8>>
end
),
gleam@string:join(_pipe@13, <<"\n"/utf8>>)
end,
<<<<Decode_ext/binary, "\n"/utf8>>/binary, Response_exts/binary>>;
{_, _} ->
<<""/utf8>>
end,
Decode_msg_call = case Wire_module of
{some, _} ->
<<" let msg = wire_decode_client_msg(msg)\n"/utf8>>;
none ->
<<""/utf8>>
end,
Dispatch_known = case Endpoints of
[] ->
<<""/utf8>>;
_ ->
<<<<<<<<"
fn dispatch_known(msg, request_id, server_context"/utf8,
Extra_args/binary>>/binary,
") {
case trace.try_call(fn() {
let typed_msg: ClientMsg = wire.coerce(msg)
case typed_msg {
"/utf8>>/binary,
(gleam@string:join(Case_arms, <<"\n"/utf8>>))/binary>>/binary,
"
}
}) {
Ok(response) -> response
Error(reason) -> {
let trace_id = trace.new_trace_id()
io.println_error(\"[libero] \" <> trace_id <> \" malformed message: \" <> reason)
#(wire.encode_response(request_id:, value:Error(MalformedRequest)), server_context)
}
}
}
"/utf8>>
end,
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"//// Code generated by libero. DO NOT EDIT.
import gleam/io
import libero/error.{InternalError, MalformedRequest, UnknownFunction}
import libero/trace
import libero/etf/wire"/utf8,
Dict_import/binary>>/binary,
Option_import/binary>>/binary,
"
import "/utf8>>/binary,
Context_module/binary>>/binary,
".{type "/utf8>>/binary,
Context_type_name/binary>>/binary,
"}
"/utf8>>/binary,
(gleam@string:join(
Handler_imports,
<<"\n"/utf8>>
))/binary>>/binary,
"
"/utf8>>/binary,
(gleam@string:join(
Shared_type_imports,
<<"\n"/utf8>>
))/binary>>/binary,
"
"/utf8>>/binary,
(case Extra_import_lines of
[] ->
<<""/utf8>>;
Lines ->
<<(gleam@string:join(
Lines,
<<"\n"/utf8>>
))/binary,
"\n"/utf8>>
end)/binary>>/binary,
Atoms_external/binary>>/binary,
Wire_externals/binary>>/binary,
"
pub type ClientMsg {
"/utf8>>/binary,
(gleam@string:join(
Client_msg_variants,
<<"\n"/utf8>>
))/binary>>/binary,
"
}
pub fn handle(
server_context server_context: "/utf8>>/binary,
Context_type_name/binary>>/binary,
",
data data: BitArray,"/utf8>>/binary,
Extra_handle_params/binary>>/binary,
"
) -> #(BitArray, "/utf8>>/binary,
Context_type_name/binary>>/binary,
") {
"/utf8>>/binary,
Ensure_call/binary>>/binary,
"case wire.decode_request(data) {
Ok(#(\""/utf8>>/binary,
Wire_module_tag/binary>>/binary,
"\", request_id, msg)) -> {
"/utf8>>/binary,
Decode_msg_call/binary>>/binary,
" case wire.variant_tag(msg) {
"/utf8>>/binary,
Inner_case/binary>>/binary,
"
}
}
Ok(#(name, request_id, _)) ->
#(wire.encode_response(request_id:, value:Error(UnknownFunction(name))), server_context)
Error(_) ->
#(wire.encode_response(request_id: 0, value: Error(MalformedRequest)), server_context)
}
}
"/utf8>>/binary,
Dispatch_known/binary>>.
-file("src/libero/codegen_dispatch.gleam", 25).
?DOC(" Generate dispatch.gleam source from scanned endpoints.\n").
-spec generate(
list(libero@scanner:handler_endpoint()),
binary(),
binary(),
binary(),
gleam@option:option(binary()),
gleam@option:option(binary())
) -> binary().
generate(
Endpoints,
Context_module,
Context_type_name,
Wire_module_tag,
Atoms_module,
Wire_module
) ->
generate_with_extra_params(
Endpoints,
Context_module,
Context_type_name,
Wire_module_tag,
Atoms_module,
Wire_module,
[]
).
-file("src/libero/codegen_dispatch.gleam", 334).
?DOC(
" Generate an Erlang FFI file that pre-registers all constructor atoms\n"
" discovered from handler endpoints, 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"
"\n"
" Under the wire-identity scheme there is no AtomMap: identity is\n"
" baked into the per-type transformer functions in `<consumer>_wire.erl`\n"
" at codegen time, so the runtime carries no qualification table.\n"
).
-spec generate_atoms_erl(
list(libero@scanner:handler_endpoint()),
list(libero@walker:discovered_type()),
binary(),
gleam@option:option(binary())
) -> binary().
generate_atoms_erl(Endpoints, Discovered, Atoms_module, Wire_module) ->
Framework_atoms = [<<"ok"/utf8>>,
<<"error"/utf8>>,
<<"some"/utf8>>,
<<"none"/utf8>>,
<<"nil"/utf8>>,
<<"true"/utf8>>,
<<"false"/utf8>>,
<<"malformed_request"/utf8>>,
<<"unknown_function"/utf8>>,
<<"internal_error"/utf8>>,
<<"decode_error"/utf8>>],
Handler_atoms = gleam@list:flat_map(
Endpoints,
fun(E) ->
[erlang:element(3, E),
<<"server_"/utf8, (erlang:element(3, E))/binary>>]
end
),
Variant_atoms = gleam@list:flat_map(
Discovered,
fun(Dt) ->
gleam@list:flat_map(
erlang:element(5, Dt),
fun(V) ->
{_, Hash} = libero@wire_identity:wire_identity(
erlang:element(2, V),
erlang:element(3, V),
erlang:element(7, V)
),
[libero@walker:to_snake_case(erlang:element(3, V)), Hash]
end
)
end
),
All_atoms = begin
_pipe = lists:append([Framework_atoms, Handler_atoms, Variant_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,
<<<<<<<<<<<<"%% Code generated by libero. DO NOT EDIT.
%%
%% Pre-registers all atoms that may appear in client ETF payloads,
%% so binary_to_term([safe]) can decode them without rejecting unknown
%% atoms. Includes framework atoms, handler function names, bare
%% constructor names, and the 10-char hex wire-identity hashes that
%% the per-type transformer functions emit on the wire.
%%
%% ensure/0 uses persistent_term as a one-shot guard so the
%% binary_to_atom calls only run once per VM lifetime.
-module("/utf8,
Atoms_module/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,
"
]),
"/utf8>>/binary,
(case Wire_module of
{some, Mod} ->
<<<<"persistent_term:put({libero, wire_module}, '"/utf8,
Mod/binary>>/binary,
"'),\n "/utf8>>;
none ->
<<""/utf8>>
end)/binary>>/binary,
"persistent_term:put({?MODULE, done}, true),
nil.
"/utf8>>.