atproto_lexicon

Alpha, pre-release. Pre-1.0, evolving alongside at-record; expect breaking changes between 0.x releases.

A faithful, bidirectional atproto lexicon AST for Gleam. encode(decode(json)) is structurally equal to the source for every corpus file (key order aside); absent optional fields stay absent. Unrecognized keys are dropped on decode (documented fidelity gap); an unrecognized type discriminator is a decode error.

Installation

Monorepo path dependency:

[dependencies]
atproto_lexicon = { path = "../atproto_lexicon" }

Usage

Decode/encode:

import atproto_lexicon/decoding
import atproto_lexicon/encoding
let assert Ok(doc) = decoding.decode_json(lexicon_json)
let round_tripped = encoding.to_json_string(doc)

Semantic diff, classified for a CI drift gate:

import atproto_lexicon/diff
import gleam/list
let changes = diff.diff(old_doc, new_doc)
let breaking = list.any(changes, fn(c) { diff.severity(c.kind) == diff.Breaking })

Resolve a lexicon over the network and pin it to a local cache:

import atproto_lexicon/pin
import atproto_lexicon/source
let assert Ok(spec) = source.parse_spec("com.example.thing")
let network = source.default_network_config()
let assert Ok(summary) =
pin.refresh(send, "./lexicons", "./lexicons.lock.json", [spec], network, fetched_at)
let assert Ok(#(docs, _lock)) =
pin.read("./lexicons", "./lexicons.lock.json", [spec])

send is a caller-injected source.Send; nothing in this package performs IO on its own.

Architecture

ModuleWhat it does
astThe lexicon types; def-position vs. property-position type split
decodingdecode_json/decode_dynamic, composable document_decoder(), structured errors
encodingencode/to_json_string
diffSemantic doc diff, breaking vs. non-breaking classification (CI drift gate)
sourceSpec parsing + network resolution: NSID authority -> DNS -> DID -> PDS -> record
pinPinned schema cache with lexicons.lock.json; refresh online, read offline

This is the shared core of the lexicon toolchain: the syntax front-ends atproto_mlf and atproto_sdl and the codegen back-end atproto_codegen all meet at atproto_lexicon/ast.

Development

gleam test. The corpus test walks test/fixtures/**/*.json (vendored at-record snapshot); refresh by re-copying and bumping the count in test/corpus_test.gleam.