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