From dd7c71c3d48ea1c7f9a42b0830e30ab9ab309ed2 Mon Sep 17 00:00:00 2001 From: Matt Stavola Date: Wed, 15 Apr 2026 23:46:51 -0400 Subject: [PATCH] Add docs for conversion quirks --- website/content/docs/cli/06-generate.md | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/website/content/docs/cli/06-generate.md b/website/content/docs/cli/06-generate.md index 9932e1b..2d2d2ad 100644 --- a/website/content/docs/cli/06-generate.md +++ b/website/content/docs/cli/06-generate.md @@ -134,6 +134,17 @@ mlf generate lexicon -i ./src/lexicons -o ./dist --root ./src/lexicons Without `--root`, it defaults to the `source.directory` from mlf.toml or the current directory. +### `@const` Float Values + +ATProto's data model has no floats — integers only. Annotations themselves accept any numeric literal (they're free-form metadata), but `@const` is special: its values become actual fields in the generated JSON Lexicon. + +To keep output spec-compliant, the lexicon generator applies two rules to `@const` numeric values: + +- **Whole-number floats → integers.** `@const("revision", 3)` emits `"revision": 3` (JSON number), never `3.0`. +- **Fractional floats → strings.** `@const("x-threshold", 3.14)` emits `"x-threshold": "3.14"` (JSON string) and prints a warning. Wrapping as a string is the spec-compliant way to carry non-integer numeric values through ATProto. + +If you genuinely need a float-shaped value in your lexicon JSON, pass it as a string in MLF — `@const("x-threshold", "3.14")` — to skip the coercion. + --- ## Generate Code @@ -377,6 +388,36 @@ record thread { } ``` +### Conversion Warnings + +Some published lexicons in the wild use shapes the ATProto spec doesn't strictly require, or def types MLF doesn't have dedicated syntax for. The converter accepts these with a warning rather than failing, so you can always roundtrip a lexicon and decide what to do next. + +**Lenient handling of non-spec shapes:** + +| Source JSON shape | Converter behavior | +|---|---| +| `object` with no `properties` field | Treated as an empty object; emits `{}` | +| Open `union` with empty `refs: []` | Emits `unknown` | +| `array` with no `items` field | Emits `unknown[]` | + +Each of these produces a warning printed to stderr identifying the namespace and the shape that was coerced. + +**Unknown def types:** + +When a def's `type` isn't in MLF's known set (`record`, `query`, `procedure`, `subscription`, `object`, `array`, `union`, `ref`, `token`, primitive types), the converter emits it as a placeholder: + +```mlf +@const("type", "permission-set") +@const("permissions", [...]) +def type mySet = unknown; +``` + +Every field of the original JSON def is preserved as `@const` annotations. On the return trip through `mlf generate lexicon`, codegen re-emits all those fields verbatim, making any future spec-defined def type roundtrip automatically without grammar work. + +**NSID-shaped `@const` strings:** + +When a `@const` string value contains `#` (the ATProto local-ref marker), the converter emits a warning suggesting you may want `@reference` instead when hand-editing the MLF. `@reference` resolves type paths through the workspace and emits the resulting NSID at codegen time, which is useful when the target moves; `@const` is kept as the default since it's always safe. + --- ## Code Generator Features -- 2.51.2