Current section
Files
Jump to
Current section
Files
src/libero.erl
-module(libero).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/libero.gleam").
-export([qualified_atom_name/2, client_output_dir_from_env/1, generate_decoders_gleam/0, generate_decoders_ffi/3, generate_wire_erl/4, generate_atoms/4, generate_dispatch/3, walk/1, collect_seeds/1, scan/0, main/0, scan_excluding/1, generate_dispatch_with_extra_params/4, generate_json_contract/4]).
-export_type([write_error/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(
" Libero: RPC plumbing library for Gleam.\n"
"\n"
" Provides handler scanning, dispatch codegen, ETF wire protocol,\n"
" and decoder generation.\n"
"\n"
" Run `gleam run -m libero` to generate the RPC pipeline into\n"
" `src/generated/libero/`. Or call the library functions directly\n"
" for programmatic use (e.g. from a framework).\n"
).
-type write_error() :: {cannot_create_dir, binary(), simplifile:file_error()} |
{cannot_write_file, binary(), simplifile:file_error()}.
-file("src/libero.gleam", 39).
?DOC(
" Re-export so consumers can build qualified atom names for push\n"
" dispatch entries using the same logic as the codegen.\n"
).
-spec qualified_atom_name(binary(), binary()) -> binary().
qualified_atom_name(Module_path, Variant_name) ->
libero@walker:qualified_atom_name(Module_path, Variant_name).
-file("src/libero.gleam", 392).
-spec print_write_error(write_error()) -> nil.
print_write_error(Err) ->
Message = case Err of
{cannot_create_dir, Path, Cause} ->
libero@gen_error:error_box(
<<"Cannot create output directory"/utf8>>,
Path,
[simplifile:describe_error(Cause)],
none
);
{cannot_write_file, Path@1, Cause@1} ->
libero@gen_error:error_box(
<<"Cannot write generated file"/utf8>>,
Path@1,
[simplifile:describe_error(Cause@1)],
none
)
end,
gleam_stdlib:println_error(Message).
-file("src/libero.gleam", 387).
-spec write_file(binary(), binary()) -> {ok, nil} | {error, write_error()}.
write_file(Path, Content) ->
_pipe = simplifile:write(Path, Content),
gleam@result:map_error(
_pipe,
fun(Cause) -> {cannot_write_file, Path, Cause} end
).
-file("src/libero.gleam", 460).
-spec generate_json_codecs(list(libero@walker:discovered_type()), binary()) -> nil.
generate_json_codecs(Discovered, Out_dir) ->
case Discovered of
[] ->
nil;
_ ->
case libero@json@codegen:generate(Discovered) of
{ok, Json_codecs_src} ->
case write_file(
<<Out_dir/binary, "/json_codecs.gleam"/utf8>>,
libero@format:format_gleam(Json_codecs_src)
) of
{ok, nil} ->
nil;
{error, Err} ->
print_write_error(Err),
libero_ffi:halt(1)
end;
{error, Errors} ->
gleam@list:each(
Errors,
fun(E) ->
gleam_stdlib:println_error(
libero@gen_error:error_box(
<<"JSON codec generation failed"/utf8>>,
erlang:element(2, E),
[erlang:element(3, E)],
none
)
)
end
),
libero_ffi:halt(1)
end
end.
-file("src/libero.gleam", 412).
-spec write_client_files(binary(), binary(), binary()) -> {ok, nil} |
{error, write_error()}.
write_client_files(Out, Js, Gleam) ->
gleam@result:'try'(
begin
_pipe = simplifile:create_directory_all(Out),
gleam@result:map_error(
_pipe,
fun(Cause) -> {cannot_create_dir, Out, Cause} end
)
end,
fun(_) ->
gleam@result:'try'(
write_file(<<Out/binary, "/rpc_decoders_ffi.mjs"/utf8>>, Js),
fun(_) ->
write_file(
<<Out/binary, "/rpc_decoders.gleam"/utf8>>,
libero@format:format_gleam(Gleam)
)
end
)
end
).
-file("src/libero.gleam", 343).
?DOC(
" Resolve the optional client output directory from environment config.\n"
" Set `LIBERO_CLIENT_OUT_DIR` to opt in to client-side decoder writes.\n"
).
-spec client_output_dir_from_env(gleam@option:option(binary())) -> gleam@option:option(binary()).
client_output_dir_from_env(Env_value) ->
case Env_value of
{some, Path} ->
case gleam@string:trim(Path) of
<<""/utf8>> ->
none;
Trimmed ->
{some, Trimmed}
end;
_ ->
none
end.
-file("src/libero.gleam", 357).
-spec write_generated_files(
binary(),
binary(),
binary(),
binary(),
binary(),
binary(),
binary()
) -> {ok, nil} | {error, write_error()}.
write_generated_files(
Dispatch_src,
Decoders_js,
Decoders_gleam,
Atoms_path,
Atoms_erl,
Wire_path,
Wire_erl
) ->
gleam@result:'try'(
begin
_pipe = simplifile:create_directory_all(
<<"src/generated/libero"/utf8>>
),
gleam@result:map_error(
_pipe,
fun(Cause) ->
{cannot_create_dir, <<"src/generated/libero"/utf8>>, Cause}
end
)
end,
fun(_) ->
gleam@result:'try'(
write_file(
<<"src/generated/libero"/utf8, "/dispatch.gleam"/utf8>>,
libero@format:format_gleam(Dispatch_src)
),
fun(_) ->
gleam@result:'try'(
write_file(
<<"src/generated/libero"/utf8,
"/rpc_decoders_ffi.mjs"/utf8>>,
Decoders_js
),
fun(_) ->
gleam@result:'try'(
write_file(
<<"src/generated/libero"/utf8,
"/rpc_decoders.gleam"/utf8>>,
libero@format:format_gleam(Decoders_gleam)
),
fun(_) ->
gleam@result:'try'(
write_file(Atoms_path, Atoms_erl),
fun(_) ->
gleam@result:'try'(
write_file(Wire_path, Wire_erl),
fun(_) -> {ok, nil} end
)
end
)
end
)
end
)
end
)
end
).
-file("src/libero.gleam", 324).
?DOC(" Generate the Gleam wrapper for the typed decoder FFI.\n").
-spec generate_decoders_gleam() -> binary().
generate_decoders_gleam() ->
libero@codegen_decoders:generate_decoders_gleam(
<<"rpc_decoders_ffi.mjs"/utf8>>
).
-file("src/libero.gleam", 309).
?DOC(
" Generate the JS typed decoder FFI source.\n"
" `package` is the Gleam package name that owns the modules (determines\n"
" the top-level directory in the JS build output).\n"
).
-spec generate_decoders_ffi(
list(libero@walker:discovered_type()),
list(libero@scanner:handler_endpoint()),
binary()
) -> binary().
generate_decoders_ffi(Discovered, Endpoints, Package) ->
libero@codegen_decoders:generate_decoders_ffi(
Discovered,
Endpoints,
<<"../../../"/utf8>>,
Package,
{some, <<"generated/libero/dispatch"/utf8>>}
).
-file("src/libero.gleam", 425).
-spec read_package_name() -> {ok, binary()} | {error, binary()}.
read_package_name() ->
case simplifile:read(<<"gleam.toml"/utf8>>) of
{error, _} ->
{error,
libero@gen_error:error_box(
<<"Could not read gleam.toml"/utf8>>,
<<"gleam.toml"/utf8>>,
[<<"File is missing or unreadable."/utf8>>],
{some,
<<"Run libero from the project root where gleam.toml lives."/utf8>>}
)};
{ok, Content} ->
case tom:parse(Content) of
{error, _} ->
{error,
libero@gen_error:error_box(
<<"Could not parse gleam.toml"/utf8>>,
<<"gleam.toml"/utf8>>,
[<<"The file contains invalid TOML."/utf8>>],
none
)};
{ok, Parsed} ->
case tom:get_string(Parsed, [<<"name"/utf8>>]) of
{error, _} ->
{error,
libero@gen_error:error_box(
<<"Missing \"name\" in gleam.toml"/utf8>>,
<<"gleam.toml"/utf8>>,
[<<"Expected a top-level `name = \"...\"` field."/utf8>>],
none
)};
{ok, Name} ->
{ok, Name}
end
end
end.
-file("src/libero.gleam", 292).
?DOC(" Generate the per-type wire-transformer Erlang module.\n").
-spec generate_wire_erl(
list(libero@walker:discovered_type()),
binary(),
list(libero@scanner:handler_endpoint()),
list(libero@etf@codegen_erl:push_dispatch())
) -> {ok, binary()} | {error, libero@gen_error:gen_error()}.
generate_wire_erl(Discovered, Wire_module, Endpoints, Push_dispatches) ->
libero@etf@codegen_erl:generate(
Wire_module,
Discovered,
Endpoints,
Push_dispatches
).
-file("src/libero.gleam", 277).
?DOC(
" Generate the Erlang atoms pre-registration file content.\n"
" Module name uses Gleam's @-separated convention (e.g. \"generated@rpc_atoms\").\n"
).
-spec generate_atoms(
list(libero@scanner:handler_endpoint()),
list(libero@walker:discovered_type()),
binary(),
gleam@option:option(binary())
) -> binary().
generate_atoms(Endpoints, Discovered, Atoms_module, Wire_module) ->
libero@codegen_dispatch:generate_atoms_erl(
Endpoints,
Discovered,
Atoms_module,
Wire_module
).
-file("src/libero.gleam", 241).
?DOC(" Generate the server dispatch module source.\n").
-spec generate_dispatch(
list(libero@scanner:handler_endpoint()),
gleam@option:option(binary()),
gleam@option:option(binary())
) -> binary().
generate_dispatch(Endpoints, Atoms_module, Wire_module) ->
libero@codegen_dispatch:generate(
Endpoints,
<<"server_context"/utf8>>,
<<"ServerContext"/utf8>>,
<<"rpc"/utf8>>,
Atoms_module,
Wire_module
).
-file("src/libero.gleam", 226).
?DOC(
" Walk the type graph from seeds. File paths are derived from this package's\n"
" `src/` directory. Frameworks that generate clients from a different package\n"
" layout should call the lower-level walker APIs with their own file list.\n"
).
-spec walk(list({binary(), binary()})) -> {ok,
list(libero@walker:discovered_type())} |
{error, list(libero@gen_error:gen_error())}.
walk(Seeds) ->
gleam@result:'try'(
begin
_pipe = libero@scanner:walk_directory(<<"./src"/utf8>>),
gleam@result:map_error(_pipe, fun(E) -> [E] end)
end,
fun(Files) -> libero@walker:walk(Seeds, Files) end
).
-file("src/libero.gleam", 217).
?DOC(" Extract type seeds from endpoints for the walker.\n").
-spec collect_seeds(list(libero@scanner:handler_endpoint())) -> list({binary(),
binary()}).
collect_seeds(Endpoints) ->
libero@scanner:collect_seeds(Endpoints).
-file("src/libero.gleam", 198).
?DOC(
" Scan `src/` for handler endpoints.\n"
" Context type is always `ServerContext`. Skips `src/generated/`.\n"
).
-spec scan() -> {ok, list(libero@scanner:handler_endpoint())} |
{error, list(libero@gen_error:gen_error())}.
scan() ->
libero@scanner:scan(<<"./src"/utf8>>, <<"ServerContext"/utf8>>).
-file("src/libero.gleam", 60).
?DOC(" Run the full generation pipeline, writing files to `src/generated/libero/`.\n").
-spec main() -> nil.
main() ->
Endpoints = case scan() of
{ok, Eps} ->
Eps;
{error, Errors} ->
gleam@list:each(Errors, fun libero@gen_error:print_error/1),
libero_ffi:halt(1)
end,
Seeds = collect_seeds(Endpoints),
Discovered = case walk(Seeds) of
{ok, Types} ->
Types;
{error, Errors@1} ->
gleam@list:each(Errors@1, fun libero@gen_error:print_error/1),
libero_ffi:halt(1)
end,
Atoms_module = <<"generated@rpc_atoms"/utf8>>,
Wire_module = <<"generated@rpc_wire"/utf8>>,
Dispatch_src = generate_dispatch(
Endpoints,
{some, Atoms_module},
{some, Wire_module}
),
Atoms_erl = generate_atoms(
Endpoints,
Discovered,
Atoms_module,
{some, Wire_module}
),
Wire_erl = case generate_wire_erl(Discovered, Wire_module, Endpoints, []) of
{ok, Src} ->
Src;
{error, Err} ->
libero@gen_error:print_error(Err),
libero_ffi:halt(1)
end,
Package = case read_package_name() of
{ok, Name} ->
Name;
{error, Msg} ->
gleam_stdlib:println_error(Msg),
libero_ffi:halt(1)
end,
Decoders_js = generate_decoders_ffi(Discovered, Endpoints, Package),
Decoders_gleam = generate_decoders_gleam(),
Atoms_path = <<<<"src/"/utf8, Atoms_module/binary>>/binary, ".erl"/utf8>>,
Wire_path = <<<<"src/"/utf8, Wire_module/binary>>/binary, ".erl"/utf8>>,
case write_generated_files(
Dispatch_src,
Decoders_js,
Decoders_gleam,
Atoms_path,
Atoms_erl,
Wire_path,
Wire_erl
) of
{ok, nil} ->
nil;
{error, Err@1} ->
print_write_error(Err@1),
libero_ffi:halt(1)
end,
case client_output_dir_from_env(
libero_ffi:get_env(<<"LIBERO_CLIENT_OUT_DIR"/utf8>>)
) of
{some, Client_out} ->
case write_client_files(Client_out, Decoders_js, Decoders_gleam) of
{ok, nil} ->
nil;
{error, Err@2} ->
print_write_error(Err@2),
libero_ffi:halt(1)
end;
none ->
nil
end,
Json_contract = libero@json@contract:generate(Endpoints, Discovered, [], []),
case write_file(
<<"src/generated/libero"/utf8, "/rpc_contract.json"/utf8>>,
Json_contract
) of
{ok, nil} ->
nil;
{error, Err@3} ->
print_write_error(Err@3),
libero_ffi:halt(1)
end,
case libero_ffi:get_env(<<"LIBERO_GEN_JSON_CODECS"/utf8>>) of
{some, Val} when (Val =:= <<"1"/utf8>>) orelse (Val =:= <<"true"/utf8>>) ->
generate_json_codecs(Discovered, <<"src/generated/libero"/utf8>>);
_ ->
nil
end,
Json_codecs_msg = case libero_ffi:get_env(<<"LIBERO_GEN_JSON_CODECS"/utf8>>) of
{some, Val@1} when (Val@1 =:= <<"1"/utf8>>) orelse (Val@1 =:= <<"true"/utf8>>) ->
<<<<", "/utf8, "src/generated/libero"/utf8>>/binary,
"/json_codecs.gleam"/utf8>>;
_ ->
<<""/utf8>>
end,
gleam_stdlib:println(
<<<<<<<<<<<<<<<<<<"wrote "/utf8, "src/generated/libero"/utf8>>/binary,
"/dispatch.gleam, rpc_decoders_ffi.mjs, rpc_decoders.gleam, "/utf8>>/binary,
Atoms_path/binary>>/binary,
", "/utf8>>/binary,
Wire_path/binary>>/binary,
", "/utf8>>/binary,
"src/generated/libero"/utf8>>/binary,
"/rpc_contract.json"/utf8>>/binary,
Json_codecs_msg/binary>>
).
-file("src/libero.gleam", 206).
?DOC(
" Like `scan`, but excludes handler params whose resolved type matches any\n"
" #(module_path, type_name) in exclude_param_types. This lets frameworks\n"
" strip server-injected params (e.g. auth identity) before message-type\n"
" resolution, so the scanner sees only the client-facing payload.\n"
).
-spec scan_excluding(list({binary(), binary()})) -> {ok,
list(libero@scanner:handler_endpoint())} |
{error, list(libero@gen_error:gen_error())}.
scan_excluding(Exclude_param_types) ->
libero@scanner:scan_excluding(
<<"./src"/utf8>>,
<<"ServerContext"/utf8>>,
Exclude_param_types
).
-file("src/libero.gleam", 258).
?DOC(
" Generate dispatch with extra pass-through parameters on handle()\n"
" and every handler call.\n"
).
-spec generate_dispatch_with_extra_params(
list(libero@scanner:handler_endpoint()),
gleam@option:option(binary()),
gleam@option:option(binary()),
list(libero@codegen_dispatch:extra_param())
) -> binary().
generate_dispatch_with_extra_params(
Endpoints,
Atoms_module,
Wire_module,
Extra_params
) ->
libero@codegen_dispatch:generate_with_extra_params(
Endpoints,
<<"server_context"/utf8>>,
<<"ServerContext"/utf8>>,
<<"rpc"/utf8>>,
Atoms_module,
Wire_module,
Extra_params
).
-file("src/libero.gleam", 332).
?DOC(
" Generate a deterministic JSON contract artifact from discovered types\n"
" and handler endpoints. The artifact describes every type, variant, and\n"
" endpoint that crosses the wire so external tools and SDKs can generate\n"
" clients from it.\n"
).
-spec generate_json_contract(
list(libero@scanner:handler_endpoint()),
list(libero@walker:discovered_type()),
list(libero@json@contract:push_contract()),
list(libero@json@contract:ssr_model_contract())
) -> binary().
generate_json_contract(Endpoints, Discovered, Push_types, Ssr_models) ->
libero@json@contract:generate(Endpoints, Discovered, Push_types, Ssr_models).