UNPKG

@atproto/lexicon

Version:

atproto Lexicon schema language library

477 lines (423 loc) 11.4 kB
import { z } from 'zod' import { NSID } from '@atproto/syntax' import { requiredPropertiesRefinement } from './util' // primitives // = export const lexBoolean = z .object({ type: z.literal('boolean'), description: z.string().optional(), default: z.boolean().optional(), const: z.boolean().optional(), }) .strict() export type LexBoolean = z.infer<typeof lexBoolean> export const lexInteger = z .object({ type: z.literal('integer'), description: z.string().optional(), default: z.number().int().optional(), minimum: z.number().int().optional(), maximum: z.number().int().optional(), enum: z.number().int().array().optional(), const: z.number().int().optional(), }) .strict() export type LexInteger = z.infer<typeof lexInteger> export const lexStringFormat = z.enum([ 'datetime', 'uri', 'at-uri', 'did', 'handle', 'at-identifier', 'nsid', 'cid', 'language', 'tid', 'record-key', ]) export type LexStringFormat = z.infer<typeof lexStringFormat> export const lexString = z .object({ type: z.literal('string'), format: lexStringFormat.optional(), description: z.string().optional(), default: z.string().optional(), minLength: z.number().int().optional(), maxLength: z.number().int().optional(), minGraphemes: z.number().int().optional(), maxGraphemes: z.number().int().optional(), enum: z.string().array().optional(), const: z.string().optional(), knownValues: z.string().array().optional(), }) .strict() export type LexString = z.infer<typeof lexString> export const lexUnknown = z .object({ type: z.literal('unknown'), description: z.string().optional(), }) .strict() export type LexUnknown = z.infer<typeof lexUnknown> export const lexPrimitive = z.discriminatedUnion('type', [ lexBoolean, lexInteger, lexString, lexUnknown, ]) export type LexPrimitive = z.infer<typeof lexPrimitive> // ipld types // = export const lexBytes = z .object({ type: z.literal('bytes'), description: z.string().optional(), maxLength: z.number().optional(), minLength: z.number().optional(), }) .strict() export type LexBytes = z.infer<typeof lexBytes> export const lexCidLink = z .object({ type: z.literal('cid-link'), description: z.string().optional(), }) .strict() export type LexCidLink = z.infer<typeof lexCidLink> export const lexIpldType = z.discriminatedUnion('type', [lexBytes, lexCidLink]) export type LexIpldType = z.infer<typeof lexIpldType> // references // = export const lexRef = z .object({ type: z.literal('ref'), description: z.string().optional(), ref: z.string(), }) .strict() export type LexRef = z.infer<typeof lexRef> export const lexRefUnion = z .object({ type: z.literal('union'), description: z.string().optional(), refs: z.string().array(), closed: z.boolean().optional(), }) .strict() export type LexRefUnion = z.infer<typeof lexRefUnion> export const lexRefVariant = z.discriminatedUnion('type', [lexRef, lexRefUnion]) export type LexRefVariant = z.infer<typeof lexRefVariant> // blobs // = export const lexBlob = z .object({ type: z.literal('blob'), description: z.string().optional(), accept: z.string().array().optional(), maxSize: z.number().optional(), }) .strict() export type LexBlob = z.infer<typeof lexBlob> // complex types // = export const lexArray = z .object({ type: z.literal('array'), description: z.string().optional(), items: z.discriminatedUnion('type', [ // lexPrimitive lexBoolean, lexInteger, lexString, lexUnknown, // lexIpldType lexBytes, lexCidLink, // lexRefVariant lexRef, lexRefUnion, // other lexBlob, ]), minLength: z.number().int().optional(), maxLength: z.number().int().optional(), }) .strict() export type LexArray = z.infer<typeof lexArray> export const lexPrimitiveArray = lexArray.merge( z .object({ items: lexPrimitive, }) .strict(), ) export type LexPrimitiveArray = z.infer<typeof lexPrimitiveArray> export const lexToken = z .object({ type: z.literal('token'), description: z.string().optional(), }) .strict() export type LexToken = z.infer<typeof lexToken> export const lexObject = z .object({ type: z.literal('object'), description: z.string().optional(), required: z.string().array().optional(), nullable: z.string().array().optional(), properties: z.record( z.discriminatedUnion('type', [ lexArray, // lexPrimitive lexBoolean, lexInteger, lexString, lexUnknown, // lexIpldType lexBytes, lexCidLink, // lexRefVariant lexRef, lexRefUnion, // other lexBlob, ]), ), }) .strict() .superRefine(requiredPropertiesRefinement) export type LexObject = z.infer<typeof lexObject> // xrpc // = export const lexXrpcParameters = z .object({ type: z.literal('params'), description: z.string().optional(), required: z.string().array().optional(), properties: z.record( z.discriminatedUnion('type', [ lexPrimitiveArray, // lexPrimitive lexBoolean, lexInteger, lexString, lexUnknown, ]), ), }) .strict() .superRefine(requiredPropertiesRefinement) export type LexXrpcParameters = z.infer<typeof lexXrpcParameters> export const lexXrpcBody = z .object({ description: z.string().optional(), encoding: z.string(), // @NOTE using discriminatedUnion with a refined schema requires zod >= 4 schema: z.union([lexRefVariant, lexObject]).optional(), }) .strict() export type LexXrpcBody = z.infer<typeof lexXrpcBody> export const lexXrpcSubscriptionMessage = z .object({ description: z.string().optional(), // @NOTE using discriminatedUnion with a refined schema requires zod >= 4 schema: z.union([lexRefVariant, lexObject]).optional(), }) .strict() export type LexXrpcSubscriptionMessage = z.infer< typeof lexXrpcSubscriptionMessage > export const lexXrpcError = z .object({ name: z.string(), description: z.string().optional(), }) .strict() export type LexXrpcError = z.infer<typeof lexXrpcError> export const lexXrpcQuery = z .object({ type: z.literal('query'), description: z.string().optional(), parameters: lexXrpcParameters.optional(), output: lexXrpcBody.optional(), errors: lexXrpcError.array().optional(), }) .strict() export type LexXrpcQuery = z.infer<typeof lexXrpcQuery> export const lexXrpcProcedure = z .object({ type: z.literal('procedure'), description: z.string().optional(), parameters: lexXrpcParameters.optional(), input: lexXrpcBody.optional(), output: lexXrpcBody.optional(), errors: lexXrpcError.array().optional(), }) .strict() export type LexXrpcProcedure = z.infer<typeof lexXrpcProcedure> export const lexXrpcSubscription = z .object({ type: z.literal('subscription'), description: z.string().optional(), parameters: lexXrpcParameters.optional(), message: lexXrpcSubscriptionMessage.optional(), errors: lexXrpcError.array().optional(), }) .strict() export type LexXrpcSubscription = z.infer<typeof lexXrpcSubscription> // database // = export const lexRecord = z .object({ type: z.literal('record'), description: z.string().optional(), key: z.string().optional(), record: lexObject, }) .strict() export type LexRecord = z.infer<typeof lexRecord> // core // = // We need to use `z.custom` here because // lexXrpcProperty and lexObject are refined // `z.union` would work, but it's too slow // see #915 for details export const lexUserType = z.custom< | LexRecord | LexXrpcQuery | LexXrpcProcedure | LexXrpcSubscription | LexBlob | LexArray | LexToken | LexObject | LexBoolean | LexInteger | LexString | LexBytes | LexCidLink | LexUnknown >( (val) => { if (!val || typeof val !== 'object') { return } if (val['type'] === undefined) { return } switch (val['type']) { case 'record': return lexRecord.parse(val) case 'query': return lexXrpcQuery.parse(val) case 'procedure': return lexXrpcProcedure.parse(val) case 'subscription': return lexXrpcSubscription.parse(val) case 'blob': return lexBlob.parse(val) case 'array': return lexArray.parse(val) case 'token': return lexToken.parse(val) case 'object': return lexObject.parse(val) case 'boolean': return lexBoolean.parse(val) case 'integer': return lexInteger.parse(val) case 'string': return lexString.parse(val) case 'bytes': return lexBytes.parse(val) case 'cid-link': return lexCidLink.parse(val) case 'unknown': return lexUnknown.parse(val) } }, (val) => { if (!val || typeof val !== 'object') { return { message: 'Must be an object', fatal: true, } } if (val['type'] === undefined) { return { message: 'Must have a type', fatal: true, } } if (typeof val['type'] !== 'string') { return { message: 'Type property must be a string', fatal: true, } } return { message: `Invalid type: ${val['type']} must be one of: record, query, procedure, subscription, blob, array, token, object, boolean, integer, string, bytes, cid-link, unknown`, fatal: true, } }, ) export type LexUserType = z.infer<typeof lexUserType> export const lexiconDoc = z .object({ lexicon: z.literal(1), id: z.string().refine((v: string) => NSID.isValid(v), { message: 'Must be a valid NSID', }), revision: z.number().optional(), description: z.string().optional(), defs: z.record(lexUserType), }) .strict() .superRefine((doc, ctx) => { for (const defId in doc.defs) { const def = doc.defs[defId] if ( defId !== 'main' && (def.type === 'record' || def.type === 'procedure' || def.type === 'query' || def.type === 'subscription') ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Records, procedures, queries, and subscriptions must be the main definition.`, }) } } }) export type LexiconDoc = z.infer<typeof lexiconDoc> // helpers // = export function isValidLexiconDoc(v: unknown): v is LexiconDoc { return lexiconDoc.safeParse(v).success } export function isObj<V>(v: V): v is V & object { return v != null && typeof v === 'object' } export type DiscriminatedObject = { $type: string } export function isDiscriminatedObject(v: unknown): v is DiscriminatedObject { return isObj(v) && '$type' in v && typeof v.$type === 'string' } export function parseLexiconDoc(v: unknown): LexiconDoc { lexiconDoc.parse(v) return v as LexiconDoc } export type ValidationResult<V = unknown> = | { success: true value: V } | { success: false error: ValidationError } export class ValidationError extends Error {} export class InvalidLexiconError extends Error {} export class LexiconDefNotFoundError extends Error {}