cerulea/lexicon

define atproto schemas in TypeScript

git clone https://git.t4t.associates/cerulea/lexicon

Charlotte Somrecord the line width and add a check task1831627

main
1.7 KiB58 linesraw

@cerulea/lexicon

define AT Protocol lexicon schemas in TypeScript

  • no deps :D
  • runtime validation via schema.validate(value)
    • returns { success: true, value } or { success: false, issues } with a path per issue
  • emit lexicon JSON documents via toLexicons(...roots)
    • easily generate a lexicons/ dir of json from typescript defs; it's like a three-liner
  • simple record builder via schema.build(...)
    • will auto-fill $type for records
    • will expand short #fragment strings for unions

usage

import * as l from "@cerulea/lexicon";

const facetRange = { start: l.integer, end: l.integer };
const Facet = l.union("example.myapp.facet", {
  "#mention": l.object({ ...facetRange, did: l.did }),
  "#link": l.object({ ...facetRange, uri: l.uri }),
});

const Post = l.record("example.myapp.post", { key: "tid" }, {
  text: l.stringWith({ maxGraphemes: 300 }),
  facets: l.optional(l.array(Facet)),
  createdAt: l.datetime,
});
type Post = l.Infer<typeof Post>;

const getPost = l.query("example.myapp.getPost", {
  parameters: l.params({ uri: l.atUri }),
  output: Post,
  errors: [{ name: "NotFound" }],
});

const post: Post = Post.build({
  text: "hi @alice",
  facets: [{ $type: "#mention", start: 3, end: 9, did: "did:example:alice" }],
  createdAt: new Date().toISOString(),
});

const untrusted: unknown = {
  $type: "example.myapp.post",
  text: "hello",
  createdAt: "my evil invalid date",
};
const result = Post.validate(untrusted);
if (!result.success) console.error(result.issues);

const lexicons = l.toLexicons(Post, getPost);
/* [
  { lexicon: 1, id: "example.myapp.facet", … },
  { lexicon: 1, id: "example.myapp.getPost", … },
  …
] */
1# @cerulea/lexicon
2
3define AT Protocol lexicon schemas in TypeScript
4
5- no deps :D
6- runtime validation via `schema.validate(value)`
7  - returns `{ success: true, value }` or `{ success: false, issues }` with a path per issue
8- emit lexicon JSON documents via `toLexicons(...roots)`
9  - easily generate a `lexicons/` dir of json from typescript defs; it's like a three-liner
10- simple record builder via `schema.build(...)`
11  - will auto-fill `$type` for records
12  - will expand short `#fragment` strings for unions
13
14## usage
15
16```typescript
17import * as l from "@cerulea/lexicon";
18
19const facetRange = { start: l.integer, end: l.integer };
20const Facet = l.union("example.myapp.facet", {
21  "#mention": l.object({ ...facetRange, did: l.did }),
22  "#link": l.object({ ...facetRange, uri: l.uri }),
23});
24
25const Post = l.record("example.myapp.post", { key: "tid" }, {
26  text: l.stringWith({ maxGraphemes: 300 }),
27  facets: l.optional(l.array(Facet)),
28  createdAt: l.datetime,
29});
30type Post = l.Infer<typeof Post>;
31
32const getPost = l.query("example.myapp.getPost", {
33  parameters: l.params({ uri: l.atUri }),
34  output: Post,
35  errors: [{ name: "NotFound" }],
36});
37
38const post: Post = Post.build({
39  text: "hi @alice",
40  facets: [{ $type: "#mention", start: 3, end: 9, did: "did:example:alice" }],
41  createdAt: new Date().toISOString(),
42});
43
44const untrusted: unknown = {
45  $type: "example.myapp.post",
46  text: "hello",
47  createdAt: "my evil invalid date",
48};
49const result = Post.validate(untrusted);
50if (!result.success) console.error(result.issues);
51
52const lexicons = l.toLexicons(Post, getPost);
53/* [
54  { lexicon: 1, id: "example.myapp.facet", … },
55  { lexicon: 1, id: "example.myapp.getPost", … },
56  …
57] */
58```