Libero

Libero

Package VersionHex Docs

Libero is Rally's typed wire-contract layer for Gleam.

Frameworks like Rally decide which values cross the boundary. Libero walks those seeded types, generates the codec artifacts, and exposes ETF and JSON wire helpers that agree on type identity. It does not scan handlers, generate dispatch modules, write app transport, or own request/result/push API design.

That boundary is deliberate. For example, Rally owns pages, loaders, actions, request correlation, WebSocket lifecycle, SSR, hydration, and broadcast delivery. Libero owns the shared type graph and the protocol-facing pieces that must stay in lockstep.

What Libero Generates

Rally currently uses these Libero outputs:

ArtifactPurpose
generated@libero_atoms.erlPre-registers ETF atoms before safe BEAM decode.
generated@libero_wire.erlConverts seeded custom types between BEAM shape and wire shape.
decoders_ffi.mjsJavaScript ETF decoders for every discovered type.
decoders.gleamGleam wrapper that registers JavaScript decoders.
etf.gleamNeutral ETF facade used by generated framework protocol code.
contract.jsonJSON contract artifact with protocol version, hash, push types, SSR models, and discovered types.

Libero also keeps the JSON codec generator and JSON wire helpers. Those are for framework-generated JSON contracts, not a separate standalone application path.

Library API

Frameworks call Libero from their own generator:

import gleam/option
import libero
let seeds = [
#("public/pages/home", "ServerMsg"),
#("public/pages/home", "LoadResult"),
#("libero/error", "TransportError"),
]
let assert Ok(discovered) = libero.walk(seeds)
let atoms =
libero.generate_atoms(
discovered:,
atoms_module: "generated@libero_atoms",
wire_module: option.Some("generated@libero_wire"),
)
let assert Ok(wire) =
libero.generate_wire_erl(
discovered:,
wire_module: "generated@libero_wire",
)
let decoders_js =
libero.generate_decoders_ffi(
discovered:,
package: "my_app",
dependency_packages: ["shared"],
)
let decoders_gleam = libero.generate_decoders_gleam()
let etf =
libero.generate_etf_codec_module(
atoms_module: "generated@libero_atoms",
decoders_module: "generated/libero/decoders",
)
let contract =
libero.generate_json_contract(
discovered:,
push_types: [],
ssr_models: [],
)

Each function returns source text or JSON text. The caller owns file layout, formatting, rebuilds, and whether generated files are checked in.

Type Discovery

libero.walk(seeds) searches ./src and follows custom types reachable from the given #(module_path, type_name) seeds. Lower-level callers can use libero/source.walk_directory and libero/walker.walk when they need a different source root, as the JavaScript E2E fixture does for a staged multi-package project.

Libero skips generated directories while walking source. It also skips stdlib and Libero runtime modules whose shapes are handled by codec runtime support.

Wire Runtime

ETF helpers live in libero/etf/wire. JSON helpers live in libero/json/wire. The generated ETF module from generate_etf_codec_module exposes only the surface Rally uses: ensure, encode, and decode. Rally-generated protocol modules own request ids, frame tags, dispatch, and result envelopes around those helpers.

The lower-level runtime modules still expose codec helper functions used by the ETF and JSON test matrix:

ConceptETF helperJSON helper
Encode a request envelopeencode_requestencode_request
Decode a request envelopedecode_requestdecode_request
Encode a response frameencode_responseencode_response
Decode a server framedecode_server_framedecode_server_frame
Encode a push frameencode_pushencode_push
Encode SSR flagsencode_flagsencode_flags
Decode SSR flagsdecode_flags_typeddecode_flags_typed

ETF preserves BEAM term fidelity and uses generated wire hashes for user custom types. JSON uses readable type identity and contract hashes. Both codecs use the same discovered type graph.

Should I Use ETF Or JSON?

Use ETF for Rally-style Gleam browser/server traffic. It is the path Rally uses by default, it preserves BEAM term fidelity, and it keeps the generated protocol surface compact.

Use JSON when a non-Gleam client, SDK, fixture, log, or tool needs to inspect or produce protocol data without an ETF implementation. JSON is readable and uses the same contract hashes, but payloads are larger and generated validators do more shape checking at the boundary.

For measured performance guidance, see the benchmark README.

ETF Safety

Untrusted ETF input is decoded with binary_to_term(Bin, [safe, used]) on the BEAM. This blocks new atom creation and rejects trailing bytes. Generated atom registration makes legitimate custom type atoms available before decode.

libero/etf/wire.set_strict_data_terms(True) enables an extra BEAM term-kind validator that rejects pids, refs, ports, and functions after decode. It is off by default because generated typed decoding already walks legitimate payloads. Applications that intentionally accept hand-written ETF should also enforce transport frame limits and process memory limits.

libero/etf/wire.set_js_term_depth_limit(512) enables the optional JavaScript recursive ETF depth cap. 0 disables it, which is the default.

Example

Rally Scoreboard is the canonical consumer. It uses Rally's generator to pick the wire types, then uses Libero to generate the shared codec and contract artifacts under src/generated/libero/**.

More Docs

License

MIT. See LICENSE.