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([write_file/2, extract_dir/1, ensure_parent_dir/1, last_split/2, module_to_mjs_path/1, module_to_underscored/1, to_pascal_case/1, endpoints_contain/2, variant_pattern/2, emit_client_msg_variants/1, collect_endpoint_type_imports/2, is_dict/1, is_option/1, import_if/3]).
-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(
" Cross-cutting helpers used by every codegen submodule.\n"
"\n"
" File I/O, path utilities, naming helpers, and small predicates over\n"
" `field_type.FieldType` graphs. The actual generators live in the\n"
" per-domain modules (codegen_dispatch, codegen_stubs, codegen_decoders,\n"
" codegen_server).\n"
).
-file("src/libero/codegen.gleam", 33).
?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", 61).
?DOC(
" Strip the final path segment, returning the directory portion of `path`.\n"
" Returns \".\" when there's no parent (single-segment input).\n"
).
-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", 54).
?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", 77).
?DOC(
" Take the substring after the LAST occurrence of `separator` in `input`.\n"
" Falls back to `input` if the separator is absent. Splitting on the last\n"
" occurrence (not the first) matters for nested paths like\n"
" `vendor/x/src/inner/src/types`, where the leftmost match would yield the\n"
" wrong segment.\n"
).
-spec last_split(binary(), binary()) -> binary().
last_split(Input, Separator) ->
case gleam@string:split(Input, Separator) of
[_] ->
Input;
Parts ->
_pipe = gleam@list:last(Parts),
gleam@result:unwrap(_pipe, Input)
end.
-file("src/libero/codegen.gleam", 88).
?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", 104).
?DOC(
" Convert a Gleam module path like \"shared/discount\" to a flat\n"
" underscore-separated alias suitable for use as a Gleam identifier\n"
" or import alias suffix. e.g. \"shared/discount\" -> \"shared_discount\".\n"
).
-spec module_to_underscored(binary()) -> binary().
module_to_underscored(Module_path) ->
gleam@string:replace(Module_path, <<"/"/utf8>>, <<"_"/utf8>>).
-file("src/libero/codegen.gleam", 110).
?DOC(
" Convert a snake_case name to PascalCase.\n"
" e.g. \"get_items\" -> \"GetItems\", \"create_item\" -> \"CreateItem\"\n"
).
-spec to_pascal_case(binary()) -> binary().
to_pascal_case(Name) ->
_pipe = Name,
_pipe@1 = gleam@string:split(_pipe, <<"_"/utf8>>),
_pipe@2 = gleam@list:map(
_pipe@1,
fun(Word) -> case gleam_stdlib:string_pop_grapheme(Word) of
{ok, {First, Rest}} ->
<<(string:uppercase(First))/binary, Rest/binary>>;
{error, nil} ->
Word
end end
),
gleam@string:join(_pipe@2, <<""/utf8>>).
-file("src/libero/codegen.gleam", 127).
?DOC(
" True if any endpoint's parameter or return type (transitively)\n"
" satisfies `predicate`. Used for stdlib import detection\n"
" (Option, Dict).\n"
).
-spec endpoints_contain(
list(libero@scanner:handler_endpoint()),
fun((libero@field_type:field_type()) -> boolean())
) -> boolean().
endpoints_contain(Endpoints, Predicate) ->
gleam@list:any(
Endpoints,
fun(E) ->
(libero@field_type:contains(erlang:element(4, E), Predicate) orelse libero@field_type:contains(
erlang:element(5, E),
Predicate
))
orelse gleam@list:any(
erlang:element(6, E),
fun(P) ->
libero@field_type:contains(erlang:element(2, P), Predicate)
end
)
end
).
-file("src/libero/codegen.gleam", 141).
?DOC(
" Emit a constructor / pattern shape like `Variant(label1:, label2:)` or\n"
" just `Variant` when there are no parameters. Used by both the dispatch\n"
" match arms (destructure) and client stubs (constructor call).\n"
).
-spec variant_pattern(
binary(),
list({binary(), libero@field_type:field_type()})
) -> binary().
variant_pattern(Variant_name, Params) ->
case Params of
[] ->
Variant_name;
_ ->
Labels = gleam@list:map(
Params,
fun(P) -> <<(erlang:element(1, P))/binary, ":"/utf8>> end
),
<<<<<<Variant_name/binary, "("/utf8>>/binary,
(gleam@string:join(Labels, <<", "/utf8>>))/binary>>/binary,
")"/utf8>>
end.
-file("src/libero/codegen.gleam", 159).
?DOC(
" Emit the body lines of the generated `ClientMsg` type — one variant\n"
" per endpoint, indented by two spaces. Both the server dispatch and\n"
" the client stubs need this exact shape; routing both through this\n"
" helper guarantees they stay in sync (mismatched variants would\n"
" silently break the wire contract).\n"
).
-spec emit_client_msg_variants(list(libero@scanner:handler_endpoint())) -> list(binary()).
emit_client_msg_variants(Endpoints) ->
gleam@list:map(
Endpoints,
fun(E) ->
Variant_name = to_pascal_case(erlang:element(3, E)),
case erlang:element(6, E) of
[] ->
<<" "/utf8, Variant_name/binary>>;
Params ->
Fields = gleam@list:map(
Params,
fun(P) ->
{Label, Ft} = P,
<<<<Label/binary, ": "/utf8>>/binary,
(libero@field_type:to_gleam_source(Ft))/binary>>
end
),
<<<<<<<<" "/utf8, Variant_name/binary>>/binary, "("/utf8>>/binary,
(gleam@string:join(Fields, <<", "/utf8>>))/binary>>/binary,
")"/utf8>>
end
end
).
-file("src/libero/codegen.gleam", 181).
?DOC(
" Collect `import <module>` lines for every shared module path\n"
" referenced (transitively) by the endpoints' parameter types and,\n"
" optionally, return types. Output is unique and sorted.\n"
).
-spec collect_endpoint_type_imports(
list(libero@scanner:handler_endpoint()),
boolean()
) -> list(binary()).
collect_endpoint_type_imports(Endpoints, Include_return) ->
_pipe = Endpoints,
_pipe@3 = gleam@list:flat_map(
_pipe,
fun(E) ->
From_params = gleam@list:flat_map(
erlang:element(6, E),
fun(P) ->
libero@field_type:collect_user_types(erlang:element(2, P))
end
),
case Include_return of
true ->
_pipe@1 = From_params,
_pipe@2 = lists:append(
_pipe@1,
libero@field_type:collect_user_types(
erlang:element(4, E)
)
),
lists:append(
_pipe@2,
libero@field_type:collect_user_types(
erlang:element(5, E)
)
);
false ->
From_params
end
end
),
_pipe@4 = gleam@list:map(_pipe@3, fun(Ref) -> erlang:element(1, Ref) end),
_pipe@5 = gleam@list:unique(_pipe@4),
_pipe@6 = gleam@list:sort(_pipe@5, fun gleam@string:compare/2),
gleam@list:map(_pipe@6, fun(Mod) -> <<"import "/utf8, Mod/binary>> end).
-file("src/libero/codegen.gleam", 203).
-spec is_dict(libero@field_type:field_type()) -> boolean().
is_dict(Ft) ->
case Ft of
{dict_of, _, _} ->
true;
_ ->
false
end.
-file("src/libero/codegen.gleam", 210).
-spec is_option(libero@field_type:field_type()) -> boolean().
is_option(Ft) ->
case Ft of
{option_of, _} ->
true;
_ ->
false
end.
-file("src/libero/codegen.gleam", 220).
?DOC(
" Emit `import_line` (with a leading newline) iff any endpoint type\n"
" transitively satisfies `predicate`; otherwise the empty string. Used\n"
" for conditional stdlib imports like `gleam/dict` or `gleam/option`.\n"
).
-spec import_if(
list(libero@scanner:handler_endpoint()),
fun((libero@field_type:field_type()) -> boolean()),
binary()
) -> binary().
import_if(Endpoints, Predicate, Import_line) ->
gleam@bool:guard(
not endpoints_contain(Endpoints, Predicate),
<<""/utf8>>,
fun() -> <<"\n"/utf8, Import_line/binary>> end
).