cerulea/lexicon

define atproto schemas in TypeScript

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

Charlotte Somschema: split union() into its three ways of naming variants62f5f17

main
2.7 KiB72 linesraw
1import { object, type ObjectSchema, type Schema, type Shape } from "./schema.ts";
2import { isNsid } from "./util/syntax.ts";
3
4export type ParamsSchema<S extends Shape = Shape> = Omit<ObjectSchema<S>, "type"> & {
5  readonly type: "params";
6};
7
8export function params<const S extends Shape>(properties: S): ParamsSchema<S> {
9  for (const [key, field] of Object.entries(properties)) {
10    const inner = field.type === "optional" ? field.inner : field;
11    const value = inner.type === "array" ? inner.items : inner;
12    if (value.type !== "string" && value.type !== "integer" && value.type !== "boolean") {
13      throw new Error(`Parameter ${key} must be a string, integer, boolean, or array of those`);
14    }
15  }
16  const base = object(properties);
17  return Object.setPrototypeOf({ ...base, type: "params" }, Object.getPrototypeOf(base));
18}
19
20export type RpcError = {
21  readonly name: string;
22  readonly description?: string;
23};
24export type QueryOptions = {
25  readonly description?: string;
26  readonly parameters?: ParamsSchema;
27  readonly output?: Schema;
28  readonly errors?: readonly RpcError[];
29};
30export type ProcedureOptions = QueryOptions & { readonly input?: Schema };
31export type Rpc =
32  & { readonly id: string }
33  & (
34    | ({ readonly type: "query" } & QueryOptions)
35    | ({ readonly type: "procedure" } & ProcedureOptions)
36  );
37
38function checkRpc(id: string, options: ProcedureOptions): void {
39  if (!isNsid(id)) throw new Error(`Invalid RPC NSID: ${id}`);
40  for (const body of [options.input, options.output]) {
41    if (body === undefined) continue;
42    const schema = body.type === "ref" ? body.target : body;
43    if (schema.type !== "object" && schema.type !== "union" && schema.type !== "record") {
44      throw new Error("JSON RPC bodies must be objects, unions, or references to objects/records");
45    }
46  }
47  const names = new Set<string>();
48  for (const error of options.errors ?? []) {
49    if (!/^[A-Za-z][A-Za-z0-9]*$/.test(error.name)) {
50      throw new Error(`Invalid RPC error: ${error.name}`);
51    }
52    if (names.has(error.name)) throw new Error(`Duplicate RPC error: ${error.name}`);
53    names.add(error.name);
54  }
55}
56
57export function query<const Id extends string, const O extends QueryOptions>(
58  id: Id,
59  options: O & { readonly input?: never },
60): O & { readonly type: "query"; readonly id: Id } {
61  if ("input" in options) throw new Error("Queries cannot have input bodies");
62  checkRpc(id, options);
63  return { ...options, type: "query", id };
64}
65
66export function procedure<const Id extends string, const O extends ProcedureOptions>(
67  id: Id,
68  options: O,
69): O & { readonly type: "procedure"; readonly id: Id } {
70  checkRpc(id, options);
71  return { ...options, type: "procedure", id };
72}