idiolect-records
Serde record types and typed atproto identifiers for the
dev.idiolect.* lexicon family.
Overview
The crate ships two layers. The first is a typed core for atproto
identifiers (Nsid, AtUri, Did) that enforces the spec at parse
time so malformed values cannot leak into routing, codegen, or
dispatch. The second is the generated record set: every lexicon in
lexicons/dev/idiolect/ (plus the vendored dev/panproto/ tree)
produces a strongly-typed struct, deserializable from the lexicon's
canonical JSON shape. The generated tree mirrors the lexicon
directory layout 1:1: lexicons/dev/idiolect/encounter.json emits
generated/dev/idiolect/encounter.rs. Each lexicon's main record
type is re-exported at the crate root for ergonomic call sites
(idiolect_records::Encounter).
Architecture
flowchart LR
LEX["lexicons/dev/idiolect/*.json<br/>+ vendored dev/panproto/"]
CG["idiolect-codegen"]
subgraph crate["idiolect-records"]
TYPED["nsid · at_uri · did<br/>(typed identifiers)"]
GEN["generated/dev/<authority>/…/<name>.rs<br/>(one module per lexicon)"]
FAM["IdiolectFamily<br/>+ AnyRecord + decode_record<br/>(generated/family.rs)"]
TRAIT["Record trait<br/>(NSID const · nsid() -> Nsid)"]
FAMTRAIT["RecordFamily trait<br/>+ OrFamily / OrAny composers"]
EX["examples::<br/>minimally-valid fixtures"]
end
CONS["consumers: indexer · orchestrator · observer · lens · verify · cli"]
LEX --> CG --> GEN
GEN --> TRAIT
GEN --> FAM
GEN --> EX
FAMTRAIT --> FAM
TYPED --> CONS
FAM --> CONS
FAMTRAIT --> CONS
TRAIT --> CONS
RecordFamily is the trait every workspace boundary parameterises
over once it wants to consume more than one record set: a membership
predicate over NSIDs, a decoder, and a serializer back. The
IdiolectFamily marker (codegen output) implements it for the
dev.idiolect.* set. Two families compose via OrFamily<F1, F2>
into a single family that recognises every NSID either side claims;
its AnyRecord is the tagged union OrAny. Use
detect_or_family_overlap at boot when wiring an OrFamily to
catch configuration errors that would otherwise silently shadow the
right side.
AnyRecord is the runtime discriminated union across every shipped
idiolect record kind, and decode_record(nsid, value) dispatches
into the matching variant. Both are generated from the lexicon set
by idiolect-codegen's family emitter; adding a record is a
one-file lexicon change. The RecordFamily impl on IdiolectFamily
delegates contains/decode to the same generated table, so
consumers can switch between the bare decode_record API and the
family-generic F::decode API without behaviour change.
Usage
use ;
// Decode a record body whose nsid is only known at runtime.
let nsid = parse?;
let record: AnyRecord = decode_record?;
match record
// Or decode directly into a typed struct.
let e: Encounter = from_value?;
The same dispatch is reachable through the family abstraction, which is what indexer / orchestrator / observer code sees:
use ;
let nsid = parse?;
match decode?
Compose two families into one when you want to index across record sets in a single pipeline:
use ;
// Pretend `LayersFamily` came from a sibling codegen pass.
type Combined = ;
// drive_indexer::<Combined, _, _, _>(...).await?;
Every generated record type implements the Record trait, which
carries const NSID: &'static str plus fn nsid() -> Nsid and
fn kind() for generic code:
use Record;
Minimally-valid record fixtures are available via
idiolect_records::examples:
use examples;
let e = encounter;
let json = ENCOUNTER_JSON;
Sub-modules (defs, query types, vendored panproto records) are addressed by their full lexicon path:
use ;
use PanprotoLens;
Design notes
- Records:
#[serde(rename_all = "camelCase")]. - Enum variants:
#[serde(rename_all = "kebab-case")]. - Datetimes: RFC 3339
Strings (compared byte-wise for ordering; callers parse viatime::OffsetDateTimewhen they need arithmetic). - CID links:
{ "$link": "bafy..." }wrapper (ingenerated::dev::idiolect::defs). #[serde(skip_serializing_if = "Option::is_none")]on every optional field.Nsid::parseenforces the atproto spec (≥3 segments, ASCII only, ≤317 bytes total, ≤63 bytes per segment, name segment is camelCase).AtUri::parserejects fragments, query strings, and trailing or extra path segments — the at-uris idiolect cares about always point at a single record.
Stability
idiolect is pre-1.0. Releases in the 0.x series may include
arbitrary breaking changes between minor versions — Rust APIs,
lexicon shapes, wire formats, and CLI surfaces are all in scope.
Pin to an exact version if you depend on this crate, and read
CHANGELOG.md before bumping.
Related
idiolect-codegen— emits this crate.@idiolect-dev/schema— TypeScript twin, generated from the same lexicons.