-module(rally@generator@codec).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/rally/generator/codec.gleam").
-export([generate/5, generate_json_type_registry_js/1, generate_json_decode_dispatch/1, generate_json_codecs/2, client_context_seeds/2]).
-export_type([codec_file/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(
" Client package codec and page module generator.\n"
"\n"
" Generates:\n"
" - codec_ffi.mjs — JS typed decoders (from walker-discovered types)\n"
" - types.gleam — ClientMsg type for RPC dispatch\n"
" - codec.gleam — decode_flags utility\n"
" - Per-page client modules — tree-shaken page source\n"
" - rally_runtime/effect.gleam — client-side effect shim\n"
).
-type codec_file() :: {codec_file, binary(), binary()}.
-file("src/rally/generator/codec.gleam", 452).
-spec drop_unused_effect_import(binary(), list(binary())) -> binary().
drop_unused_effect_import(Source, Aliases) ->
Alias_still_used = gleam@list:any(
Aliases,
fun(Alias) ->
gleam_stdlib:contains_string(Source, <<Alias/binary, "."/utf8>>)
end
),
case Alias_still_used of
true ->
Source;
false ->
_pipe = Source,
_pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>),
_pipe@2 = gleam@list:filter(
_pipe@1,
fun(Line) ->
not gleam_stdlib:string_starts_with(
gleam@string:trim(Line),
<<"import rally_runtime/effect"/utf8>>
)
end
),
gleam@string:join(_pipe@2, <<"\n"/utf8>>)
end.
-file("src/rally/generator/codec.gleam", 441).
-spec replace_send_to_server_calls(binary(), list(binary())) -> binary().
replace_send_to_server_calls(Source, Aliases) ->
gleam@list:fold(Aliases, Source, fun(Acc, Alias) -> _pipe = Acc,
_pipe@1 = gleam@string:replace(
_pipe,
<<Alias/binary, ".send_to_server("/utf8>>,
<<"send_to_server("/utf8>>
),
gleam@string:replace(
_pipe@1,
<<Alias/binary, ".send_to_server ("/utf8>>,
<<"send_to_server ("/utf8>>
) end).
-file("src/rally/generator/codec.gleam", 859).
-spec json_client_encoder(libero@field_type:field_type(), binary()) -> binary().
json_client_encoder(Ft, Var) ->
case Ft of
string_field ->
<<<<"json.string("/utf8, Var/binary>>/binary, ")"/utf8>>;
int_field ->
<<<<"json.int("/utf8, Var/binary>>/binary, ")"/utf8>>;
float_field ->
<<<<"json.float("/utf8, Var/binary>>/binary, ")"/utf8>>;
bool_field ->
<<<<"json.bool("/utf8, Var/binary>>/binary, ")"/utf8>>;
nil_field ->
<<"json.null()"/utf8>>;
bit_array_field ->
erlang:error(#{gleam_error => panic,
message => <<"client encoder: BitArray not supported in client-side JSON encoding"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"rally/generator/codec"/utf8>>,
function => <<"json_client_encoder"/utf8>>,
line => 867});
{user_type, Module_path, Type_name, _} ->
Qual = libero@walker:qualified_atom_name(Module_path, Type_name),
<<<<<<<<"json_codecs.json_encode_"/utf8, Qual/binary>>/binary,
"("/utf8>>/binary,
Var/binary>>/binary,
")"/utf8>>;
{list_of, Inner} ->
<<<<<<<<"json.array("/utf8, Var/binary>>/binary,
", of: fn(x) { "/utf8>>/binary,
(json_client_encoder(Inner, <<"x"/utf8>>))/binary>>/binary,
" })"/utf8>>;
{option_of, Inner@1} ->
<<<<<<<<<<<<<<"(fn(opt) { case opt {"/utf8,
" None -> json.object([#(\"type\", json.string(\"gleam/option.Option\")), #(\"variant\", json.string(\"None\")), #(\"fields\", json.object([]))])"/utf8>>/binary,
" Some(x) -> json.object([#(\"type\", json.string(\"gleam/option.Option\")), #(\"variant\", json.string(\"Some\")), #(\"fields\", json.array(["/utf8>>/binary,
(json_client_encoder(Inner@1, <<"x"/utf8>>))/binary>>/binary,
"], of: fn(y) { y }))])"/utf8>>/binary,
" } })("/utf8>>/binary,
Var/binary>>/binary,
")"/utf8>>;
{result_of, Ok, Err} ->
<<<<<<<<<<<<<<<<<<"(fn(res) { case res {"/utf8,
" Ok(x) -> json.object([#(\"type\", json.string(\"gleam/result.Result\")), #(\"variant\", json.string(\"Ok\")), #(\"fields\", json.array(["/utf8>>/binary,
(json_client_encoder(
Ok,
<<"x"/utf8>>
))/binary>>/binary,
"], of: fn(y) { y }))])"/utf8>>/binary,
" Error(x) -> json.object([#(\"type\", json.string(\"gleam/result.Result\")), #(\"variant\", json.string(\"Error\")), #(\"fields\", json.array(["/utf8>>/binary,
(json_client_encoder(Err, <<"x"/utf8>>))/binary>>/binary,
"], of: fn(y) { y }))])"/utf8>>/binary,
" } })("/utf8>>/binary,
Var/binary>>/binary,
")"/utf8>>;
{dict_of, _, _} ->
erlang:error(#{gleam_error => panic,
message => <<"client encoder: Dict not supported in client-side JSON encoding"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"rally/generator/codec"/utf8>>,
function => <<"json_client_encoder"/utf8>>,
line => 899});
{tuple_of, _} ->
erlang:error(#{gleam_error => panic,
message => <<"client encoder: Tuple not supported in client-side JSON encoding"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"rally/generator/codec"/utf8>>,
function => <<"json_client_encoder"/utf8>>,
line => 901});
{type_var, _} ->
erlang:error(#{gleam_error => panic,
message => <<"client encoder: type variable not supported in client-side JSON encoding"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"rally/generator/codec"/utf8>>,
function => <<"json_client_encoder"/utf8>>,
line => 903})
end.
-file("src/rally/generator/codec.gleam", 907).
-spec json_primitive_encoder(libero@field_type:field_type(), binary()) -> binary().
json_primitive_encoder(Ft, Var) ->
json_client_encoder(Ft, Var).
-file("src/rally/generator/codec.gleam", 374).
-spec generate_json_page_msg_encoder(list(rally@types:variant_info()), binary()) -> binary().
generate_json_page_msg_encoder(Variants, Page_module_path) ->
Arms = begin
_pipe = gleam@list:map(
Variants,
fun(V) ->
Type_id = <<Page_module_path/binary, ".Msg"/utf8>>,
Fields = case erlang:element(3, V) of
[] ->
<<"json.object([])"/utf8>>;
_ ->
Pairs = gleam@list:map(
erlang:element(3, V),
fun(F) ->
<<<<<<<<"#(\""/utf8,
(erlang:element(2, F))/binary>>/binary,
"\", "/utf8>>/binary,
(json_primitive_encoder(
erlang:element(3, F),
erlang:element(2, F)
))/binary>>/binary,
")"/utf8>>
end
),
<<<<"json.object(["/utf8,
(gleam@string:join(Pairs, <<", "/utf8>>))/binary>>/binary,
"])"/utf8>>
end,
<<<<<<<<<<<<<<<<<<<<<<<<" "/utf8,
(erlang:element(
2,
V
))/binary>>/binary,
" -> json.object([\n"/utf8>>/binary,
" #(\"type\", json.string(\""/utf8>>/binary,
Type_id/binary>>/binary,
"\")),\n"/utf8>>/binary,
" #(\"variant\", json.string(\""/utf8>>/binary,
(erlang:element(2, V))/binary>>/binary,
"\")),\n"/utf8>>/binary,
" #(\"fields\", "/utf8>>/binary,
Fields/binary>>/binary,
"),\n"/utf8>>/binary,
" ])"/utf8>>
end
),
gleam@string:join(_pipe, <<"\n"/utf8>>)
end,
<<<<"\npub fn json_encode_msg(msg: Msg) -> json.Json {\n case msg {\n"/utf8,
Arms/binary>>/binary,
"\n }\n}\n"/utf8>>.
-file("src/rally/generator/codec.gleam", 415).
-spec effect_module_aliases(binary()) -> list(binary()).
effect_module_aliases(Source) ->
case glance:module(Source) of
{error, unexpected_end_of_input} ->
[<<"rally_effect"/utf8>>];
{error, {unexpected_token, _, _}} ->
[<<"rally_effect"/utf8>>];
{ok, Ast} ->
_pipe = erlang:element(2, Ast),
_pipe@1 = gleam@list:filter_map(
_pipe,
fun(Def) ->
Import_ = erlang:element(3, Def),
case erlang:element(3, Import_) =:= <<"rally_runtime/effect"/utf8>> of
false ->
{error, nil};
true ->
case erlang:element(4, Import_) of
{some, {named, Name}} ->
{ok, Name};
_ ->
{ok, <<"effect"/utf8>>}
end
end
end
),
(fun(Aliases) -> case Aliases of
[] ->
[<<"rally_effect"/utf8>>];
_ ->
Aliases
end end)(_pipe@1)
end.
-file("src/rally/generator/codec.gleam", 304).
?DOC(
" Post-process tree-shaken source for client usage:\n"
" - Replace rally_effect.send_to_server with local wrapper\n"
" - Add transport import and local send_to_server wrapper\n"
" - For JSON protocol, generate json_encode_msg for page Msg type\n"
).
-spec post_process_page(
binary(),
binary(),
binary(),
rally@types:page_contract(),
binary()
) -> binary().
post_process_page(Source, Variant_name, Protocol, Contract, Page_module_path) ->
Effect_aliases = effect_module_aliases(Source),
Has_send_to_server = gleam@list:any(
Effect_aliases,
fun(Alias) ->
gleam_stdlib:contains_string(
Source,
<<Alias/binary, ".send_to_server("/utf8>>
)
orelse gleam_stdlib:contains_string(
Source,
<<Alias/binary, ".send_to_server ("/utf8>>
)
end
),
Json_msg_encoder = case {Protocol, erlang:element(3, Contract)} of
{<<"json"/utf8>>, []} ->
<<""/utf8>>;
{<<"json"/utf8>>, _} ->
Encoder = generate_json_page_msg_encoder(
erlang:element(3, Contract),
Page_module_path
),
Has_user_type = gleam@list:any(
erlang:element(3, Contract),
fun(V) ->
gleam@list:any(
erlang:element(3, V),
fun(F) -> case erlang:element(3, F) of
{user_type, _, _, _} ->
true;
_ ->
false
end end
)
end
),
case Has_user_type of
true ->
<<"import generated/json_codecs as json_codecs\n\n"/utf8,
Encoder/binary>>;
false ->
Encoder
end;
{_, _} ->
<<""/utf8>>
end,
Wrapper = case Has_send_to_server of
true ->
Transport_import = <<"\nimport generated/transport\n"/utf8>>,
Json_import = case Protocol of
<<"json"/utf8>> ->
<<"import gleam/json\n"/utf8>>;
_ ->
<<""/utf8>>
end,
Encoded_msg = case Protocol of
<<"json"/utf8>> ->
<<"json_encode_msg(msg)"/utf8>>;
_ ->
<<"msg"/utf8>>
end,
<<<<<<<<<<<<<<<<<<<<<<<<Transport_import/binary,
Json_import/binary>>/binary,
"\nfn send_to_server(msg: a) -> effect.Effect(b) {\n"/utf8>>/binary,
" effect.from(fn(_dispatch) {\n"/utf8>>/binary,
" transport.send_to_server(\""/utf8>>/binary,
Variant_name/binary>>/binary,
"\", "/utf8>>/binary,
Encoded_msg/binary>>/binary,
")\n"/utf8>>/binary,
" Nil\n"/utf8>>/binary,
" })\n"/utf8>>/binary,
"}\n"/utf8>>/binary,
Json_msg_encoder/binary>>;
false ->
<<""/utf8>>
end,
_pipe = Source,
_pipe@1 = replace_send_to_server_calls(_pipe, Effect_aliases),
_pipe@2 = drop_unused_effect_import(_pipe@1, Effect_aliases),
(fun(S) -> <<S/binary, Wrapper/binary>> end)(_pipe@2).
-file("src/rally/generator/codec.gleam", 296).
?DOC(
" Convert a server module path to a client page path.\n"
" \"pages/home_\" -> \"pages/home_\"\n"
" \"pages/article/slug_\" -> \"pages/article/slug_\"\n"
).
-spec page_module_path(binary()) -> binary().
page_module_path(Module_path) ->
Module_path.
-file("src/rally/generator/codec.gleam", 64).
?DOC(" Generate per-page client modules from tree-shaken source.\n").
-spec generate_page_modules(
list({rally@types:scanned_route(), rally@types:page_contract()}),
list(binary()),
binary()
) -> list(codec_file()).
generate_page_modules(Contracts, Server_symbols, Protocol) ->
gleam@list:filter_map(
Contracts,
fun(Pair) ->
{Route, Contract} = Pair,
case erlang:element(7, Contract) of
false ->
{error, nil};
true ->
Shaken = rally@tree_shaker:shake(
erlang:element(10, Contract),
Server_symbols
),
Page_path = page_module_path(erlang:element(5, Route)),
Content = post_process_page(
Shaken,
erlang:element(3, Route),
Protocol,
Contract,
erlang:element(5, Route)
),
{ok,
{codec_file,
<<<<"src/"/utf8, Page_path/binary>>/binary,
".gleam"/utf8>>,
Content}}
end
end
).
-file("src/rally/generator/codec.gleam", 587).
-spec emit_rally_effect_ffi() -> binary().
emit_rally_effect_ffi() ->
<<"// Generated by Rally — do not edit.
export function navigate(path) {
globalThis.history?.pushState(null, \"\", path);
globalThis.dispatchEvent(new PopStateEvent(\"popstate\"));
}
export function setDarkMode(enabled) {
document.documentElement.classList.toggle(\"dark\", enabled);
document.cookie = \"__rally_dark_mode=\" + (enabled ? \"1\" : \"0\") + \";path=/;max-age=31536000;SameSite=Lax\";
}
export function setLang(lang) {
document.cookie = \"__rally_lang=\" + lang + \";path=/;max-age=31536000;SameSite=Lax\";
}
export function readDarkModeCookie() {
var c = document.cookie;
if (c.includes('__rally_dark_mode=1')) return true;
if (c.includes('__rally_dark_mode=0')) return false;
return window.matchMedia('(prefers-color-scheme:dark)').matches;
}
export function readLangCookie() {
var m = document.cookie.match(/(?:^|;)\\s*__rally_lang=([^;]+)/);
if (m) return m[1];
var lang = navigator.language || navigator.userLanguage || '';
return lang ? lang.split('-')[0] : 'en';
}
"/utf8>>.
-file("src/rally/generator/codec.gleam", 482).
-spec emit_rally_effect_shim(binary()) -> binary().
emit_rally_effect_shim(Protocol) ->
Rpc_fn = case Protocol of
<<"json"/utf8>> ->
<<<<<<<<<<<<"\npub fn rpc(msg: a, on_response on_response: fn(b) -> msg) -> Effect(msg) {\n"/utf8,
" effect.from(fn(dispatch) {\n"/utf8>>/binary,
" transport.send_rpc(types.json_encode_client_msg(transport.coerce(msg)), fn(response) {\n"/utf8>>/binary,
" dispatch(on_response(transport.coerce(response)))\n"/utf8>>/binary,
" })\n"/utf8>>/binary,
" })\n"/utf8>>/binary,
"}\n"/utf8>>;
_ ->
<<<<<<<<<<<<"\npub fn rpc(msg: a, on_response on_response: fn(b) -> msg) -> Effect(msg) {\n"/utf8,
" effect.from(fn(dispatch) {\n"/utf8>>/binary,
" transport.send_rpc(msg, fn(response) {\n"/utf8>>/binary,
" dispatch(on_response(response))\n"/utf8>>/binary,
" })\n"/utf8>>/binary,
" })\n"/utf8>>/binary,
"}\n"/utf8>>
end,
Client_context_fn = case Protocol of
<<"json"/utf8>> ->
<<<<"\npub fn send_to_client_context(_msg: a) -> Effect(b) {\n"/utf8,
" panic as \"send_to_client_context: JSON client context encoding is not yet implemented\"\n"/utf8>>/binary,
"}\n"/utf8>>;
_ ->
<<<<<<<<<<"\npub fn send_to_client_context(msg: a) -> Effect(b) {\n"/utf8,
" effect.from(fn(_dispatch) {\n"/utf8>>/binary,
" transport.send_to_server(\"__ClientContext__\", msg)\n"/utf8>>/binary,
" Nil\n"/utf8>>/binary,
" })\n"/utf8>>/binary,
"}\n"/utf8>>
end,
Imports = case Protocol of
<<"json"/utf8>> ->
<<"import generated/transport\nimport generated/types\n\n"/utf8>>;
_ ->
<<"import generated/transport\n\n"/utf8>>
end,
<<<<<<<<Imports/binary,
"// Generated by Rally — do not edit.
////
//// Client-side effect shim. Provides the same API as
//// rally_runtime/effect but backed by the client transport.
import lustre/effect.{type Effect}"/utf8>>/binary,
Rpc_fn/binary>>/binary,
Client_context_fn/binary>>/binary,
"
pub fn navigate(path: String) -> Effect(a) {
effect.from(fn(_dispatch) {
do_navigate(path)
Nil
})
}
@external(javascript, \"./rally_effect_ffi.mjs\", \"navigate\")
fn do_navigate(_path: String) -> Nil {
Nil
}
pub fn none() -> Effect(a) {
effect.none()
}
pub fn from(f: fn(fn(a) -> Nil) -> Nil) -> Effect(a) {
effect.from(f)
}
pub fn get_ws_session() -> String {
\"\"
}
pub fn set_dark_mode(enabled: Bool) -> Effect(a) {
effect.from(fn(_dispatch) {
do_set_dark_mode(enabled)
Nil
})
}
@external(javascript, \"./rally_effect_ffi.mjs\", \"setDarkMode\")
fn do_set_dark_mode(_enabled: Bool) -> Nil {
Nil
}
pub fn set_lang(lang: String) -> Effect(a) {
effect.from(fn(_dispatch) {
do_set_lang(lang)
Nil
})
}
@external(javascript, \"./rally_effect_ffi.mjs\", \"setLang\")
fn do_set_lang(_lang: String) -> Nil {
Nil
}
@external(javascript, \"./rally_effect_ffi.mjs\", \"readDarkModeCookie\")
pub fn read_dark_mode() -> Bool {
False
}
@external(javascript, \"./rally_effect_ffi.mjs\", \"readLangCookie\")
pub fn read_lang() -> String {
\"\"
}
"/utf8>>.
-file("src/rally/generator/codec.gleam", 952).
-spec etf_codec_gleam_content() -> binary().
etf_codec_gleam_content() ->
<<"// Generated by Rally — do not edit.
////
//// Typed flags helpers. Delegates to Libero for the actual
//// base64 + ETF + typed decode pipeline.
import libero/error.{type DecodeError}
import libero/wire
/// Decode SSR flags and apply a typed decoder in one call.
/// The decoder_name is the function name in codec_ffi.mjs,
/// e.g. \"decode_pages_home__model\".
pub fn decode_flags_typed(
flags: String,
decoder_name: String,
) -> Result(a, DecodeError) {
wire.decode_flags_typed(flags:, decoder_name:)
}
"/utf8>>.
-file("src/rally/generator/codec.gleam", 920).
-spec json_codec_gleam_content() -> binary().
json_codec_gleam_content() ->
<<"// Generated by Rally — do not edit.
////
//// Typed flags helpers. Parses JSON SSR flags and returns
//// the parsed value. Typed decode is handled by the JS facade
//// (typedJsonToGleamValue) for response/push, and by the
//// generated json_decode_dispatch for SSR model hydration
//// on the server side.
import gleam/dynamic.{type Dynamic}
import gleam/result
import libero/json/error.{type JsonError}
@external(javascript, \"./protocol_wire.mjs\", \"decode_flags_typed\")
fn decode_json_flags(flags: String) -> Result(Dynamic, List(JsonError))
@external(javascript, \"./protocol_wire.mjs\", \"identity\")
fn coerce(value: a) -> b
/// Decode SSR flags from JSON text.
/// Returns the parsed JSON value as Dynamic; the caller
/// type-casts via coerce or uses typedJsonToGleamValue on the JS side.
pub fn decode_flags_typed(
flags: String,
_decoder_name: String,
) -> Result(a, List(JsonError)) {
use parsed <- result.try(decode_json_flags(flags))
Ok(coerce(parsed))
}
"/utf8>>.
-file("src/rally/generator/codec.gleam", 913).
-spec emit_codec_gleam(binary()) -> binary().
emit_codec_gleam(Protocol) ->
case Protocol of
<<"json"/utf8>> ->
json_codec_gleam_content();
_ ->
etf_codec_gleam_content()
end.
-file("src/rally/generator/codec.gleam", 845).
-spec collect_user_type_modules(libero@field_type:field_type()) -> list(binary()).
collect_user_type_modules(Ft) ->
case Ft of
{user_type, Module_path, _, _} ->
[Module_path];
{list_of, Inner} ->
collect_user_type_modules(Inner);
{option_of, Inner@1} ->
collect_user_type_modules(Inner@1);
{result_of, Ok, Err} ->
lists:append(
collect_user_type_modules(Ok),
collect_user_type_modules(Err)
);
{dict_of, K, V} ->
lists:append(
collect_user_type_modules(K),
collect_user_type_modules(V)
);
{tuple_of, Elems} ->
gleam@list:flat_map(Elems, fun collect_user_type_modules/1);
_ ->
[]
end.
-file("src/rally/generator/codec.gleam", 620).
-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/rally/generator/codec.gleam", 818).
?DOC(
" Build a resolver that uses the last segment when unique, or the full\n"
" underscored path when two modules share the same last segment.\n"
).
-spec build_type_alias_resolver(list(libero@scanner:handler_endpoint())) -> fun((binary()) -> binary()).
build_type_alias_resolver(Endpoints) ->
All_modules = begin
_pipe = Endpoints,
_pipe@1 = gleam@list:flat_map(
_pipe,
fun(E) ->
gleam@list:flat_map(
erlang:element(6, E),
fun(P) ->
collect_user_type_modules(erlang:element(2, P))
end
)
end
),
gleam@list:unique(_pipe@1)
end,
Segment_counts = gleam@list:fold(
All_modules,
maps:new(),
fun(Acc, Mod) ->
Seg = libero@field_type:last_segment(Mod),
Count = case gleam_stdlib:map_get(Acc, Seg) of
{ok, N} ->
N + 1;
{error, nil} ->
1
end,
gleam@dict:insert(Acc, Seg, Count)
end
),
fun(Module_path) ->
Seg@1 = libero@field_type:last_segment(Module_path),
case gleam_stdlib:map_get(Segment_counts, Seg@1) of
{ok, N@1} when N@1 > 1 ->
gleam@string:replace(Module_path, <<"/"/utf8>>, <<"_"/utf8>>);
_ ->
Seg@1
end
end.
-file("src/rally/generator/codec.gleam", 649).
-spec emit_types_gleam(
list({rally@types:scanned_route(), rally@types:page_contract()}),
list(libero@scanner:handler_endpoint()),
binary()
) -> binary().
emit_types_gleam(_, Endpoints, Protocol) ->
Resolve_alias = build_type_alias_resolver(Endpoints),
Client_msg_type = case Endpoints of
[] ->
<<""/utf8>>;
_ ->
Variants = gleam@list:map(
Endpoints,
fun(E) ->
Variant_name = to_pascal_case(
<<"server_"/utf8, (erlang:element(3, E))/binary>>
),
case erlang:element(6, E) of
[] ->
<<" "/utf8, Variant_name/binary>>;
Params ->
Fields = gleam@list:map(
Params,
fun(P) ->
<<<<(erlang:element(1, P))/binary,
": "/utf8>>/binary,
(libero@field_type:to_gleam_source_with_alias(
erlang:element(2, P),
Resolve_alias
))/binary>>
end
),
<<<<<<<<" "/utf8, Variant_name/binary>>/binary,
"("/utf8>>/binary,
(gleam@string:join(Fields, <<", "/utf8>>))/binary>>/binary,
")"/utf8>>
end
end
),
<<<<"\npub type ClientMsg {\n"/utf8,
(gleam@string:join(Variants, <<"\n"/utf8>>))/binary>>/binary,
"\n}\n"/utf8>>
end,
Json_encode_fn = case Protocol of
<<"json"/utf8>> ->
case Endpoints of
[] ->
<<""/utf8>>;
_ ->
Json_import = <<"import gleam/json\n"/utf8>>,
Arms = begin
_pipe = gleam@list:map(
Endpoints,
fun(E@1) ->
Variant_name@1 = to_pascal_case(
<<"server_"/utf8,
(erlang:element(3, E@1))/binary>>
),
case erlang:element(8, E@1) of
{some, {Type_module, Type_name}} ->
Param_pattern = case erlang:element(
6,
E@1
) of
[] ->
<<""/utf8>>;
Params@1 ->
<<<<"("/utf8,
(gleam@string:join(
gleam@list:map(
Params@1,
fun(P@1) ->
<<<<(erlang:element(
1,
P@1
))/binary,
": "/utf8>>/binary,
(erlang:element(
1,
P@1
))/binary>>
end
),
<<", "/utf8>>
))/binary>>/binary,
")"/utf8>>
end,
Field_encoders = case erlang:element(
6,
E@1
) of
[] ->
<<"json.object([])"/utf8>>;
Params@2 ->
Pairs = gleam@list:map(
Params@2,
fun(P@2) ->
<<<<<<<<"#(\""/utf8,
(erlang:element(
1,
P@2
))/binary>>/binary,
"\", "/utf8>>/binary,
(json_primitive_encoder(
erlang:element(
2,
P@2
),
erlang:element(
1,
P@2
)
))/binary>>/binary,
")"/utf8>>
end
),
<<<<"json.object(["/utf8,
(gleam@string:join(
Pairs,
<<", "/utf8>>
))/binary>>/binary,
"])"/utf8>>
end,
<<<<<<<<<<<<<<<<<<<<<<<<<<<<Variant_name@1/binary,
Param_pattern/binary>>/binary,
" -> json.object([\n"/utf8>>/binary,
" #(\"type\", json.string(\""/utf8>>/binary,
Type_module/binary>>/binary,
"."/utf8>>/binary,
Type_name/binary>>/binary,
"\")),\n"/utf8>>/binary,
" #(\"variant\", json.string(\""/utf8>>/binary,
Type_name/binary>>/binary,
"\")),\n"/utf8>>/binary,
" #(\"fields\", "/utf8>>/binary,
Field_encoders/binary>>/binary,
"),\n"/utf8>>/binary,
" ])"/utf8>>;
none ->
Field_encoders@1 = gleam@list:map(
erlang:element(6, E@1),
fun(P@3) ->
<<<<<<<<"#(\""/utf8,
(erlang:element(
1,
P@3
))/binary>>/binary,
"\", "/utf8>>/binary,
(json_primitive_encoder(
erlang:element(
2,
P@3
),
erlang:element(
1,
P@3
)
))/binary>>/binary,
")"/utf8>>
end
),
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<Variant_name@1/binary,
"("/utf8>>/binary,
(gleam@string:join(
gleam@list:map(
erlang:element(
6,
E@1
),
fun(
P@4
) ->
<<<<(erlang:element(
1,
P@4
))/binary,
": "/utf8>>/binary,
(erlang:element(
1,
P@4
))/binary>>
end
),
<<", "/utf8>>
))/binary>>/binary,
") -> json.object([\n"/utf8>>/binary,
" #(\"type\", json.string(\""/utf8>>/binary,
(erlang:element(
2,
E@1
))/binary>>/binary,
"."/utf8>>/binary,
(to_pascal_case(
<<"server_"/utf8,
(erlang:element(
3,
E@1
))/binary>>
))/binary>>/binary,
"\")),\n"/utf8>>/binary,
" #(\"variant\", json.string(\""/utf8>>/binary,
(to_pascal_case(
<<"server_"/utf8,
(erlang:element(
3,
E@1
))/binary>>
))/binary>>/binary,
"\")),\n"/utf8>>/binary,
" #(\"fields\", json.object(["/utf8>>/binary,
(gleam@string:join(
Field_encoders@1,
<<", "/utf8>>
))/binary>>/binary,
"])),\n"/utf8>>/binary,
" ])"/utf8>>
end
end
),
gleam@string:join(_pipe, <<"\n"/utf8>>)
end,
<<<<<<Json_import/binary,
"\npub fn json_encode_client_msg(msg: ClientMsg) -> json.Json {\n case msg {\n "/utf8>>/binary,
Arms/binary>>/binary,
"\n }\n}\n"/utf8>>
end;
_ ->
<<""/utf8>>
end,
All_modules = begin
_pipe@1 = Endpoints,
_pipe@2 = gleam@list:flat_map(
_pipe@1,
fun(E@2) ->
gleam@list:flat_map(
erlang:element(6, E@2),
fun(P@5) ->
collect_user_type_modules(erlang:element(2, P@5))
end
)
end
),
gleam@list:unique(_pipe@2)
end,
Type_imports = begin
_pipe@3 = All_modules,
_pipe@4 = gleam@list:map(
_pipe@3,
fun(Mod) ->
Alias = Resolve_alias(Mod),
case Alias =:= libero@field_type:last_segment(Mod) of
true ->
<<"import "/utf8, Mod/binary>>;
false ->
<<<<<<"import "/utf8, Mod/binary>>/binary, " as "/utf8>>/binary,
Alias/binary>>
end
end
),
_pipe@5 = gleam@string:join(_pipe@4, <<"\n"/utf8>>),
(fun(S) -> case S of
<<""/utf8>> ->
<<""/utf8>>;
_ ->
<<S/binary, "\n"/utf8>>
end end)(_pipe@5)
end,
Option_import = libero@codegen:import_if(
Endpoints,
fun libero@codegen:is_option/1,
<<"import gleam/option.{type Option}"/utf8>>
),
Dict_import = libero@codegen:import_if(
Endpoints,
fun libero@codegen:is_dict/1,
<<"import gleam/dict.{type Dict}"/utf8>>
),
<<<<<<<<<<<<"// Generated by Rally — do not edit.
////
//// Mirrored page types for the client package.
"/utf8,
Type_imports/binary>>/binary,
Option_import/binary>>/binary,
Dict_import/binary>>/binary,
Client_msg_type/binary>>/binary,
"\n"/utf8>>/binary,
Json_encode_fn/binary>>.
-file("src/rally/generator/codec.gleam", 634).
-spec emit_codec_ffi_with_endpoints(
list(libero@walker:discovered_type()),
list(libero@scanner:handler_endpoint())
) -> binary().
emit_codec_ffi_with_endpoints(Discovered, Endpoints) ->
libero@codegen_decoders:generate_decoders_ffi(
Discovered,
Endpoints,
<<"../../"/utf8>>,
<<"client"/utf8>>,
{some, <<"generated/types"/utf8>>}
).
-file("src/rally/generator/codec.gleam", 34).
?DOC(" Generate all codec files for the client package.\n").
-spec generate(
list({rally@types:scanned_route(), rally@types:page_contract()}),
list(libero@walker:discovered_type()),
list(libero@scanner:handler_endpoint()),
list(binary()),
binary()
) -> list(codec_file()).
generate(Contracts, Discovered, Endpoints, Server_symbols, Protocol) ->
Codec_files = [{codec_file,
<<"src/generated/codec_ffi.mjs"/utf8>>,
emit_codec_ffi_with_endpoints(Discovered, Endpoints)},
{codec_file,
<<"src/generated/types.gleam"/utf8>>,
emit_types_gleam(Contracts, Endpoints, Protocol)},
{codec_file,
<<"src/generated/codec.gleam"/utf8>>,
emit_codec_gleam(Protocol)},
{codec_file,
<<"src/rally_runtime/effect.gleam"/utf8>>,
emit_rally_effect_shim(Protocol)},
{codec_file,
<<"src/rally_runtime/rally_effect_ffi.mjs"/utf8>>,
emit_rally_effect_ffi()}],
Page_files = generate_page_modules(Contracts, Server_symbols, Protocol),
lists:append(Codec_files, Page_files).
-file("src/rally/generator/codec.gleam", 168).
?DOC(
" Generate a JS type registry that maps \"<module>.<type>#<variant>\"\n"
" identities to Gleam JS constructors. The registry is imported by\n"
" protocol_wire.mjs and used in typedJsonToGleamValue to produce\n"
" properly typed instances instead of generic CustomType objects.\n"
"\n"
" Keys include the parent type name so a mismatched \"type\" field\n"
" (e.g. { type: \"some/module.OldType\", variant: \"Discount\" }) never\n"
" resolves to the registry entry for \"some/module.Discount#Discount\".\n"
).
-spec next_unused_alias(gleam@dict:dict(binary(), nil), binary(), integer()) -> binary().
next_unused_alias(Used, Candidate, N) ->
Alias = case N of
-1 ->
Candidate;
_ ->
<<<<Candidate/binary, "_"/utf8>>/binary,
(erlang:integer_to_binary(N))/binary>>
end,
case gleam@dict:has_key(Used, Alias) of
false ->
Alias;
true ->
next_unused_alias(Used, Candidate, N + 1)
end.
-file("src/rally/generator/codec.gleam", 189).
?DOC(
" Build a collision-safe mapping from module_path to JS import alias.\n"
" Maintains a global used-aliases set. For each module the clean\n"
" candidate is tried first; if already taken, `_0`, `_1`, ... suffixes\n"
" are appended until an unused alias is found. This handles both\n"
" slash/underscore collisions (`admin/foo_bar/baz` vs `admin/foo/bar_baz`)\n"
" and suffix-poisoning (`admin/foo/bar_0` vs a suffixed `admin/foo/bar`).\n"
).
-spec build_module_aliases(list(binary())) -> gleam@dict:dict(binary(), binary()).
build_module_aliases(Modules) ->
{_, Aliases@2} = gleam@list:fold(
Modules,
{maps:new(), maps:new()},
fun(State, Mod) ->
{Used, Aliases} = State,
Candidate = <<"_m_"/utf8,
(gleam@string:replace(Mod, <<"/"/utf8>>, <<"_"/utf8>>))/binary>>,
Alias = next_unused_alias(Used, Candidate, -1),
Used@1 = gleam@dict:insert(Used, Alias, nil),
Aliases@1 = gleam@dict:insert(Aliases, Mod, Alias),
{Used@1, Aliases@1}
end
),
Aliases@2.
-file("src/rally/generator/codec.gleam", 202).
-spec generate_json_type_registry_js(list(libero@walker:discovered_type())) -> codec_file().
generate_json_type_registry_js(Discovered) ->
Modules = begin
_pipe = Discovered,
_pipe@1 = gleam@list:map(_pipe, fun(Dt) -> erlang:element(2, Dt) end),
gleam@list:unique(_pipe@1)
end,
Aliases = build_module_aliases(Modules),
Imports = case Modules of
[] ->
<<""/utf8>>;
_ ->
_pipe@2 = gleam@list:map(
Modules,
fun(Mod) ->
Alias = case gleam_stdlib:map_get(Aliases, Mod) of
{ok, A} ->
A;
{error, nil} ->
<<"_m_"/utf8,
(gleam@string:replace(
Mod,
<<"/"/utf8>>,
<<"_"/utf8>>
))/binary>>
end,
<<<<<<<<"import * as "/utf8, Alias/binary>>/binary,
" from \"../../client/"/utf8>>/binary,
Mod/binary>>/binary,
".mjs\";"/utf8>>
end
),
gleam@string:join(_pipe@2, <<"\n"/utf8>>)
end,
Entries = begin
_pipe@5 = gleam@list:flat_map(
Discovered,
fun(Dt@1) ->
Mod_alias = case gleam_stdlib:map_get(
Aliases,
erlang:element(2, Dt@1)
) of
{ok, A@1} ->
A@1;
{error, nil} ->
<<"_m_"/utf8,
(gleam@string:replace(
erlang:element(2, Dt@1),
<<"/"/utf8>>,
<<"_"/utf8>>
))/binary>>
end,
gleam@list:map(
erlang:element(5, Dt@1),
fun(V) ->
Key = <<<<<<<<(erlang:element(2, Dt@1))/binary,
"."/utf8>>/binary,
(erlang:element(3, Dt@1))/binary>>/binary,
"#"/utf8>>/binary,
(erlang:element(3, V))/binary>>,
Ctor_expr = case erlang:element(7, V) of
[] ->
<<<<<<<<"() => new "/utf8, Mod_alias/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(3, V))/binary>>/binary,
"()"/utf8>>;
_ ->
All_labelled = gleam@list:all(
erlang:element(6, V),
fun(L) -> L /= none end
),
case All_labelled of
true ->
Args = begin
_pipe@3 = gleam@list:filter_map(
gleam@list:zip(
erlang:element(7, V),
erlang:element(6, V)
),
fun(Pair) ->
{_, Label} = Pair,
case Label of
{some, Name} ->
{ok,
<<"fields."/utf8,
Name/binary>>};
none ->
{error, nil}
end
end
),
gleam@string:join(
_pipe@3,
<<", "/utf8>>
)
end,
<<<<<<<<<<<<"(fields) => new "/utf8,
Mod_alias/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(3, V))/binary>>/binary,
"("/utf8>>/binary,
Args/binary>>/binary,
")"/utf8>>;
false ->
Args@1 = begin
_pipe@4 = gleam@list:index_map(
erlang:element(7, V),
fun(_, I) ->
<<<<"fields["/utf8,
(erlang:integer_to_binary(
I
))/binary>>/binary,
"]"/utf8>>
end
),
gleam@string:join(
_pipe@4,
<<", "/utf8>>
)
end,
<<<<<<<<<<<<"(fields) => new "/utf8,
Mod_alias/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(3, V))/binary>>/binary,
"("/utf8>>/binary,
Args@1/binary>>/binary,
")"/utf8>>
end
end,
<<<<<<<<" \""/utf8, Key/binary>>/binary, "\": "/utf8>>/binary,
Ctor_expr/binary>>/binary,
","/utf8>>
end
)
end
),
gleam@string:join(_pipe@5, <<"\n"/utf8>>)
end,
Body = <<<<<<<<<<<<<<<<"// Generated by Rally — do not edit.\n"/utf8,
"// Type registry for JSON decode path.\n"/utf8>>/binary,
"// Maps \"<module>.<type>#<variant>\" identities to Gleam JS constructors.\n"/utf8>>/binary,
"\n"/utf8>>/binary,
Imports/binary>>/binary,
"\n\n"/utf8>>/binary,
"export const typeRegistry = {\n"/utf8>>/binary,
Entries/binary>>/binary,
"\n};\n"/utf8>>,
{codec_file, <<"src/generated/type_registry.mjs"/utf8>>, Body}.
-file("src/rally/generator/codec.gleam", 118).
?DOC(
" Generate a Gleam module that dispatches decoder names to the\n"
" corresponding JSON typed decoders at runtime.\n"
"\n"
" The decoder_name follows the ETF codec convention\n"
" (e.g. \"decode_pages_home__model\") and is mapped to the\n"
" `json_decode_<qualified_atom_name>` function in json_codecs.\n"
).
-spec generate_json_decode_dispatch(list(libero@walker:discovered_type())) -> codec_file().
generate_json_decode_dispatch(Discovered) ->
Cases = gleam@list:map(
Discovered,
fun(Dt) ->
Qual = libero@walker:qualified_atom_name(
erlang:element(2, Dt),
erlang:element(3, Dt)
),
<<<<<<<<" \"decode_"/utf8, Qual/binary>>/binary,
"\" -> result.map(json_codecs.json_decode_"/utf8>>/binary,
Qual/binary>>/binary,
"(value), identity)"/utf8>>
end
),
Body = <<<<"// Generated by Rally — do not edit.
////
//// JSON typed decode dispatch. Routes decoder names to the
//// corresponding typed decoders generated by libero JSON codegen.
import gleam/dynamic.{type Dynamic}
import gleam/result
import generated/json_codecs as json_codecs
import libero/json/error.{type JsonError, JsonError}
@external(javascript, \"./protocol_wire.mjs\", \"identity\")
fn identity(value: a) -> b
/// Decode a Dynamic value using the named typed decoder.
/// The decoder_name follows the pattern decode_<module>__<type>,
/// matching the ETF codec naming convention.
pub fn decode_json_typed(
value: Dynamic,
decoder_name: String,
) -> Result(Dynamic, List(JsonError)) {
case decoder_name {
"/utf8,
(gleam@string:join(Cases, <<"\n"/utf8>>))/binary>>/binary,
"\n _ -> Error([JsonError(\"decoder\", \"unknown: \" <> decoder_name)])
}
}
"/utf8>>,
{codec_file, <<"src/generated/json_decode_dispatch.gleam"/utf8>>, Body}.
-file("src/rally/generator/codec.gleam", 93).
?DOC(
" Generate JSON typed encoder/decoder source for the client package.\n"
" Uses libero's JSON codegen to produce per-type json.Json builders\n"
" and typed decoders for all discovered types.\n"
).
-spec generate_json_codecs(
list(libero@walker:discovered_type()),
list(libero@scanner:handler_endpoint())
) -> list(codec_file()).
generate_json_codecs(Discovered, _) ->
case libero@json@codegen:generate(Discovered) of
{ok, Content} ->
[{codec_file, <<"src/generated/json_codecs.gleam"/utf8>>, Content},
generate_json_decode_dispatch(Discovered),
generate_json_type_registry_js(Discovered)];
{error, Errors} ->
gleam@list:each(
Errors,
fun(E) ->
gleam_stdlib:println_error(
<<<<<<"JSON codegen error: "/utf8,
(erlang:element(2, E))/binary>>/binary,
": "/utf8>>/binary,
(erlang:element(3, E))/binary>>
)
end
),
[]
end.
-file("src/rally/generator/codec.gleam", 470).
?DOC(
" Extract (module_path, type_name) seeds from a client_context.gleam\n"
" source so the walker can discover ClientContext types with proper\n"
" field type resolution, instead of the old hardcoded-StringField path.\n"
).
-spec client_context_seeds(binary(), binary()) -> list({binary(), binary()}).
client_context_seeds(Source, Module_path) ->
case glance:module(Source) of
{error, unexpected_end_of_input} ->
[];
{error, {unexpected_token, _, _}} ->
[];
{ok, Ast} ->
gleam@list:map(
erlang:element(3, Ast),
fun(Def) ->
{Module_path, erlang:element(3, erlang:element(3, Def))}
end
)
end.