···4455## Introduction
6677-### What's TypeSpec?
88-99-[TypeSpec](https://typespec.io/) is a TypeScript-like language for writing schemas for data and API calls.
1010-1111-TypeSpec offers flexible syntax for describing schemas, as well as the tooling for it (like LSP), but it doesn't specify the semantics or the output format. It can target different output formats via *emitters*. For example, there's a [JSON Schema emitter](https://typespec.io/docs/emitters/json-schema/reference/) and a [Protobuf emitter](https://typespec.io/docs/emitters/protobuf/reference/) for TypeSpec. Emitters determine the output format. Emitters can define or restrict the available built-in types. Emitters can also define their own *decorators* that attach to different pieces of syntax.
1212-137### What's Lexicon?
1481515-[Lexicon](https://atproto.com/specs/lexicon) is a schema format used by [AT](https://atproto.com/) applications.
1616-1717-Here is a small Lexicon schema defining an `app.bsky.bookmark.defs` Lexicon containing a `listItemView` definition, which describes an `object` with a required `uri` property of type `at-uri`:
99+[Lexicon](https://atproto.com/specs/lexicon) is a schema format used by [AT](https://atproto.com/) applications. Here's a small example:
18101911```json
2012{
···3224}
3325```
34263535-You would then generate code from this schema that takes care of parsing and validating a piece of data of that shape, as well as the type definitions (e.g. for Go or TypeScript).
2727+This schema is then used to generate code for parsing of these objects, their validation, and their types.
36283737-### What's typelex?
2929+### What's TypeSpec?
38303939-Typelex is a TypeSpec emitter that targets AT Lexicon as the output format. As such, it adds built-in Lexicon data types and a few decorators to control the structure of the Lexicon output.
3131+[TypeSpec](https://typespec.io/) is a TypeScript-like language for writing schemas for data and API calls. It offers flexible syntax and tooling (like LSP), but doesn't specify output format—that's what *emitters* do. For example, there's a [JSON Schema emitter](https://typespec.io/docs/emitters/json-schema/reference/) and a [Protobuf emitter](https://typespec.io/docs/emitters/protobuf/reference/). Emitters can define built-in types and *decorators* for different pieces of syntax.
3232+3333+### What's typelex?
40344141-Here's the above Lexicon written in TypeSpec (with the typelex emitter):
3535+Typelex is a TypeSpec emitter targeting Lexicon. Here's the same schema in TypeSpec:
42364337```typescript
4438import "@typelex/emitter";
···5044}
5145```
52465353-Then you run the compiler, and it generates the Lexicon JSON for you.
4747+Run the compiler, and it generates Lexicon JSON for you.
54485555-It is important to note that the JSON format is in which you'll publish your Lexicons. Typelex only exists for authoring convenience and does not aim to supplant or replace Lexicon JSON itself. Think of it as a "CoffeeScript for Lexicon" (however terrible that may be).
4949+The JSON is what you'll publish—typelex just makes authoring easier. Think of it as "CoffeeScript for Lexicon" (however terrible that may be).
56505757-Typelex tries to stay faithful to both TypeSpec idioms and Lexicon concepts, which makes it a bit confusing and sometimes not elegant. Since we can't add new keywords to TypeSpec, often there's a decorator that fills the need. For example, you'll have to write `@procedure op` whereas ideally there would just be a `procedure` keyword, or you'll have to write `model` for something that Lexicon calls a "def". In essence, you have to learn both Lexicon *and* TypeSpec. It is a good idea to scan through the [TypeSpec language overview](https://typespec.io/docs/language-basics/overview/) to get a feel for it.
5151+Typelex tries to stay faithful to both TypeSpec idioms and Lexicon concepts, which is a tricky balance. Since we can't add keywords to TypeSpec, decorators fill the gaps—you'll write `@procedure op` instead of `procedure`, or `model` for what Lexicon calls a "def". One downside of this approach is you'll need to learn both Lexicon *and* TypeSpec to know what you're doing. Scan the [TypeSpec language overview](https://typespec.io/docs/language-basics/overview/) to get a feel for it, it isn't much.
58525959-Personally, I find JSON Lexicons difficult to read and to author so the tradeoff is worth it for me.
5353+Personally, I find JSON Lexicons hard to read and author, so the tradeoff works for me.
60546155### Playground
62566357[Open Playground](https://playground.typelex.org/) to play with a bunch of lexicons and to see how typelex code translates to Lexicon JSON. If you already know Lexicon, you can learn the syntax by just opening the Lexicons you're familiar with.
64586559## Quick Start
6666-6767-Let's dive into some common patterns and see how to write them.
68606961### Namespaces
70627171-A namespace corresponds to a Lexicon output file. For example:
6363+A namespace corresponds to a Lexicon file:
72647365```typescript
7466import "@typelex/emitter";
···8072}
8173```
82748383-will emit `app/bsky/feed/defs.json`:
7575+This emits `app/bsky/feed/defs.json`:
84768577```json
8678{
8779 "lexicon": 1,
8880 "id": "app.bsky.feed.defs",
8989- // ...
8181+ "defs": { ... }
9082}
9183```
92849393-You can [try it in the playground](https://playground.typelex.org/?c=aW1wb3J0ICJAdHlwZWxleC9lbWl0dGVyIjsKCm5hbWVzcGFjZSBhcHAuYnNreS5mZWVkLmRlZnMgewogIG1vZGVsIFBvc3RWaWV3xRMgIHJlcGx5Q291bnQ%2FOiBpbnRlZ2VyO8gab3N01RtsaWtl0xl9Cn0K&e=%40typelex%2Femitter&options=%7B%7D&vs=%7B%7D).
8585+[Try it in the playground](https://playground.typelex.org/?c=aW1wb3J0ICJAdHlwZWxleC9lbWl0dGVyIjsKCm5hbWVzcGFjZSBhcHAuYnNreS5mZWVkLmRlZnMgewogIG1vZGVsIFBvc3RWaWV3xRMgIHJlcGx5Q291bnQ%2FOiBpbnRlZ2VyO8gab3N01RtsaWtl0xl9Cn0K&e=%40typelex%2Femitter&options=%7B%7D&vs=%7B%7D).
94869595-### Reserved Words
8787+Use backticks for reserved words:
96889797-Use backticks for TypeScript/TypeSpec reserved words:
8989+```typescript
9090+import "@typelex/emitter";
98919999-```typescript
10092namespace app.bsky.feed.post.`record` { }
101101-10293namespace `pub`.blocks.blockquote { }
10394```
10495105105-### Multiple Namespaces in One File
106106-107107-Multiple namespaces can be defined in one file:
9696+You can define multiple namespaces in one file:
1089710998```typescript
11099import "@typelex/emitter";
111100112101namespace com.example.foo {
113113- @rec("tid")
114114- model Main {
115115- bar?: com.example.bar.Main
116116- }
102102+ model Main { /* ... */ }
117103}
118104119105namespace com.example.bar {
120120- model Main {
121121- @maxGraphemes(1)
122122- bla?: string;
123123- }
106106+ model Main { /* ... */ }
124107}
125108```
126109127127-This single `.tsp` file will emit two Lexicon files:
128128-129129-```
130130-- com/example/foo.json
131131-- com/example/bar.json
132132-```
110110+This emits two files: `com/example/foo.json` and `com/example/bar.json`.
133111134134-You can `import` other `.tsp` files to avoid defining the same thing multiple times. The output structure is only determined by namespaces, not by how you split your input files.
112112+You can `import` other `.tsp` files to reuse definitions. Output structure is determined solely by namespaces, not by source file organization.
135113136114## Models
137115138138-A namespace may contain [`model`s](https://typespec.io/docs/language-basics/models/) inside. By default, **every `model` turns into a Lexicon definition** (`defs` in the Lexicon output). For example, if you have a `PostView` model defined like this:
116116+By default, **every `model` becomes a Lexicon definition**:
139117140118```typescript
141119import "@typelex/emitter";
142120143121namespace app.bsky.feed.defs {
144144- model PostView {
145145- // ...
146146- }
122122+ model PostView { /* ... */ }
123123+ model ViewerState { /* ... */ }
147124}
148125```
149126150150-It will then become `postView` definition in the `defs` of this namespace's Lexicon:
127127+Model names convert PascalCase → camelCase:
151128152129```json
153130{
154154- "lexicon": 1,
155131 "id": "app.bsky.feed.defs",
156132 "defs": {
157157- "postView": {
158158- // ...
159159- }
133133+ "postView": { /* ... */ },
134134+ "viewerState": { /* ... */ }
160135 }
136136+ // ...
161137}
162138```
163139164164-A namespace may have multiple `model`s:
165165-166166-```typescript
167167-namespace app.bsky.feed.defs {
168168- model PostView {
169169- // ...
170170- }
140140+Models in the same namespace can be in separate `namespace` blocks or even different files (via [`import`](https://typespec.io/docs/language-basics/imports/)). TypeSpec bundles them all into one Lexicon file per namespace.
171141172172- model ViewerState {
173173- // ...
174174- }
175175-}
176176-```
142142+### Namespace Conventions: `.defs` vs `Main`
177143178178-Every `model` becomes an entry in `defs`:
179179-180180-```json
181181-{
182182- "lexicon": 1,
183183- "id": "app.bsky.feed.defs",
184184- "defs": {
185185- "postView": {
186186- // ...
187187- },
188188- "viewerState": {
189189- // ..
190190- }
191191- }
192192-}
193193-```
144144+By convention, a namespace must either have a `Main` model or end with `.defs`.
194145195195-This works even if they're declared in separate `namespace` blocks:
146146+Use `.defs` for a grabbag of reusable definitions:
196147197148```typescript
198198-namespace app.bsky.feed.defs {
199199- model PostView {
200200- // ...
201201- }
202202-}
149149+import "@typelex/emitter";
203150204151namespace app.bsky.feed.defs {
205205- model ViewerState {
206206- // ...
207207- }
152152+ model PostView { /* ... */ }
153153+ model ViewerState { /* ... */ }
208154}
209155```
210156211211-These blocks could even be in different files, one [`import`ing](https://typespec.io/docs/language-basics/imports/) the other.
212212-213213-In either case, TypeSpec will find all the models inside each namespace (such as `app.bsky.feed.defs`), and bundle each namespace into a separate Lexicon file (such as `app/bsky/feed/defs.json`) with all its `defs`.
214214-215215-### Namespace Conventions: `.defs` vs `Main`
216216-217217-By Lexicon convention, a namespace must either have a `Main` model or end with `.defs`.
218218-219219-You should end your namespace in `.defs` if you want a grabbag of reusable definitions:
157157+For a Lexicon about one main concept, add a `Main` model instead:
220158221159```typescript
222222-namespace app.bsky.feed.defs {
223223- model PostView {
224224- // ...
225225- }
226226-227227- model ViewerState {
228228- // ...
229229- }
230230-}
231231-```
232232-233233-On the other hand, if your Lexicon is about one main concept, don't add `.defs` to the namespace, and instead pick some model to be called `Main`:
160160+import "@typelex/emitter";
234161235235-```typescript
236162namespace app.bsky.embed.video {
237237- model Main {
238238- @required
239239- video: Blob<#["video/mp4"], 100000000>;
240240-241241- @maxItems(20)
242242- captions?: Caption[];
243243- }
244244-245245- model Caption {
246246- // ...
247247- }
163163+ model Main { /* ... */ }
164164+ model Caption { /* ... */ }
248165}
249166```
250167251251-Either do one or the other, or you'll get a compile error forcing you to choose.
168168+Pick one or the other—the compiler will error if you don't.
252169253170### References
254171255255-A `model` can reference another `model` as a data type like so:
172172+Models can reference other models:
256173257174```typescript
175175+import "@typelex/emitter";
176176+258177namespace app.bsky.embed.video {
259178 model Main {
260260- // A reference to the `Caption` model below:
261179 captions?: Caption[];
262180 }
263263-264264- model Caption {
265265- // ...
266266- }
181181+ model Caption { /* ... */ }
267182}
268183```
269184270270-In the resulting Lexicon JSON, this becomes a `ref` to the local `#caption` def:
185185+This becomes a local `ref`:
271186272187```json
273273-{
274274- "lexicon": 1,
275275- "id": "app.bsky.embed.video",
276276- "defs": {
277277- "main": {
278278- "type": "object",
279279- "properties": {
280280- "captions": {
281281- "type": "array",
282282- "items": {
283283- // A reference to the `caption` def below:
284284- "type": "ref",
285285- "ref": "#caption"
286286- }
287287- }
288288- }
289289- },
290290- "caption": {
291291- "type": "object",
292292- "properties": {}
293293- }
294294- }
188188+// ...
189189+"captions": {
190190+ "type": "array",
191191+ "items": { "type": "ref", "ref": "#caption" }
295192}
193193+// ...
296194```
297195298298-You can also reference `model`s from other namespaces:
196196+You can also reference models from other namespaces:
299197300198```typescript
301199import "@typelex/emitter";
302200303201namespace app.bsky.actor.profile {
304202 model Main {
305305- // A reference to the `SelfLabel` model from another Lexicon:
306203 labels?: (com.atproto.label.defs.SelfLabels | unknown);
307204 }
308205}
309206310207namespace com.atproto.label.defs {
311311- model SelfLabels {
312312- // ...
313313- }
208208+ model SelfLabels { /* ... */ }
314209}
315210```
316211317317-This will become a fully qualified reference:
212212+This becomes a fully qualified reference:
318213319214```json
320215// ...
···325220// ...
326221```
327222328328-This will work even if you move the `com.atproto.label.defs` definition to another `.tsp` file, as long as you don't forget to `import` that file.
223223+([See it in the Playground](https://playground.typelex.org/?c=aW1wb3J0ICJAdHlwZWxleC9lbWl0dGVyIjsKCm5hbWVzcGFjZSBhcHAuYnNreS5hY3Rvci5wcm9maWxlIHsKICBAcmVjKCJsaXRlcmFsOnNlbGYiKQogIG1vZGVsIE1haW7FJiAgZGlzcGxheU5hbWU%2FOiBzdHJpbmc7xRpsYWJlbHM%2FOiAoY29tLmF0cHJvdG8uxRYuZGVmcy5TZWxmTMUlIHwgdW5rbm93binEPH0KfewAptY%2F5QCA5gCPy0nEFSAgLy8gLi4uxko%3D&e=%40typelex%2Femitter&options=%7B%7D&vs=%7B%7D).)
329224330330-As you can see [in the Playground](https://playground.typelex.org/?c=aW1wb3J0ICJAdHlwZWxleC9lbWl0dGVyIjsKCm5hbWVzcGFjZSBhcHAuYnNreS5hY3Rvci5wcm9maWxlIHsKICBAcmVjKCJsaXRlcmFsOnNlbGYiKQogIG1vZGVsIE1haW7FJiAgZGlzcGxheU5hbWU%2FOiBzdHJpbmc7xRpsYWJlbHM%2FOiAoY29tLmF0cHJvdG8uxRYuZGVmcy5TZWxmTMUlIHwgdW5rbm93binEPH0KfewAptY%2F5QCA5gCPy0nEFSAgLy8gLi4uxko%3D&e=%40typelex%2Femitter&options=%7B%7D&vs=%7B%7D), this actually emits *two Lexicon files* since there are two separate namespaces being declared.
331331-332332-This makes sense if both definitions are yours, but what if you just want to *refer to* someone else's Lexicon?
225225+This works across files too—just remember to `import` the file with the definition.
333226334227### External Stubs
335228336336-The easiest way to use someone else's Lexicon is to import its definition, assuming it's also written in TypeSpec.
337337-338338-```typescript
339339-import "../atproto.tsp"
340340-```
341341-342342-In practice, you probably won't have all the definitions of the Lexicons you depend on written in TypeSpec. However, you can **stub out any definition you depend on**:
229229+If you don't have TypeSpec definitions for external Lexicons, you can stub them out:
343230344231```typescript
345232import "@typelex/emitter";
346233347234namespace app.bsky.actor.profile {
348235 model Main {
349349- // A reference to a stub
350236 labels?: (com.atproto.label.defs.SelfLabels | unknown);
351237 }
352238}
353239354354-// This is a stub! It's fine for it to be empty.
355355-// Think of it similarly to a .d.ts definition in TypeScript.
240240+// Empty stub (like .d.ts in TypeScript)
356241namespace com.atproto.label.defs {
357242 model SelfLabels { }
358243}
359244```
360245361361-You could place stubs together and import them from the definitions that need them, and then ignore the (incomplete) Lexicon JSON output for those stubs (since it's going to be empty).
246246+You could collect stubs in one file and import them:
362247363248```typescript
364249import "@typelex/emitter";
365365-import "../atproto-stubs.tsp"; // All your stubs here
250250+import "../atproto-stubs.tsp";
366251367252namespace app.bsky.actor.profile {
368253 model Main {
369369- // A reference to a stub
370254 labels?: (com.atproto.label.defs.SelfLabels | unknown);
371255 }
372256}
373257```
374258375375-I'm not sure which organizational patterns work best in practice. Try different things and let me know.
259259+You'll want to replace the stubbed lexicons in the output folder with their real JSON before running codegen.
376260377261### Inline Models
378262379379-By default, every `model` becomes a top-level entry in Lexicon `defs`.
380380-381381-This:
263263+By default, every `model` becomes a top-level def:
382264383265```typescript
384266import "@typelex/emitter";
···387269 model Main {
388270 captions?: Caption[];
389271 }
390390-391391- model Caption {
392392- text?: string
393393- }
272272+ model Caption { /* ... */ }
394273}
395274```
396275397397-turns into:
398398-399399-```json
400400-{
401401- "lexicon": 1,
402402- "id": "app.bsky.embed.video",
403403- "defs": {
404404- "main": {
405405- // ...
406406- },
407407- "caption": {
408408- // ...
409409- }
410410- }
411411-}
412412-```
276276+This creates two defs: `main` and `caption`.
413277414414-Sometimes, you might want to avoid exposing a model as its own `def`, and you just want it to be expanded inline. Put the `@inline` decorator on the `model` to force it into being inlined at every usage site:
278278+Use `@inline` to expand a model inline instead:
415279416280```typescript
417281import "@typelex/emitter";
···428292}
429293```
430294431431-Then it will be inlined wherever it's being used:
295295+Now `Caption` is expanded inline:
432296433297```json
434434-{
435435- "lexicon": 1,
436436- "id": "app.bsky.embed.video",
437437- "defs": {
438438- "main": {
439439- "type": "object",
440440- "properties": {
441441- "captions": {
442442- "type": "array",
443443- "items": {
444444- // That's our Caption, inlined.
445445- "type": "object",
446446- "properties": {
447447- "text": { "type": "string" }
448448- }
449449- }
450450- }
451451- }
452452- }
298298+// ...
299299+"captions": {
300300+ "type": "array",
301301+ "items": {
302302+ "type": "object",
303303+ "properties": { "text": { "type": "string" } }
453304 }
454305}
306306+// ...
455307```
456308457457-Note this means that different usages of `Caption` will have no relation to each other from the Lexicon perspective. The fact that the `Caption` abstraction exists will essentially be erased.
309309+Note that `Caption` won't exist as a separate def—the abstraction is erased in the output.
458310459311## Top-Level Lexicon Types
460312461461-In TypeSpec, definitions of things are called [Models](https://typespec.io/docs/language-basics/models/). So you'll see `model Foo { }` used for almost everything, but with different decorators that make its purpose more concrete.
313313+TypeSpec uses `model` for almost everything. Decorators specify what kind of Lexicon type it becomes.
462314463315### Objects
464316465465-If you haven't marked your `model` with any decorator, it will be a Lexicon *object*.
317317+A plain `model` becomes a Lexicon object:
466318467319```typescript
320320+import "@typelex/emitter";
321321+468322namespace com.example.post {
469469- model Main {
470470- // ...
471471- }
323323+ model Main { /* ... */ }
472324}
473325```
474326475475-becomes
327327+Output:
476328477329```json
478478-{
479479- "lexicon": 1,
480480- "id": "com.example.post",
481481- "defs": {
482482- "main": {
483483- "type": "object",
484484- "properties": {
485485- // ...
486486- }
487487- }
488488- }
330330+// ...
331331+"main": {
332332+ "type": "object",
333333+ "properties": { /* ... */ }
489334}
335335+// ...
490336```
491337492338### Records
493339494494-Mark a `model` with a `@rec` decorator to make it a Lexicon *record*.
495495-496496-For example:
340340+Use `@rec` to make a model a Lexicon record:
497341498342```typescript
499499-import "@typelex/emitter"; // Don't forget this import!
343343+import "@typelex/emitter";
500344501345namespace com.example.post {
502346 @rec("tid")
503503- model Main {
504504- // ...
505505- }
347347+ model Main { /* ... */ }
506348}
507349```
508350509509-(Note it's `@rec` and not `@record` because unfortunately "record" is reserved in TypeSpec.)
510510-511511-This becomes:
351351+Output:
512352513353```json
514514-{
515515- "lexicon": 1,
516516- "id": "com.example.post",
517517- "defs": {
518518- "main": {
519519- "type": "record",
520520- "key": "tid",
521521- "record": {
522522- "type": "object",
523523- "properties": {
524524- // ...
525525- }
526526- }
527527- }
528528- }
354354+// ...
355355+"main": {
356356+ "type": "record",
357357+ "key": "tid",
358358+ "record": { "type": "object", "properties": { /* ... */ } }
529359}
360360+// ...
530361```
531362532532-You can pass any [Record Key Type](https://atproto.com/specs/record-key) to `@rec`, like `@rec("nsid")`, `@rec("literal:self")`, etc.
363363+You can pass any [Record Key Type](https://atproto.com/specs/record-key): `@rec("tid")`, `@rec("nsid")`, `@rec("literal:self")`, etc.
364364+365365+(It's `@rec` not `@record` because "record" is reserved in TypeSpec.)
533366534367### Queries
535368536536-In TypeSpec, functions are defined with [`op`](https://typespec.io/docs/language-basics/operations/) (for "operation"). Since Lexicon distinguishes *queries* and *procedures*, you need to mark an `op` with either `@query` or `@procedure`.
537537-538538-Here is an example query:
369369+In TypeSpec, use [`op`](https://typespec.io/docs/language-basics/operations/) for functions. Mark with `@query` for queries:
539370540371```typescript
541372import "@typelex/emitter";
542373543543-// examples/com/atproto/repo/getRecord.tsp
544374namespace com.atproto.repo.getRecord {
545375 @query
546376 op main(
···555385}
556386```
557387558558-The sequential arguments to `main` are rendered as `params` in the generated JSON, while the return type becomes its `output`:
388388+Arguments become `parameters`, return type becomes `output`:
559389560390```json
561561-{
562562- "lexicon": 1,
563563- "id": "com.atproto.repo.getRecord",
564564- "defs": {
565565- "main": {
566566- "type": "query",
567567- "parameters": {
568568- "type": "params",
569569- "properties": {
570570- "repo": { /* ... */ },
571571- "collection": { /* ... */ },
572572- "rkey": { /* ... */ },
573573- "cid": { /* ... */ }
574574- },
575575- "required": ["repo", "collection", "rkey"]
391391+// ...
392392+"main": {
393393+ "type": "query",
394394+ "parameters": {
395395+ "type": "params",
396396+ "properties": {
397397+ "repo": { /* ... */ },
398398+ "collection": { /* ... */ },
399399+ // ...
400400+ },
401401+ "required": ["repo", "collection", "rkey"]
402402+ },
403403+ "output": {
404404+ "encoding": "application/json",
405405+ "schema": {
406406+ "type": "object",
407407+ "properties": {
408408+ "uri": { /* ... */ },
409409+ "cid": { /* ... */ }
576410 },
577577- "output": {
578578- "encoding": "application/json",
579579- "schema": {
580580- "type": "object",
581581- "properties": {
582582- "uri": { /* ... */ },
583583- "cid": { /* ... */ }
584584- },
585585- "required": ["uri"]
586586- }
587587- }
411411+ "required": ["uri"]
588412 }
589413 }
590414}
415415+// ...
591416```
592417593593-Note that `encoding` is assumed to be `"application/json"`. You can override it with the `@encoding` decorator:
418418+`encoding` defaults to `"application/json"`. Override with `@encoding("foo/bar")` on the `op`.
594419595595-```typescript
596596-@query
597597-@encoding("foo/bar")
598598-op main(
599599- // ...
600600-```
601601-602602-You may also declare `errors` with the `@errors` decorator like so:
420420+Declare errors with `@errors`:
603421604422```typescript
605423import "@typelex/emitter";
···607425namespace com.atproto.repo.getRecord {
608426 @query
609427 @errors(FooError, BarError)
610610- op main(
611611- // ...
612612- ): {
613613- // ...
614614- };
428428+ op main(/* ... */): { /* ... */ };
615429616430 model FooError {}
617431 model BarError {}
618432}
619433```
620434621621-If you don't like writing the output definition inline as the `main` return type, you can extract it to a separate `model`. You'll probably want to mark that model as `@inline` so it doesn't clutter the `defs`.
435435+You can extract the `{ /* ... */ }` output type to a separate `@inline` model if you prefer.
622436623437### Procedures
624438625625-Procedures are declared with `@procedure op`:
439439+Use `@procedure` for procedures. The first argument must be called `input`:
626440627441```typescript
628442import "@typelex/emitter";
629443630444namespace com.example.createRecord {
631631- /** Create a new record */
632445 @procedure
633446 op main(input: {
634447 @required text: string;
···639452}
640453```
641454642642-Note that, unlike with queries, you can't change the argument names. The first argument *must* be called `input`. It will correspond to the `input` section inside the procedure's JSON:
455455+Output:
643456644457```json
645645-{
646646- "lexicon": 1,
647647- "id": "com.example.createRecord",
648648- "defs": {
649649- "main": {
650650- "type": "procedure",
651651- "description": "Create a new record",
652652- "input": {
653653- "encoding": "application/json",
654654- "schema": {
655655- "type": "object",
656656- "properties": {
657657- "text": { "type": "string" }
658658- },
659659- "required": ["text"]
660660- }
458458+// ...
459459+"main": {
460460+ "type": "procedure",
461461+ "input": {
462462+ "encoding": "application/json",
463463+ "schema": {
464464+ "type": "object",
465465+ "properties": { "text": { "type": "string" } },
466466+ "required": ["text"]
467467+ }
468468+ },
469469+ "output": {
470470+ "encoding": "application/json",
471471+ "schema": {
472472+ "type": "object",
473473+ "properties": {
474474+ "uri": { /* ... */ },
475475+ "cid": { /* ... */ }
661476 },
662662- "output": {
663663- "encoding": "application/json",
664664- "schema": {
665665- "type": "object",
666666- "properties": {
667667- "uri": { /* ... */ },
668668- "cid": { /* ... */ }
669669- },
670670- "required": ["uri", "cid"]
671671- }
672672- }
477477+ "required": ["uri", "cid"]
673478 }
674479 }
675480}
676676-```
677677-678678-Although this is very rarely done, procedures can also receive parameters (like queries). However, they are declared inside an optional second object argument like `@procedure op(input: {}, parameters: {})`.
679679-680680-Use `: void` for procedures with no output:
681681-682682-```typescript
683683-@procedure
684684-op main(input: {
685685- @required uri: atUri;
686686-}): void;
481481+// ...
687482```
688483689689-Use `: never` with `@encoding()` for output with encoding but no schema:
484484+Procedures can also receive parameters (rarely used): `op main(input: {}, parameters: {})`.
690485691691-```typescript
692692-@query
693693-@encoding("application/json")
694694-op main(id?: string): never;
695695-```
486486+Use `: void` for no output, or `: never` with `@encoding()` on the `op` for output with encoding but no schema.
696487697488### Subscriptions
698489699699-Defining an `op` with `@subscription` gives you a subscription:
490490+Use `@subscription` for subscriptions:
700491701492```typescript
702493import "@typelex/emitter";
···704495namespace com.atproto.sync.subscribeRepos {
705496 @subscription
706497 @errors(FutureCursor, ConsumerTooSlow)
707707- op main(
708708- cursor?: integer
709709- ): Commit | Sync | unknown;
710710-711711- model Commit {
712712- // ...
713713- }
714714-715715- model Sync {
716716- // ...
717717- }
498498+ op main(cursor?: integer): Commit | Sync | unknown;
718499500500+ model Commit { /* ... */ }
501501+ model Sync { /* ... */ }
719502 model FutureCursor {}
720503 model ConsumerTooSlow {}
721504}
722505```
723506724724-This becomes:
507507+Output:
725508726509```json
727727-{
728728- "lexicon": 1,
729729- "id": "com.atproto.sync.subscribeRepos",
730730- "defs": {
731731- "main": {
732732- "type": "subscription",
733733- "parameters": {
734734- "type": "params",
735735- "properties": {
736736- "cursor": { /* ... */ }
737737- }
738738- },
739739- "message": {
740740- "schema": {
741741- "type": "union",
742742- "refs": ["#commit", "#sync"]
743743- }
744744- },
745745- "errors": [{ "name": "FutureCursor" }, { "name": "ConsumerTooSlow" }]
746746- },
747747- "commit": { /* ... */ },
748748- "sync": { /* ... */ }
749749- }
510510+// ...
511511+"main": {
512512+ "type": "subscription",
513513+ "parameters": {
514514+ "type": "params",
515515+ "properties": { "cursor": { /* ... */ } }
516516+ },
517517+ "message": {
518518+ "schema": {
519519+ "type": "union",
520520+ "refs": ["#commit", "#sync"]
521521+ }
522522+ },
523523+ "errors": [{ "name": "FutureCursor" }, { "name": "ConsumerTooSlow" }]
750524}
525525+// ...
751526```
752527753528### Tokens
754529755755-Empty models marked with `@token` become token definitions:
530530+Use `@token` for empty token models:
756531757532```typescript
758758-/** Indicates spam content */
759759-@token
760760-model ReasonSpam {}
533533+namespace com.example.moderation.defs {
534534+ @token
535535+ model ReasonSpam {}
761536762762-/** Indicates policy violation */
763763-@token
764764-model ReasonViolation {}
537537+ @token
538538+ model ReasonViolation {}
765539766766-model Report {
767767- @required reason: (ReasonSpam | ReasonViolation | unknown);
540540+ model Report {
541541+ @required reason: (ReasonSpam | ReasonViolation | unknown);
542542+ }
768543}
769544```
770545771771-**Maps to:**
546546+Output:
547547+772548```json
773773-{
774774- "report": {
775775- "properties": {
776776- "reason": {
777777- "type": "union",
778778- "refs": ["#reasonSpam", "#reasonViolation"]
779779- }
549549+// ...
550550+"reasonSpam": { "type": "token" },
551551+"reasonViolation": { "type": "token" },
552552+"report": {
553553+ "type": "object",
554554+ "properties": {
555555+ "reason": {
556556+ "type": "union",
557557+ "refs": ["#reasonSpam", "#reasonViolation"]
780558 }
781559 },
782782- "reasonSpam": {
783783- "type": "token",
784784- "description": "Indicates spam content"
785785- }
560560+ "required": ["reason"]
786561}
562562+// ...
787563```
788564789565## Data Types
···824600Use `[]` suffix:
825601826602```typescript
827827-model Main {
828828- /** Array of strings */
829829- stringArray?: string[];
603603+import "@typelex/emitter";
830604831831- /** Array with size constraints */
832832- @minItems(1)
833833- @maxItems(10)
834834- limitedArray?: integer[];
605605+namespace com.example.arrays {
606606+ model Main {
607607+ stringArray?: string[];
835608836836- /** Array of references */
837837- items?: Item[];
609609+ @minItems(1)
610610+ @maxItems(10)
611611+ limitedArray?: integer[];
838612839839- /** Array of union types */
840840- mixed?: (TypeA | TypeB | unknown)[];
613613+ items?: Item[];
614614+ mixed?: (TypeA | TypeB | unknown)[];
615615+ }
616616+ // ...
841617}
842618```
843619844844-**Maps to:** `{"type": "array", "items": {...}}`
620620+Output: `{ "type": "array", "items": {...} }`.
845621846846-**Note:** `@minItems`/`@maxItems` in TypeSpec map to `minLength`/`maxLength` in JSON.
622622+Note: `@minItems`/`@maxItems` map to `minLength`/`maxLength` in JSON.
847623848624### Blobs
849625850626```typescript
851851-model Main {
852852- /** Basic blob */
853853- file?: Blob;
627627+import "@typelex/emitter";
854628855855- /** Image up to 5MB */
856856- image?: Blob<#["image/*"], 5000000>;
857857-858858- /** Specific types up to 2MB */
859859- photo?: Blob<#["image/png", "image/jpeg"], 2000000>;
629629+namespace com.example.blobs {
630630+ model Main {
631631+ file?: Blob;
632632+ image?: Blob<#["image/*"], 5000000>;
633633+ photo?: Blob<#["image/png", "image/jpeg"], 2000000>;
634634+ }
860635}
861636```
862637863863-**Maps to:**
638638+Output:
639639+864640```json
865865-{
866866- "file": {"type": "blob"},
867867- "image": {
868868- "type": "blob",
869869- "accept": ["image/*"],
870870- "maxSize": 5000000
871871- }
641641+// ...
642642+"image": {
643643+ "type": "blob",
644644+ "accept": ["image/*"],
645645+ "maxSize": 5000000
872646}
647647+// ...
873648```
874649875650## Required and Optional Fields
876651877877-In Lexicon, the default is to make fields optional. Use `?:` for that:
652652+In Lexicon, fields are optional by default. Use `?:`:
878653879654```typescript
880655import "@typelex/emitter";
881656882657namespace tools.ozone.moderation.defs {
883658 model SubjectStatusView {
884884- subjectBlobCids?: cid[];
885659 subjectRepoHandle?: string;
886660 }
887661}
888662```
889663890890-This becomes:
664664+**Think thrice before adding required fields**—you can't make them optional later.
891665892892-```json
893893-{
894894- "lexicon": 1,
895895- "id": "tools.ozone.moderation.defs",
896896- "defs": {
897897- "subjectStatusView": {
898898- "type": "object",
899899- "properties": {
900900- "subjectBlobCids": {
901901- "type": "array",
902902- "items": { "type": "string", "format": "cid" }
903903- },
904904- "subjectRepoHandle": { "type": "string" }
905905- }
906906- }
907907- }
908908-}
909909-```
910910-911911-Think twice before adding a required field because you won't be able to make it optional later without violating the schema contract (and making all existing records invalid). In practice, this often means you'd have to deprecate the field and create another field, which is messy.
912912-913913-This is why required fields need a special `@required` decorator forcing you to think twice.
666666+This is why `@required` is explicit:
914667915668```typescript
916669import "@typelex/emitter";
917670918671namespace tools.ozone.moderation.defs {
919672 model SubjectStatusView {
920920- subjectBlobCids?: cid[];
921673 subjectRepoHandle?: string;
922922-923674 @required createdAt: datetime;
924675 }
925676}
926677```
927678928928-This becomes:
679679+Output:
929680930681```json
931682// ...
932683"required": ["createdAt"]
684684+// ...
933685```
934686935687## Unions
936688937689### Open Unions (Recommended)
938690939939-In Lexicon, unions like "A or B" default to being *open*, i.e. allowing you to add C in the future.
940940-941941-To declare an open union, write `| unknown` at the end, like `Images | Video | unknown`:
691691+Unions default to being *open*—allowing you to add more options later. Write `| unknown`:
942692943693```typescript
944694import "@typelex/emitter";
···948698 embed?: Images | Video | unknown;
949699 }
950700951951- model Images {
952952- // ...
953953- }
954954-955955- model Video {
956956- // ...
957957- }
701701+ model Images { /* ... */ }
702702+ model Video { /* ... */ }
958703}
959704```
960705961961-This produces a `union`:
706706+Output:
962707963708```json
964964-{
965965- "lexicon": 1,
966966- "id": "app.bsky.feed.post",
967967- "defs": {
968968- "main": {
969969- "type": "object",
970970- "properties": {
971971- "embed": {
972972- "type": "union",
973973- "refs": ["#images", "#video"]
974974- }
975975- }
976976- },
977977- "images": { /* ... */ },
978978- "video": { /* ... */ }
979979- }
709709+// ...
710710+"embed": {
711711+ "type": "union",
712712+ "refs": ["#images", "#video"]
980713}
714714+// ...
981715```
982716983983-Then later you can add more types to the union.
984984-985985-You may also write the same thing with the `union` syntax instead of writing `A | B | C | unknown` directly:
717717+You can also use the `union` syntax to give it a name:
986718987719```typescript
720720+import "@typelex/emitter";
721721+988722namespace app.bsky.feed.post {
989723 model Main {
990724 embed?: EmbedType;
···992726993727 @inline union EmbedType { Images, Video, unknown }
994728995995- model Images {
996996- // ...
997997- }
998998-999999- model Video {
10001000- // ...
10011001- }
729729+ model Images { /* ... */ }
730730+ model Video { /* ... */ }
1002731}
1003732```
100473310051005-This is completely equivalent. The `@inline` decorator states that it doesn't become a part of your defs.
734734+The `@inline` prevents it from becoming a separate def in the output.
10067351007736### Known Values (Open Enums)
100873710091009-You can suggest common values but allow others:
738738+Suggest common values but allow others with `| string`:
10107391011740```typescript
1012741import "@typelex/emitter";
···1018747}
1019748```
102074910211021-Note the `| string` that says you may add more "known" values later.
10221022-10231023-The `union` syntax also works for this:
750750+The `union` syntax works here too:
10247511025752```typescript
1026753import "@typelex/emitter";
···1034761}
1035762```
103676310371037-Here, `@inline` prevents creation of a `languages` top-level def. If you *do* want to make it reusable from other Lexicons, remove the `@inline` annotation and refer to `com.example.Languages` from somewhere else.
764764+You can remove `@inline` to make it a reusable `def` accessible from other Lexicons.
10387651039766### Closed Unions and Enums (Discouraged)
104076710411041-If you want to create a *closed* union (which are **heavily discouraged** in Lexicon), you can mark your union with a `@closed` decorator. This will let you remove the `unknown` case from it.
768768+**Heavily discouraged** in Lexicon.
104276910431043-There is no shorthand notation for closed unions, so you'll have to write `@closed @inline union { A, B }`.
770770+Marking a `union` as `@closed` lets you remove `unknown` from the list of options:
10447711045772```typescript
1046773import "@typelex/emitter";
10477741048775namespace com.atproto.repo.applyWrites {
10491049- @procedure
10501050- op main(input: {
10511051- @required
10521052- writes: WriteAction[];
10531053- }): {
10541054- // ...
10551055- };
776776+ model Main {
777777+ @required writes: WriteAction[];
778778+ }
10567791057780 @closed // Discouraged!
1058781 @inline
10591059- union WriteAction { Create, Update, Delete, }
782782+ union WriteAction { Create, Update, Delete }
106078310611061- model Create {
10621062- // ...
10631063- }
10641064-10651065- model Update {
10661066- // ...
10671067- }
10681068-10691069- model Delete {
10701070- // ...
10711071- }
784784+ model Create { /* ... */ }
785785+ model Update { /* ... */ }
786786+ model Delete { /* ... */ }
1072787}
1073788```
107478910751075-This gives you:
790790+Output:
10767911077792```json
793793+// ...
1078794"writes": {
1079795 "type": "array",
1080796 "items": {
···1083799 "closed": true
1084800 }
1085801}
802802+// ...
1086803```
108780410881088-You can also declare this with strings or numbers:
805805+With strings or numbers, this becomes a closed `enum`:
10898061090807```typescript
10911091-@closed // Discouraged!
10921092-@inline
10931093-union WriteAction { "create", "update", "delete" }
808808+import "@typelex/emitter";
809809+810810+namespace com.atproto.repo.applyWrites {
811811+ model Main {
812812+ @required action: WriteAction;
813813+ }
814814+815815+ @closed // Discouraged!
816816+ @inline
817817+ union WriteAction { "create", "update", "delete" }
818818+}
1094819```
109582010961096-These would become a Lexicon closed `enum`:
821821+Output:
10978221098823```json
10991099-"items": {
11001100- "type": "string",
11011101- "enum": ["create", "update", "delete"]
11021102-}
824824+// ...
825825+"type": "string",
826826+"enum": ["create", "update", "delete"]
827827+// ...
1103828```
110482911051105-Avoid closed unions and closed enums if you can.
830830+Avoid closed unions/enums when possible.
11068311107832## Constraints
110883311091109-### String Constraints
834834+### Strings
11108351111836```typescript
11121112-model Main {
11131113- /** Byte length constraints */
11141114- @minLength(1)
11151115- @maxLength(100)
11161116- text?: string;
837837+import "@typelex/emitter";
111783811181118- /** Grapheme cluster length constraints */
11191119- @minGraphemes(1)
11201120- @maxGraphemes(50)
11211121- displayName?: string;
839839+namespace com.example {
840840+ model Main {
841841+ @minLength(1)
842842+ @maxLength(100)
843843+ text?: string;
844844+845845+ @minGraphemes(1)
846846+ @maxGraphemes(50)
847847+ displayName?: string;
848848+ }
1122849}
1123850```
112485111251125-**Maps to:** `minLength`/`maxLength`, `minGraphemes`/`maxGraphemes`
852852+Maps to: `minLength`/`maxLength`, `minGraphemes`/`maxGraphemes`
112685311271127-### Integer Constraints
854854+### Integers
11288551129856```typescript
11301130-model Main {
11311131- @minValue(1)
11321132- @maxValue(100)
11331133- score?: integer;
857857+import "@typelex/emitter";
858858+859859+namespace com.example {
860860+ model Main {
861861+ @minValue(1)
862862+ @maxValue(100)
863863+ score?: integer;
864864+ }
1134865}
1135866```
113686711371137-**Maps to:** `minimum`/`maximum`
868868+Maps to: `minimum`/`maximum`
113886911391139-### Bytes Constraints
870870+### Bytes
11408711141872```typescript
11421142-model Main {
11431143- @minBytes(1)
11441144- @maxBytes(1024)
11451145- data?: bytes;
873873+import "@typelex/emitter";
874874+875875+namespace com.example {
876876+ model Main {
877877+ @minBytes(1)
878878+ @maxBytes(1024)
879879+ data?: bytes;
880880+ }
1146881}
1147882```
114888311491149-**Maps to:** `minLength`/`maxLength`
884884+Maps to: `minLength`/`maxLength`
115088511511151-**Note:** Use `@minBytes`/`@maxBytes` in TypeSpec, but they map to `minLength`/`maxLength` in JSON.
11521152-11531153-### Array Constraints
886886+### Arrays
11548871155888```typescript
11561156-model Main {
11571157- @minItems(1)
11581158- @maxItems(10)
11591159- items?: string[];
889889+import "@typelex/emitter";
890890+891891+namespace com.example {
892892+ model Main {
893893+ @minItems(1)
894894+ @maxItems(10)
895895+ items?: string[];
896896+ }
1160897}
1161898```
116289911631163-**Maps to:** `minLength`/`maxLength`
900900+Maps to: `minLength`/`maxLength`
116490111651165-**Note:** Use `@minItems`/`@maxItems` in TypeSpec, but they map to `minLength`/`maxLength` in JSON.
11661166-11671167-## Default and Constant Values
902902+## Defaults and Constants
11689031169904### Defaults
11709051171906```typescript
11721172-model Main {
11731173- version?: integer = 1;
11741174- lang?: string = "en";
907907+import "@typelex/emitter";
908908+909909+namespace com.example {
910910+ model Main {
911911+ version?: integer = 1;
912912+ lang?: string = "en";
913913+ }
1175914}
1176915```
117791611781178-**Maps to:** `{"default": 1}`, `{"default": "en"}`
917917+Maps to: `{"default": 1}`, `{"default": "en"}`
11799181180919### Constants
118192011821182-Use `@readOnly` with default value:
921921+Use `@readOnly` with a default:
11839221184923```typescript
11851185-model Main {
11861186- @readOnly status?: string = "active";
924924+import "@typelex/emitter";
925925+926926+namespace com.example {
927927+ model Main {
928928+ @readOnly status?: string = "active";
929929+ }
1187930}
1188931```
118993211901190-**Maps to:** `{"const": "active"}`
933933+Maps to: `{"const": "active"}`
11919341192935## Nullable Fields
11939361194937Use `| null` for nullable fields:
11959381196939```typescript
11971197-model Main {
11981198- @required createdAt: datetime;
11991199- updatedAt?: datetime | null; // can be omitted or null
12001200- deletedAt?: datetime; // can only be omitted
12011201-}
12021202-```
940940+import "@typelex/emitter";
120394112041204-**Maps to:**
12051205-```json
12061206-{
12071207- "required": ["createdAt"],
12081208- "nullable": ["updatedAt"],
12091209- "properties": { ... }
942942+namespace com.example {
943943+ model Main {
944944+ @required createdAt: datetime;
945945+ updatedAt?: datetime | null; // can be omitted or null
946946+ deletedAt?: datetime; // can only be omitted
947947+ }
1210948}
1211949```
121295012131213-## Naming Conventions
951951+Output:
121495212151215-Model names convert from PascalCase to camelCase in defs:
12161216-12171217-```typescript
12181218-model StatusEnum { ... } // becomes "statusEnum"
12191219-model UserMetadata { ... } // becomes "userMetadata"
12201220-model Main { ... } // becomes "main"
953953+```json
954954+// ...
955955+"required": ["createdAt"],
956956+"nullable": ["updatedAt"]
957957+// ...
1221958```