# atproto_lexicon

> **Alpha, pre-release.** Pre-1.0, evolving alongside
> [at-record](https://tangled.org/@mokkenstorm.dev/at-record); expect breaking
> changes between 0.x releases.

A faithful, bidirectional [atproto lexicon](https://atproto.com/specs/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:

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

## Usage

Decode/encode:

```gleam
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:

```gleam
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:

```gleam
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

| Module     | What it does                                                                       |
| ---------- | ---------------------------------------------------------------------------------- |
| `ast`      | The lexicon types; def-position vs. property-position type split                   |
| `decoding` | `decode_json`/`decode_dynamic`, composable `document_decoder()`, structured errors |
| `encoding` | `encode`/`to_json_string`                                                          |
| `diff`     | Semantic doc diff, breaking vs. non-breaking classification (CI drift gate)        |
| `source`   | Spec parsing + network resolution: NSID authority -> DNS -> DID -> PDS -> record   |
| `pin`      | Pinned 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`](https://hex.pm/packages/atproto_mlf) and
[`atproto_sdl`](https://hex.pm/packages/atproto_sdl) and the codegen back-end
[`atproto_codegen`](https://hex.pm/packages/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`.
