import * as v from '@badrap/valita'; const integerType = v .number() .assert((v) => Number.isInteger(v) && v >= 0, 'Number is expected to be a positive integer'); export const booleanSchema = v.object({ type: v.literal('boolean'), description: v.string().optional(), default: v.boolean().optional(), const: v.boolean().optional(), }); export type BooleanSchema = v.Infer; export const integerSchema = v.object({ type: v.literal('integer'), description: v.string().optional(), default: integerType.optional(), const: integerType.optional(), enum: v.array(v.number()).optional(), maximum: integerType.optional(), minimum: integerType.optional(), }); export type IntegerSchema = v.Infer; export const stringSchema = v .object({ type: v.literal('string'), description: v.string().optional(), format: v .union( v.literal('at-identifier'), v.literal('at-uri'), v.literal('cid'), v.literal('datetime'), v.literal('did'), v.literal('handle'), v.literal('language'), v.literal('nsid'), v.literal('record-key'), v.literal('tid'), v.literal('uri'), ) .optional(), default: v.string().optional(), const: v.string().optional(), enum: v.array(v.string()).optional(), knownValues: v.array(v.string()).optional(), maxLength: integerType.optional(), minLength: integerType.optional(), maxGraphemes: integerType.optional(), minGraphemes: integerType.optional(), }) .chain((obj) => { const format = obj.format; if (format !== undefined && format !== 'uri') { if (obj.maxLength !== undefined) { return v.err(`${format} format can't be used with maxLength`); } if (obj.minLength !== undefined) { return v.err(`${format} format can't be used with minLength`); } if (obj.maxGraphemes !== undefined) { return v.err(`${format} format can't be used with maxGraphemes`); } if (obj.minGraphemes !== undefined) { return v.err(`${format} format can't be used with minGraphemes`); } } return v.ok(obj); }); export type StringSchema = v.Infer; export const unknownSchema = v.object({ type: v.literal('unknown'), description: v.string().optional(), }); export type UnknownSchema = v.Infer; export const primitiveSchema = v.union(booleanSchema, integerSchema, stringSchema, unknownSchema); export type PrimitiveSchema = v.Infer; export const bytesSchema = v.object({ type: v.literal('bytes'), description: v.string().optional(), maxLength: integerType.optional(), minLength: integerType.optional(), }); export type BytesSchema = v.Infer; export const cidLinkSchema = v.object({ type: v.literal('cid-link'), description: v.string().optional(), }); export type CidLinkSchema = v.Infer; export const ipldTypeSchema = v.union(bytesSchema, cidLinkSchema); export type IpldTypeSchema = v.Infer; export const refSchema = v.object({ type: v.literal('ref'), description: v.string().optional(), ref: v.string(), }); export type RefSchema = v.Infer; export const refUnionSchema = v .object({ type: v.literal('union'), description: v.string().optional(), refs: v.array(v.string()), closed: v.boolean().optional(() => false), }) .assert((v) => !v.closed || v.refs.length > 0, `A closed union can't have empty refs list`); export type RefUnionSchema = v.Infer; export const refVariantSchema = v.union(refSchema, refUnionSchema); export type RefVariantSchema = v.Infer; export const blobSchema = v.object({ type: v.literal('blob'), description: v.string().optional(), accept: v.array(v.string()).optional(), maxSize: integerType.optional(), }); export type BlobSchema = v.Infer; export const arraySchema = v.object({ type: v.literal('array'), description: v.string().optional(), items: v.union(primitiveSchema, ipldTypeSchema, blobSchema, refVariantSchema), maxLength: integerType.optional(), minLength: integerType.optional(), }); export type ArraySchema = v.Infer; export const primitiveArraySchema = arraySchema.extend({ items: primitiveSchema, }); export type PrimitiveArraySchema = v.Infer; export const tokenSchema = v.object({ type: v.literal('token'), description: v.string().optional(), }); export type TokenSchema = v.Infer; const refineRequiredProperties = }>( obj: T, ): v.ValitaResult => { for (const field of obj.required) { if (obj.properties[field] === undefined) { return v.err(`Required field "${field}" not defined`); } } return v.ok(obj); }; export const objectSchema = v .object({ type: v.literal('object'), description: v.string().optional(), required: v.array(v.string()).optional(() => []), nullable: v.array(v.string()).optional(() => []), properties: v.record(v.union(refVariantSchema, ipldTypeSchema, arraySchema, blobSchema, primitiveSchema)), }) .chain(refineRequiredProperties); export type ObjectSchema = v.Infer; export const xrpcParametersSchema = v .object({ type: v.literal('params'), description: v.string().optional(), required: v.array(v.string()).optional(() => []), properties: v.record(v.union(primitiveSchema, primitiveArraySchema)), }) .chain(refineRequiredProperties); export type XrpcParametersSchema = v.Infer; export const xrpcBodySchema = v.object({ description: v.string().optional(), encoding: v.string(), schema: v.union(refVariantSchema, objectSchema).optional(), }); export type XrpcBodySchema = v.Infer; export const xrpcSubscriptionMessageSchema = v.object({ description: v.string().optional(), schema: v.union(refVariantSchema, objectSchema).optional(), }); export type XrpcSubscriptionMessageSchema = v.Infer; export const xrpcErrorSchema = v.object({ name: v.string(), description: v.string().optional(), }); export type XrpcErrorSchema = v.Infer; export const xrpcQuerySchema = v.object({ type: v.literal('query'), description: v.string().optional(), parameters: xrpcParametersSchema.optional(), output: xrpcBodySchema.optional(), errors: v.array(xrpcErrorSchema).optional(), }); export type XrpcQuerySchema = v.Infer; export const xrpcProcedureSchema = v.object({ type: v.literal('procedure'), description: v.string().optional(), parameters: xrpcParametersSchema.optional(), input: xrpcBodySchema.optional(), output: xrpcBodySchema.optional(), errors: v.array(xrpcErrorSchema).optional(), }); export type XrpcProcedureSchema = v.Infer; export const xrpcSubscriptionSchema = v.object({ type: v.literal('subscription'), description: v.string().optional(), parameters: xrpcParametersSchema.optional(), message: xrpcSubscriptionMessageSchema.optional(), errors: v.array(xrpcErrorSchema).optional(), }); export type XrpcSubscriptionSchema = v.Infer; export const recordSchema = v.object({ type: v.literal('record'), description: v.string().optional(), key: v.string().optional(), record: objectSchema, }); export type RecordSchema = v.Infer; export const userTypeSchema = v.union( recordSchema, xrpcQuerySchema, xrpcProcedureSchema, xrpcSubscriptionSchema, blobSchema, arraySchema, tokenSchema, objectSchema, booleanSchema, integerSchema, stringSchema, bytesSchema, cidLinkSchema, unknownSchema, ); export type UserTypeSchema = v.Infer; const NSID_RE = /^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z]{0,61}[a-zA-Z])?)$/; const nsidType = v.string().assert((v) => NSID_RE.test(v), `string doesn't match nsid format`); export const documentSchema = v .object({ lexicon: v.literal(1), id: nsidType, revision: v.number().optional(), description: v.string().optional(), defs: v.record(userTypeSchema), }) .chain((doc) => { const defs = doc.defs; for (const id in defs) { const def = defs[id]; const type = def.type; if ( id !== 'main' && (type === 'record' || type === 'query' || type === 'procedure' || type === 'subscription') ) { return v.err({ message: `${type} must be the \`main\` definition`, path: ['defs', id] }); } } return v.ok(doc); }); export type DocumentSchema = v.Infer;