# Libero Context Libero is an RPC plumbing library for Gleam (6.0.0). It scans Gleam source for handler function signatures and generates typed server dispatch, JavaScript decoders, Erlang atom pre-registration, and ETF wire protocol code. Most users run `gleam run -m libero` to generate server files into `src/generated/libero/` and `src/generated@rpc_atoms.erl`. Frameworks call the library API programmatically. Target boundary: Libero owns the typed client/server contract derived from handler signatures. Consumers should use generated modules and protocol helpers instead of knowing the wire shape. A good boundary test is that Libero can add JSON RPC as a configured protocol while ETF stays available. Consumers should only regenerate Libero-owned modules or update facade imports. They should not rewrite transport lifecycle, response handling, push handling, or hydration logic because the configured protocol changed. See `docs/contract-boundary-spec.md`. ## Conventions Libero is opinionated: context type is `ServerContext` in the `server_context` module, wire tag is `"rpc"`, source is always `src/`, server output is always `src/generated/libero/`, atoms output is always `src/generated@rpc_atoms.erl`, and `src/generated/` is skipped during scanning. Libero does not generate client stubs, WebSocket clients, routers, SSR, scaffolded apps, or framework transport. Frameworks such as Rally own those layers. Rally may use Libero for scanner, walker, dispatch, atoms, and ETF primitives, but Rally should generate its own client `ClientMsg` mirror and transport API. ## CLI ```sh gleam run -m libero ``` Writes three files to `src/generated/libero/`: | File | Purpose | |------|---------| | `dispatch.gleam` | server `ClientMsg` type and `handle` function with panic catching | | `rpc_decoders_ffi.mjs` | Typed JS decoders for every discovered type | | `rpc_decoders.gleam` | Gleam wrapper that registers the JS decoders | Also writes `src/generated@rpc_atoms.erl` for Erlang atom pre-registration. To mirror the JavaScript decoder files into a client package, set `LIBERO_CLIENT_OUT_DIR`, for example: ```sh LIBERO_CLIENT_OUT_DIR="../clients/web/src/generated/libero" gleam run -m libero ``` `LIBERO_CLIENT_OUT_DIR` only mirrors decoder files. It does not generate client message constructors or transport code. ## Library API ```gleam import libero let assert Ok(endpoints) = libero.scan() // 1. Discover endpoints // Or, to exclude server-injected params (e.g. auth identity) before // message-type resolution: // let assert Ok(endpoints) = libero.scan_excluding( // exclude_param_types: [#("admin/auth", "Identity")], // ) let seeds = libero.collect_seeds(endpoints) // 2. Extract type seeds let assert Ok(discovered) = libero.walk(seeds) // 3. Walk type graph let dispatch_source = libero.generate_dispatch(endpoints) // 4. Generate dispatch let decoders_js = libero.generate_decoders_ffi(discovered, endpoints) // 5. Generate JS decoders let decoders_gleam = libero.generate_decoders_gleam() // 6. Generate Gleam wrapper ``` Each function returns a string, so the caller owns file I/O. Internal modules (`libero/scanner`, `libero/codegen_dispatch`, etc.) keep flexible parameterized signatures for framework consumers that need programmatic control. ## What it provides 1. **Scanner** (`libero/scanner`): Discovers handler endpoints by parsing Gleam source files. Looks for public functions with a `server_` prefix, a `ServerContext` parameter, and a `Result` return type. `scan_excluding(exclude_param_types:)` filters out handler params whose resolved type matches any `#(module_path, type_name)` before message-type resolution, so frameworks can strip server-injected params (like auth identity) from the contract. 2. **Walker** (`libero/walker`): BFS traversal of the type graph starting from scanner output. Discovers all custom types reachable from handler params and return types. 3. **Dispatch codegen** (`libero/codegen_dispatch`): Generates a Gleam module with a `ClientMsg` type and a `handle` function. Wraps each handler in `trace.try_call` for panic safety. 4. **Decoder codegen** (`libero/codegen_decoders`): Generates JavaScript typed decoders so the JS client can reconstruct Gleam values from ETF. It also emits float type hints used by the JS ETF encoder. 5. **ETF wire protocol** (`libero/etf/wire`): ETF codec for both Erlang and JavaScript targets. Request envelope, response framing, variant tag extraction. `libero/wire` remains as a compatibility wrapper. 6. **RemoteData** (`libero/remote_data`): Client-side state machine for async data. `RpcData(value, domain)` separates transport errors from domain errors. 7. **Error types** (`libero/error`): `RpcError` (MalformedRequest, UnknownFunction, InternalError), `DecodeError`, `PanicInfo`. 8. **Trace** (`libero/trace`): Panic catching via Erlang try/catch, trace ID generation for log correlation. ## Handler-as-Contract Libero's scanner detects RPC endpoints by checking: 1. Public function with `server_` prefix 2. A `ServerContext` parameter 3. Return type is either: - `Result(value, error)` for read-only handlers - `#(Result(value, error), ServerContext)` for mutating handlers 4. All types in the signature are builtins or resolvable from the source tree ```gleam pub fn server_get_items( server_ctx server_ctx: ServerContext, ) -> Result(List(Item), ItemError) { Ok(server_ctx.items) } pub fn server_create_item( params params: ItemParams, server_ctx server_ctx: ServerContext, ) -> #(Result(Item, ItemError), ServerContext) { // ... } ``` ## Generated dispatch shape ```gleam //// Code generated by libero. DO NOT EDIT. import gleam/io import libero/error.{InternalError, MalformedRequest, UnknownFunction} import libero/trace import libero/etf/wire pub type ClientMsg { GetItems CreateItem(params: types.ItemParams) } pub fn handle( server_context server_context: ServerContext, data data: BitArray, ) -> #(BitArray, ServerContext) { case wire.decode_request(data) { Ok(#("rpc", request_id, msg)) -> { case wire.variant_tag(msg) { Ok("get_items") | Ok("create_item") -> { let typed_msg: ClientMsg = wire.coerce(msg) case typed_msg { GetItems -> { case trace.try_call(fn() { handler.server_get_items(server_context:) }) { Ok(result) -> #(wire.tag_response(request_id:, data: wire.encode(result)), server_context) Error(reason) -> { let trace_id = trace.new_trace_id() io.println_error("[libero] " <> trace_id <> " get_items: " <> reason) #(wire.tag_response(request_id:, data: wire.encode(Error(InternalError(trace_id:, message: "Something went wrong")))), server_context) } } } // ... } } Ok(tag) -> #(wire.tag_response(..., Error(UnknownFunction(...))), server_context) Error(_) -> #(wire.tag_response(..., Error(MalformedRequest)), server_context) } } // ... } } ``` ## Wire protocol - Format: ETF (Erlang External Term Format), binary WebSocket frames - Request envelope: `{module_name_binary, request_id, client_msg_value}` (3-tuple) - Response frame: `<<0x00, request_id:32-big, etf_bytes>>` - Push frame: `<<0x01, etf_bytes>>` (server-initiated, no request ID) - JS codec: custom ETF encoder/decoder in `libero/etf/wire_ffi.mjs` with typed decoder layer - Float handling: generated code calls `registerFieldTypes` so JavaScript whole-number floats are encoded as ETF floats, including inside `List`, `Option`, `Result`, `Dict`, and tuple containers ## Error model Two tiers: 1. **Domain errors**: inside the handler's Result. `Error(NotFound)` arrives as `Failure(DomainError(NotFound))`. 2. **Transport errors**: `MalformedRequest`, `UnknownFunction(name)`, `InternalError(trace_id, message)`. Arrives as `Failure(TransportError(rpc_err))`. ## RemoteData ```gleam pub type RemoteData(value, error) { NotAsked | Loading | Failure(error) | Success(value) } pub type RpcOutcome(domain) { TransportError(RpcError) | DomainError(domain) } pub type RpcData(value, domain) = RemoteData(value, RpcOutcome(domain)) ``` ## Module layout | Module | Target | Purpose | |--------|--------|---------| | `libero` | both | Public API facade and CLI entry point | | `libero/scanner` | erlang | Handler discovery | | `libero/walker` | erlang | Type graph traversal | | `libero/codegen_dispatch` | erlang | Server dispatch generator | | `libero/codegen_decoders` | erlang | JS typed decoder generator | | `libero/codegen` | both | Cross-cutting codegen helpers | | `libero/etf/wire` | both | ETF codec | | `libero/wire` | both | Compatibility wrapper for ETF codec | | `libero/error` | both | RpcError, DecodeError, PanicInfo | | `libero/remote_data` | both | RemoteData/RpcData | | `libero/field_type` | both | Structured type representation | | `libero/trace` | erlang | Panic catching + trace IDs | | `libero/format` | erlang | Gleam code formatter integration | | `libero/gen_error` | both | Structured codegen error types | ## Dependencies - `gleam_stdlib` (>= 0.69.0) - `glance` (~> 6.0): Gleam parser for scanning handler signatures - `simplifile` (~> 2.0): file I/O for source tree walking and CLI output - `glexer` (>= 2.4.0): lexer for error formatting