···11+import { CID, isCid } from "./cid.ts";
22+import { isPlainObject } from "./object.ts";
33+import { ui8Equals } from "./uint8array.ts";
44+55+// @NOTE BlobRef is just a special case of LexMap.
66+77+export type LexScalar = number | string | boolean | null | CID | Uint8Array;
88+export type LexValue = LexScalar | LexValue[] | { [_ in string]?: LexValue };
99+export type LexMap = { [_ in string]?: LexValue };
1010+export type LexArray = LexValue[];
1111+1212+export function isLexMap(value: unknown): value is LexMap {
1313+ if (!isPlainObject(value)) return false;
1414+ for (const key in value) {
1515+ if (!isLexValue(value[key])) return false;
1616+ }
1717+ return true;
1818+}
1919+2020+export function isLexArray(value: unknown): value is LexArray {
2121+ if (!Array.isArray(value)) return false;
2222+ for (let i = 0; i < value.length; i++) {
2323+ if (!isLexValue(value[i])) return false;
2424+ }
2525+ return true;
2626+}
2727+2828+export function isLexScalar(value: unknown): value is LexScalar {
2929+ switch (typeof value) {
3030+ case "object":
3131+ if (value === null) return true;
3232+ return value instanceof Uint8Array || isCid(value);
3333+ case "string":
3434+ case "boolean":
3535+ return true;
3636+ case "number":
3737+ if (Number.isInteger(value)) return true;
3838+ throw new TypeError(`Invalid Lex value: ${value}`);
3939+ default:
4040+ throw new TypeError(`Invalid Lex value: ${typeof value}`);
4141+ }
4242+}
4343+4444+export function isLexValue(value: unknown): value is LexValue {
4545+ switch (typeof value) {
4646+ case "number":
4747+ if (!Number.isInteger(value)) return false;
4848+ // fallthrough
4949+ case "string":
5050+ case "boolean":
5151+ return true;
5252+ case "object":
5353+ if (value === null) return true;
5454+ if (Array.isArray(value)) {
5555+ for (let i = 0; i < value.length; i++) {
5656+ if (!isLexValue(value[i])) return false;
5757+ }
5858+ return true;
5959+ }
6060+ if (isPlainObject(value)) {
6161+ for (const key in value) {
6262+ if (!isLexValue(value[key])) return false;
6363+ }
6464+ return true;
6565+ }
6666+ if (value instanceof Uint8Array) return true;
6767+ if (isCid(value)) return true;
6868+ // fallthrough
6969+ default:
7070+ return false;
7171+ }
7272+}
7373+7474+export type TypedLexMap = LexMap & { $type: string };
7575+export function isTypedLexMap(value: LexValue): value is TypedLexMap {
7676+ return (
7777+ isLexMap(value) && typeof value.$type === "string" && value.$type.length > 0
7878+ );
7979+}
8080+8181+export function lexEquals(a: LexValue, b: LexValue): boolean {
8282+ if (Object.is(a, b)) {
8383+ return true;
8484+ }
8585+8686+ if (
8787+ a == null ||
8888+ b == null ||
8989+ typeof a !== "object" ||
9090+ typeof b !== "object"
9191+ ) {
9292+ return false;
9393+ }
9494+9595+ if (Array.isArray(a)) {
9696+ if (!Array.isArray(b)) {
9797+ return false;
9898+ }
9999+ if (a.length !== b.length) {
100100+ return false;
101101+ }
102102+ for (let i = 0; i < a.length; i++) {
103103+ if (!lexEquals(a[i], b[i])) {
104104+ return false;
105105+ }
106106+ }
107107+ return true;
108108+ } else if (Array.isArray(b)) {
109109+ return false;
110110+ }
111111+112112+ if (ArrayBuffer.isView(a)) {
113113+ if (!ArrayBuffer.isView(b)) return false;
114114+ return ui8Equals(a as Uint8Array, b as Uint8Array);
115115+ } else if (ArrayBuffer.isView(b)) {
116116+ return false;
117117+ }
118118+119119+ if (isCid(a)) {
120120+ // @NOTE CID.equals returns its argument when it is falsy (e.g. null or
121121+ // undefined) so we need to explicitly check that the output is "true".
122122+ return CID.asCID(a)!.equals(CID.asCID(b)) === true;
123123+ } else if (isCid(b)) {
124124+ return false;
125125+ }
126126+127127+ if (!isPlainObject(a) || !isPlainObject(b)) {
128128+ // Foolproof (should never happen)
129129+ throw new TypeError(
130130+ "Invalid LexValue (expected CID, Uint8Array, or LexMap)",
131131+ );
132132+ }
133133+134134+ const aKeys = Object.keys(a);
135135+ const bKeys = Object.keys(b);
136136+137137+ if (aKeys.length !== bKeys.length) {
138138+ return false;
139139+ }
140140+141141+ for (const key of aKeys) {
142142+ const aVal = a[key];
143143+ const bVal = b[key];
144144+145145+ // Needed because of the optional index signature in the Lex object type
146146+ // though, in practice, aVal should never be undefined here.
147147+ if (aVal === undefined) {
148148+ if (bVal === undefined && bKeys.includes(key)) continue;
149149+ return false;
150150+ } else if (bVal === undefined) {
151151+ return false;
152152+ }
153153+154154+ if (!lexEquals(aVal, bVal)) {
155155+ return false;
156156+ }
157157+ }
158158+159159+ return true;
160160+}
+7
data/mod.ts
···11+export * from "./blob.ts";
22+export * from "./cid.ts";
33+export * from "./language.ts";
44+export * from "./lex.ts";
55+export * from "./object.ts";
66+export * from "./uint8array.ts";
77+export * from "./utf8.ts";
+21
data/object.ts
···11+export function isObject(input: unknown): input is object {
22+ return input != null && typeof input === 'object'
33+}
44+55+const ObjectProto = Object.prototype
66+const ObjectToString = Object.prototype.toString
77+88+export function isPlainObject(
99+ input: unknown,
1010+): input is object & Record<string, unknown> {
1111+ if (!input || typeof input !== 'object') return false
1212+ const proto = Object.getPrototypeOf(input)
1313+ if (proto === null) return true
1414+ return (
1515+ (proto === ObjectProto ||
1616+ // Needed to support NodeJS's `runInNewContext` which produces objects
1717+ // with a different prototype
1818+ Object.getPrototypeOf(proto) === null) &&
1919+ ObjectToString.call(input) === '[object Object]'
2020+ )
2121+}
···11+/**
22+ * Coerces various binary data representations into a Uint8Array.
33+ *
44+ * @return `undefined` if the input could not be coerced into a {@link Uint8Array}.
55+ */
66+export function asUint8Array(input: unknown): Uint8Array | undefined {
77+ if (input instanceof Uint8Array) {
88+ return input;
99+ }
1010+1111+ if (ArrayBuffer.isView(input)) {
1212+ return new Uint8Array(
1313+ input.buffer,
1414+ input.byteOffset,
1515+ input.byteLength / Uint8Array.BYTES_PER_ELEMENT,
1616+ );
1717+ }
1818+1919+ if (input instanceof ArrayBuffer) {
2020+ return new Uint8Array(input);
2121+ }
2222+2323+ return undefined;
2424+}
2525+2626+export function ui8Equals(a: Uint8Array, b: Uint8Array): boolean {
2727+ if (a.byteLength !== b.byteLength) {
2828+ return false;
2929+ }
3030+3131+ for (let i = 0; i < a.byteLength; i++) {
3232+ if (a[i] !== b[i]) {
3333+ return false;
3434+ }
3535+ }
3636+3737+ return true;
3838+}
+46
data/utf8.ts
···11+const segmenter = new Intl.Segmenter();
22+33+export function graphemeLen(str: string): number {
44+ let length = 0;
55+ for (const _ of segmenter.segment(str)) length++;
66+ return length;
77+}
88+99+export function utf8Len(string: string): number {
1010+ // similar to TextEncoder's implementation of UTF-8 encoding.
1111+ // However, using TextEncoder to get the byte length is slower
1212+ // as it requires allocating a new Uint8Array and copying data:
1313+1414+ // return new TextEncoder().encode(string).byteLength
1515+1616+ // The base length is the string length (all ASCII)
1717+ let len = string.length;
1818+ let code: number;
1919+2020+ // The loop calculates the number of additional bytes needed for
2121+ // non-ASCII characters
2222+ for (let i = 0; i < string.length; i += 1) {
2323+ code = string.charCodeAt(i);
2424+2525+ if (code <= 0x7f) {
2626+ // ASCII, 1 byte
2727+ } else if (code <= 0x7ff) {
2828+ // 2 bytes char
2929+ len += 1;
3030+ } else {
3131+ // 3 bytes char
3232+ len += 2;
3333+ // If the current char is a high surrogate, and the next char is a low
3434+ // surrogate, skip the next char as the total is a 4 bytes char
3535+ // (represented as a surrogate pair in UTF-16) and was already accounted
3636+ // for.
3737+ if (code >= 0xd800 && code <= 0xdbff) {
3838+ code = string.charCodeAt(i + 1);
3939+ if (code >= 0xdc00 && code <= 0xdfff) {
4040+ i++;
4141+ }
4242+ }
4343+ }
4444+ }
4545+ return len;
4646+}
···11+export * from "./core/$type.ts";
22+export * from "./core/record-key.ts";
33+export * from "./core/result.ts";
44+export * from "./core/string-format.ts";
55+export * from "./core/types.ts";
+16
lex/schema/core/$type.ts
···11+import type { Nsid } from "./string-format.ts";
22+33+export type $Type<
44+ N extends Nsid = Nsid,
55+ H extends string = string,
66+> = N extends Nsid ? string extends H ? N | `${N}#${string}`
77+ : H extends "main" ? N
88+ : `${N}#${H}`
99+ : never;
1010+1111+export function $type<N extends Nsid, H extends string>(
1212+ nsid: N,
1313+ hash: H,
1414+): $Type<N, H> {
1515+ return (hash === "main" ? nsid : `${nsid}#${hash}`) as $Type<N, H>;
1616+}
+15
lex/schema/core/record-key.ts
···11+export type RecordKey = "any" | "nsid" | "tid" | `literal:${string}`;
22+33+export function isRecordKey<T>(key: T): key is T & RecordKey {
44+ return (
55+ key === "any" ||
66+ key === "nsid" ||
77+ key === "tid" ||
88+ (typeof key === "string" && key.startsWith("literal:"))
99+ );
1010+}
1111+1212+export function asRecordKey(key: unknown): RecordKey {
1313+ if (isRecordKey(key)) return key;
1414+ throw new Error(`Invalid record key: ${String(key)}`);
1515+}
+75
lex/schema/core/result.ts
···11+export type ResultSuccess<V = unknown> = { success: true; value: V };
22+export type ResultFailure<E = Error> = { success: false; error: E };
33+44+export type Result<V = unknown, E = Error> =
55+ | ResultSuccess<V>
66+ | ResultFailure<E>;
77+88+export function success<V>(value: V): ResultSuccess<V> {
99+ return { success: true, value };
1010+}
1111+1212+export function failure<E>(error: E): ResultFailure<E> {
1313+ return { success: false, error };
1414+}
1515+1616+export function failureError<T>(result: ResultFailure<T>): T {
1717+ return result.error;
1818+}
1919+2020+export function successValue<T>(result: ResultSuccess<T>): T {
2121+ return result.value;
2222+}
2323+2424+/**
2525+ * Catches any error and wraps it in a {@link ResultFailure<Error>}.
2626+ *
2727+ * @param err - The error to catch.
2828+ * @returns A {@link ResultFailure<Error>} containing the caught error.
2929+ * @example
3030+ *
3131+ * ```ts
3232+ * declare function someFunction(): Promise<ResultSuccess<string>>
3333+ *
3434+ * const result = await someFunction().catch(catchall)
3535+ * if (result.success) {
3636+ * console.log(result.value) // string
3737+ * } else {
3838+ * console.error(result.error instanceof Error) // true
3939+ * console.error(result.error.message) // string
4040+ * }
4141+ * ```
4242+ */
4343+export function catchall(err: unknown): ResultFailure<Error> {
4444+ if (err instanceof Error) return failure(err);
4545+ return failure(new Error("Unknown error", { cause: err }));
4646+}
4747+4848+/**
4949+ * Creates a catcher function for the given constructor that wraps caught errors
5050+ * in a {@link ResultFailure}.
5151+ *
5252+ * @example
5353+ *
5454+ * ```ts
5555+ * class FooError extends Error {}
5656+ * class BarError extends Error {}
5757+ *
5858+ * declare function someFunction(): Promise<ResultSuccess<string>>
5959+ *
6060+ * const result = await someFunction()
6161+ * .catch(createCatcher(FooError))
6262+ * .catch(createCatcher(BarError))
6363+ *
6464+ * if (result.success) {
6565+ * console.log(result.value) // string
6666+ * } else {
6767+ * console.error(result.error) // FooError | BarError
6868+ * }
6969+ */
7070+export function createCatcher<T>(Ctor: new (...args: unknown[]) => T) {
7171+ return (err: unknown): ResultFailure<T> => {
7272+ if (err instanceof Ctor) return failure(err);
7373+ throw err;
7474+ };
7575+}
+121
lex/schema/core/string-format.ts
···11+import { ensureValidCidString, isLanguage } from "@atp/data";
22+import {
33+ ensureValidAtUri,
44+ ensureValidDatetime,
55+ ensureValidDid,
66+ ensureValidHandle,
77+ ensureValidNsid,
88+ ensureValidRecordKey,
99+ ensureValidTid,
1010+} from "@atp/syntax";
1111+1212+// Allow (date as Date).toISOString() to be used where datetime format is expected
1313+declare global {
1414+ interface Date {
1515+ toISOString(): `${string}T${string}Z`;
1616+ }
1717+}
1818+1919+export const STRING_FORMATS = Object.freeze(
2020+ [
2121+ "datetime",
2222+ "uri",
2323+ "at-uri",
2424+ "did",
2525+ "handle",
2626+ "at-identifier",
2727+ "nsid",
2828+ "cid",
2929+ "language",
3030+ "tid",
3131+ "record-key",
3232+ ] as const,
3333+);
3434+3535+export type StringFormat = (typeof STRING_FORMATS)[number];
3636+3737+export type Did<M extends string = string> = `did:${M}:${string}`;
3838+export type Uri = `${string}:${string}`;
3939+export type Nsid = `${string}.${string}.${string}`;
4040+/** An ISO 8601 formatted datetime string (YYYY-MM-DDTHH:mm:ss.sssZ) */
4141+export type Datetime = `${string}T${string}`;
4242+export type Handle = `${string}.${string}`;
4343+export type AtIdentifier = Did | Handle;
4444+export type AtUri = `at://${AtIdentifier}/${Nsid}/${string}`;
4545+4646+export type InferStringFormat<F> =
4747+ //
4848+ F extends "datetime" ? Datetime
4949+ : F extends "uri" ? Uri
5050+ : F extends "at-uri" ? AtUri
5151+ : F extends "did" ? Did
5252+ : F extends "handle" ? Handle
5353+ : F extends "at-identifier" ? AtIdentifier
5454+ : F extends "nsid" ? Nsid
5555+ : string;
5656+5757+type AssertFn<T> = <I extends string>(input: I) => asserts input is I & T;
5858+5959+// Re-export utility typed as assertion functions so that TypeScript can
6060+// infer the narrowed type after calling them.
6161+6262+export const assertDid: AssertFn<Did> = ensureValidDid;
6363+export const assertAtUri: AssertFn<AtUri> = ensureValidAtUri;
6464+export const assertNsid: AssertFn<Nsid> = ensureValidNsid;
6565+export const assertTid: AssertFn<string> = ensureValidTid;
6666+export const assertRecordKey: AssertFn<string> = ensureValidRecordKey;
6767+export const assertDatetime: AssertFn<Datetime> = ensureValidDatetime;
6868+export const assertCidString: AssertFn<string> = ensureValidCidString;
6969+export const assertHandle: AssertFn<Handle> = ensureValidHandle;
7070+7171+// Export utilities for formats missing from @atproto/syntax
7272+7373+export const assertUri: AssertFn<Uri> = (input) => {
7474+ if (!/^\w+:(?:\/\/)?[^\s/][^\s]*$/.test(input)) {
7575+ throw new Error("Invalid URI");
7676+ }
7777+};
7878+export const assertLanguage: AssertFn<string> = (input) => {
7979+ if (!isLanguage(input)) {
8080+ throw new Error("Invalid BCP 47 string");
8181+ }
8282+};
8383+export const assertAtIdentifier: AssertFn<AtIdentifier> = (input) => {
8484+ if (input.startsWith("did:web:") || input.startsWith("did:plc:")) {
8585+ assertDid(input);
8686+ } else if (input.startsWith("did:")) {
8787+ throw new Error("Invalid DID method");
8888+ } else {
8989+ try {
9090+ assertHandle(input);
9191+ } catch (cause) {
9292+ throw new Error("Invalid DID or handle", { cause });
9393+ }
9494+ }
9595+};
9696+9797+const formatters = /*#__PURE__*/ new Map<StringFormat, (str: string) => void>(
9898+ [
9999+ ["datetime", assertDatetime],
100100+ ["uri", assertUri],
101101+ ["at-uri", assertAtUri],
102102+ ["did", assertDid],
103103+ ["handle", assertHandle],
104104+ ["at-identifier", assertAtIdentifier],
105105+ ["nsid", assertNsid],
106106+ ["cid", assertCidString],
107107+ ["language", assertLanguage],
108108+ ["tid", assertTid],
109109+ ["record-key", assertRecordKey],
110110+ ] as const,
111111+);
112112+113113+export function assertStringFormat<F extends StringFormat>(
114114+ input: string,
115115+ format: F,
116116+): asserts input is InferStringFormat<F> {
117117+ const assertFn = formatters.get(format);
118118+ if (assertFn) assertFn(input);
119119+ // Fool-proof
120120+ else throw new Error(`Unknown string format: ${format}`);
121121+}
+22
lex/schema/core/types.ts
···11+/**
22+ * Same as {@link string} but prevents TypeScript allowing union types to
33+ * be widened to `string` in IDEs.
44+ */
55+export type UnknownString = string & NonNullable<unknown>;
66+77+export type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>;
88+99+// @NOTE there is no way to express "array containing at least one P", so we use
1010+// "array that contains P at first or last position" as a workaround.
1111+export type ArrayContaining<T, Items = unknown> =
1212+ | readonly [T, ...Items[]]
1313+ | readonly [...Items[], T];
1414+1515+declare const __restricted: unique symbol;
1616+/**
1717+ * A type that represents a value that cannot be used, with a custom
1818+ * message explaining the restriction.
1919+ */
2020+export type Restricted<Message extends string> = typeof __restricted & {
2121+ [__restricted]: Message;
2222+};
+363
lex/schema/external.ts
···11+import { type $Type, $type, type Nsid, type RecordKey } from "./core.ts";
22+import {
33+ ArraySchema,
44+ type ArraySchemaOptions,
55+ BlobSchema,
66+ type BlobSchemaOptions,
77+ BooleanSchema,
88+ type BooleanSchemaOptions,
99+ BytesSchema,
1010+ type BytesSchemaOptions,
1111+ CidSchema,
1212+ type CustomAssertion,
1313+ CustomSchema,
1414+ DictSchema,
1515+ DiscriminatedUnionSchema,
1616+ type DiscriminatedUnionSchemaVariants,
1717+ EnumSchema,
1818+ IntegerSchema,
1919+ type IntegerSchemaOptions,
2020+ IntersectionSchema,
2121+ type IntersectionSchemaValidators,
2222+ LiteralSchema,
2323+ NeverSchema,
2424+ NullSchema,
2525+ ObjectSchema,
2626+ type ObjectSchemaOptions,
2727+ type ObjectSchemaProperties,
2828+ ParamsSchema,
2929+ type ParamsSchemaOptions,
3030+ type ParamsSchemaProperties,
3131+ Payload,
3232+ type PayloadBody,
3333+ Permission,
3434+ type PermissionOptions,
3535+ PermissionSet,
3636+ type PermissionSetOptions,
3737+ Procedure,
3838+ Query,
3939+ RecordSchema,
4040+ RefSchema,
4141+ type RefSchemaGetter,
4242+ StringSchema,
4343+ type StringSchemaOptions,
4444+ Subscription,
4545+ TokenSchema,
4646+ TypedObjectSchema,
4747+ type TypedRefGetter,
4848+ TypedRefSchema,
4949+ TypedUnionSchema,
5050+ UnionSchema,
5151+ type UnionSchemaValidators,
5252+ type UnknownObjectOutput,
5353+ UnknownObjectSchema,
5454+ UnknownSchema,
5555+} from "./schema.ts";
5656+import type { Infer, PropertyKey, Validator } from "./validation.ts";
5757+5858+export * from "./core.ts";
5959+export * from "./schema.ts";
6060+export * from "./validation.ts";
6161+6262+/*@__NO_SIDE_EFFECTS__*/
6363+export function never() {
6464+ return new NeverSchema();
6565+}
6666+6767+/*@__NO_SIDE_EFFECTS__*/
6868+export function unknown() {
6969+ return new UnknownSchema();
7070+}
7171+7272+/*@__NO_SIDE_EFFECTS__*/
7373+export function _null() {
7474+ return new NullSchema();
7575+}
7676+7777+export { _null as null };
7878+7979+/*@__NO_SIDE_EFFECTS__*/
8080+export function literal<const V extends null | string | number | boolean>(
8181+ value: V,
8282+) {
8383+ return new LiteralSchema<V>(value);
8484+}
8585+8686+/*@__NO_SIDE_EFFECTS__*/
8787+export function _enum<const V extends null | string | number | boolean>(
8888+ value: readonly V[],
8989+) {
9090+ return new EnumSchema<V>(value);
9191+}
9292+9393+// @NOTE "enum" is a reserved keyword in JS/TS
9494+export { _enum as enum };
9595+9696+/*@__NO_SIDE_EFFECTS__*/
9797+export function boolean(options: BooleanSchemaOptions = {}) {
9898+ return new BooleanSchema(options);
9999+}
100100+101101+/*@__NO_SIDE_EFFECTS__*/
102102+export function integer(options: IntegerSchemaOptions = {}) {
103103+ return new IntegerSchema(options);
104104+}
105105+106106+/*@__NO_SIDE_EFFECTS__*/
107107+export function cidLink() {
108108+ return new CidSchema();
109109+}
110110+111111+/*@__NO_SIDE_EFFECTS__*/
112112+export function bytes(options: BytesSchemaOptions = {}) {
113113+ return new BytesSchema(options);
114114+}
115115+116116+/*@__NO_SIDE_EFFECTS__*/
117117+export function blob(options: BlobSchemaOptions = {}) {
118118+ return new BlobSchema(options);
119119+}
120120+121121+/*@__NO_SIDE_EFFECTS__*/
122122+export function string<
123123+ const O extends StringSchemaOptions = NonNullable<unknown>,
124124+>(options: StringSchemaOptions & O = {} as O) {
125125+ return new StringSchema<O>(options);
126126+}
127127+128128+/*@__NO_SIDE_EFFECTS__*/
129129+export function array<const T>(
130130+ items: Validator<T>,
131131+ options: ArraySchemaOptions = {},
132132+) {
133133+ return new ArraySchema(items, options);
134134+}
135135+136136+/*@__NO_SIDE_EFFECTS__*/
137137+export function object<
138138+ const P extends ObjectSchemaProperties,
139139+ const O extends ObjectSchemaOptions = NonNullable<unknown>,
140140+>(
141141+ properties: ObjectSchemaProperties & P,
142142+ options: ObjectSchemaOptions & O = {} as O,
143143+) {
144144+ return new ObjectSchema<P, O>(properties, options);
145145+}
146146+147147+/*@__NO_SIDE_EFFECTS__*/
148148+export function dict<const K extends Validator, const V extends Validator>(
149149+ key: K,
150150+ value: V,
151151+) {
152152+ return new DictSchema<K, V>(key, value);
153153+}
154154+155155+// Utility
156156+export type { UnknownObjectOutput as UnknownObject };
157157+158158+/*@__NO_SIDE_EFFECTS__*/
159159+export function unknownObject() {
160160+ return new UnknownObjectSchema();
161161+}
162162+163163+/*@__NO_SIDE_EFFECTS__*/
164164+export function ref<T>(get: RefSchemaGetter<T>) {
165165+ return new RefSchema<T>(get);
166166+}
167167+168168+/*@__NO_SIDE_EFFECTS__*/
169169+export function custom<T>(
170170+ assertion: CustomAssertion<T>,
171171+ message: string,
172172+ path?: PropertyKey | readonly PropertyKey[],
173173+) {
174174+ return new CustomSchema<T>(assertion, message, path);
175175+}
176176+177177+/*@__NO_SIDE_EFFECTS__*/
178178+export function union<const V extends UnionSchemaValidators>(validators: V) {
179179+ return new UnionSchema<V>(validators);
180180+}
181181+182182+/*@__NO_SIDE_EFFECTS__*/
183183+export function intersection<const V extends IntersectionSchemaValidators>(
184184+ validators: V,
185185+) {
186186+ return new IntersectionSchema<V>(validators);
187187+}
188188+189189+/*@__NO_SIDE_EFFECTS__*/
190190+export function discriminatedUnion<
191191+ const Discriminator extends string,
192192+ const Options extends DiscriminatedUnionSchemaVariants<Discriminator>,
193193+>(discriminator: Discriminator, variants: Options) {
194194+ return new DiscriminatedUnionSchema<Discriminator, Options>(
195195+ discriminator,
196196+ variants,
197197+ );
198198+}
199199+200200+/*@__NO_SIDE_EFFECTS__*/
201201+export function token<const N extends Nsid, const H extends string>(
202202+ nsid: N,
203203+ hash: H,
204204+) {
205205+ return new TokenSchema($type(nsid, hash));
206206+}
207207+208208+/*@__NO_SIDE_EFFECTS__*/
209209+export function typedRef<const V extends { $type?: string }>(
210210+ get: TypedRefGetter<V>,
211211+) {
212212+ return new TypedRefSchema<V>(get);
213213+}
214214+215215+/*@__NO_SIDE_EFFECTS__*/
216216+export function typedUnion<
217217+ const R extends readonly TypedRefSchema[],
218218+ const C extends boolean,
219219+>(refs: R, closed: C) {
220220+ return new TypedUnionSchema<R, C>(refs, closed);
221221+}
222222+223223+/**
224224+ * This function offers two overloads:
225225+ * - One that allows creating a {@link TypedObjectSchema}, and infer the output
226226+ * type from the provided arguments, without requiring to specify any of the
227227+ * generics. This is useful when you want to define a record without
228228+ * explicitly defining its interface. This version does not support circular
229229+ * references, as TypeScript cannot infer types in such cases.
230230+ * - One allows creating a {@link TypedObjectSchema} with an explicitly defined
231231+ * interface. This will typically be used by codegen (`lex build`) to generate
232232+ * schemas that work even if they contain circular references.
233233+ */
234234+export function typedObject<
235235+ const N extends Nsid,
236236+ const H extends string,
237237+ const Schema extends Validator<{ [_ in string]?: unknown }>,
238238+>(nsid: N, hash: H, schema: Schema): TypedObjectSchema<$Type<N, H>, Schema>;
239239+export function typedObject<const V extends { $type?: $Type }>(
240240+ nsid: V extends { $type?: infer T extends string }
241241+ ? T extends `${infer N}#${string}` ? N
242242+ : T // (T is a "main" type, so already an NSID)
243243+ : never,
244244+ hash: V extends { $type?: infer T extends string }
245245+ ? T extends `${string}#${infer H}` ? H
246246+ : "main"
247247+ : never,
248248+ schema: Validator<Omit<V, "$type">>,
249249+): TypedObjectSchema<NonNullable<V["$type"]>, typeof schema, V>;
250250+/*@__NO_SIDE_EFFECTS__*/
251251+export function typedObject<
252252+ const N extends Nsid,
253253+ const H extends string,
254254+ const Schema extends Validator<{ [_ in string]?: unknown }>,
255255+>(nsid: N, hash: H, schema: Schema) {
256256+ return new TypedObjectSchema<$Type<N, H>, Schema>($type(nsid, hash), schema);
257257+}
258258+259259+/**
260260+ * Ensures that a `$type` used in a record is a valid NSID (i.e. no fragment).
261261+ */
262262+type AsNsid<T> = T extends `${string}#${string}` ? never : T;
263263+264264+/**
265265+ * This function offers two overloads:
266266+ * - One that allows creating a {@link RecordSchema}, and infer the output type
267267+ * from the provided arguments, without requiring to specify any of the
268268+ * generics. This is useful when you want to define a record without
269269+ * explicitly defining its interface. This version does not support circular
270270+ * references, as TypeScript cannot infer types in such cases.
271271+ * - One allows creating a {@link RecordSchema} with an explicitly defined
272272+ * interface. This will typically be used by codegen (`lex build`) to generate
273273+ * schemas that work even if they contain circular references.
274274+ */
275275+export function record<
276276+ const K extends RecordKey,
277277+ const T extends Nsid,
278278+ const S extends Validator<{ [_ in string]?: unknown }>,
279279+>(
280280+ key: K,
281281+ type: AsNsid<T>,
282282+ schema: S,
283283+): RecordSchema<K, T, S, Infer<S> & { $type: T }>;
284284+export function record<
285285+ const K extends RecordKey,
286286+ const V extends { $type: Nsid },
287287+>(
288288+ key: K,
289289+ type: AsNsid<V["$type"]>,
290290+ schema: Validator<Omit<V, "$type">>,
291291+): RecordSchema<K, V["$type"], typeof schema, V>;
292292+/*@__NO_SIDE_EFFECTS__*/
293293+export function record<
294294+ const K extends RecordKey,
295295+ const T extends Nsid,
296296+ const S extends Validator<{ [_ in string]?: unknown }>,
297297+>(key: K, type: T, schema: S) {
298298+ return new RecordSchema<K, T, S, Infer<S> & { $type: T }>(key, type, schema);
299299+}
300300+301301+/*@__NO_SIDE_EFFECTS__*/
302302+export function params<
303303+ const P extends ParamsSchemaProperties = NonNullable<unknown>,
304304+ const O extends ParamsSchemaOptions = ParamsSchemaOptions,
305305+>(properties: P = {} as P, options: ParamsSchemaOptions & O = {} as O) {
306306+ return new ParamsSchema<P, O>(properties, options);
307307+}
308308+309309+/*@__NO_SIDE_EFFECTS__*/
310310+export function payload<
311311+ const E extends string | undefined = undefined,
312312+ const S extends PayloadBody<E> = undefined,
313313+>(encoding: E = undefined as E, schema: S = undefined as S) {
314314+ return new Payload<E, S>(encoding, schema);
315315+}
316316+317317+/*@__NO_SIDE_EFFECTS__*/
318318+export function query<
319319+ const N extends Nsid,
320320+ const P extends ParamsSchema,
321321+ const O extends Payload,
322322+ const E extends undefined | readonly string[] = undefined,
323323+>(nsid: N, parameters: P, output: O, errors: E = undefined as E) {
324324+ return new Query<N, P, O, E>(nsid, parameters, output, errors);
325325+}
326326+327327+/*@__NO_SIDE_EFFECTS__*/
328328+export function procedure<
329329+ const N extends Nsid,
330330+ const P extends ParamsSchema,
331331+ const I extends Payload,
332332+ const O extends Payload,
333333+ const E extends undefined | readonly string[] = undefined,
334334+>(nsid: N, parameters: P, input: I, output: O, errors: E = undefined as E) {
335335+ return new Procedure<N, P, I, O, E>(nsid, parameters, input, output, errors);
336336+}
337337+338338+/*@__NO_SIDE_EFFECTS__*/
339339+export function subscription<
340340+ const N extends string,
341341+ const P extends ParamsSchema,
342342+ const M extends undefined | RefSchema | TypedUnionSchema | ObjectSchema,
343343+ const E extends undefined | readonly string[] = undefined,
344344+>(nsid: N, parameters: P, message: M, errors: E = undefined as E) {
345345+ return new Subscription<N, P, M, E>(nsid, parameters, message, errors);
346346+}
347347+348348+/*@__NO_SIDE_EFFECTS__*/
349349+export function permission<
350350+ const R extends string,
351351+ const O extends PermissionOptions,
352352+>(resource: R, options: PermissionOptions & O = {} as O) {
353353+ return new Permission<R, O>(resource, options);
354354+}
355355+356356+/*@__NO_SIDE_EFFECTS__*/
357357+export function permissionSet<
358358+ const N extends string,
359359+ const P extends readonly Permission[],
360360+ const O extends PermissionSetOptions,
361361+>(nsid: N, permissions: P, options: PermissionSetOptions & O = {} as O) {
362362+ return new PermissionSet<N, P, O>(nsid, permissions, options);
363363+}
+3
lex/schema/index.ts
···11+import * as l from "./external.ts";
22+export * from "./external.ts";
33+export { l };
+40
lex/schema/schema.ts
···11+// Utilities (that depend on *and* are used by schemas)
22+export * from "./schema/_parameters.ts";
33+44+// Concrete Types
55+export * from "./schema/array.ts";
66+export * from "./schema/blob.ts";
77+export * from "./schema/boolean.ts";
88+export * from "./schema/bytes.ts";
99+export * from "./schema/cid.ts";
1010+export * from "./schema/dict.ts";
1111+export * from "./schema/enum.ts";
1212+export * from "./schema/integer.ts";
1313+export * from "./schema/literal.ts";
1414+export * from "./schema/never.ts";
1515+export * from "./schema/null.ts";
1616+export * from "./schema/object.ts";
1717+export * from "./schema/string.ts";
1818+export * from "./schema/unknown-object.ts";
1919+export * from "./schema/unknown.ts";
2020+2121+// Composite Types
2222+export * from "./schema/custom.ts";
2323+export * from "./schema/discriminated-union.ts";
2424+export * from "./schema/intersection.ts";
2525+export * from "./schema/ref.ts";
2626+export * from "./schema/union.ts";
2727+2828+// Lexicon specific Types
2929+export * from "./schema/params.ts";
3030+export * from "./schema/payload.ts";
3131+export * from "./schema/permission-set.ts";
3232+export * from "./schema/permission.ts";
3333+export * from "./schema/procedure.ts";
3434+export * from "./schema/query.ts";
3535+export * from "./schema/record.ts";
3636+export * from "./schema/subscription.ts";
3737+export * from "./schema/token.ts";
3838+export * from "./schema/typed-object.ts";
3939+export * from "./schema/typed-ref.ts";
4040+export * from "./schema/typed-union.ts";
+26
lex/schema/schema/_parameters.ts
···11+import type { Infer, Validator } from "../validation.ts";
22+import { ArraySchema } from "./array.ts";
33+import { BooleanSchema } from "./boolean.ts";
44+import { DictSchema } from "./dict.ts";
55+import { IntegerSchema } from "./integer.ts";
66+import { StringSchema } from "./string.ts";
77+import { UnionSchema } from "./union.ts";
88+99+export type ParamScalar = Infer<typeof paramScalarSchema>;
1010+const paramScalarSchema = new UnionSchema([
1111+ new BooleanSchema({}),
1212+ new IntegerSchema({}),
1313+ new StringSchema({}),
1414+]);
1515+1616+export type Param = Infer<typeof paramSchema>;
1717+export const paramSchema = new UnionSchema([
1818+ paramScalarSchema,
1919+ new ArraySchema(paramScalarSchema, {}),
2020+]);
2121+2222+export type Params = { [_: string]: undefined | Param };
2323+export const paramsSchema = new DictSchema(
2424+ new StringSchema({}),
2525+ paramSchema,
2626+) satisfies Validator<Params>;
+55
lex/schema/schema/array.ts
···11+import {
22+ type ValidationResult,
33+ Validator,
44+ type ValidatorContext,
55+} from "../validation.ts";
66+77+export type ArraySchemaOptions = {
88+ minLength?: number;
99+ maxLength?: number;
1010+};
1111+1212+export class ArraySchema<Item = unknown> extends Validator<Array<Item>> {
1313+ override readonly lexiconType = "array" as const;
1414+1515+ constructor(
1616+ readonly items: Validator<Item>,
1717+ readonly options: ArraySchemaOptions,
1818+ ) {
1919+ super();
2020+ }
2121+2222+ override validateInContext(
2323+ input: unknown,
2424+ ctx: ValidatorContext,
2525+ ): ValidationResult<Array<Item>> {
2626+ if (!Array.isArray(input)) {
2727+ return ctx.issueInvalidType(input, "array");
2828+ }
2929+3030+ const { minLength, maxLength } = this.options;
3131+3232+ if (minLength != null && input.length < minLength) {
3333+ return ctx.issueTooSmall(input, "array", minLength, input.length);
3434+ }
3535+3636+ if (maxLength != null && input.length > maxLength) {
3737+ return ctx.issueTooBig(input, "array", maxLength, input.length);
3838+ }
3939+4040+ let copy: undefined | Array<Item>;
4141+4242+ for (let i = 0; i < input.length; i++) {
4343+ const result = ctx.validateChild(input, i, this.items);
4444+ if (!result.success) return result;
4545+4646+ if (result.value !== input[i]) {
4747+ // Copy on write (but only if we did not already make a copy)
4848+ copy ??= Array.from(input);
4949+ copy[i] = result.value;
5050+ }
5151+ }
5252+5353+ return ctx.success(copy ?? input) as ValidationResult<Array<Item>>;
5454+ }
5555+}
+86
lex/schema/schema/blob.ts
···11+import {
22+ type BlobRef,
33+ isBlobRef,
44+ isLegacyBlobRef,
55+ type LegacyBlobRef,
66+} from "@atp/data";
77+import {
88+ type ValidationResult,
99+ Validator,
1010+ type ValidatorContext,
1111+} from "../validation.ts";
1212+1313+export type BlobSchemaOptions = {
1414+ /**
1515+ * Whether to allow legacy blob references format
1616+ * @see {@link LegacyBlobRef}
1717+ */
1818+ allowLegacy?: boolean;
1919+ /**
2020+ * Whether to enforce strict validation on the blob reference (CID version, codec, hash function)
2121+ */
2222+ strict?: boolean;
2323+ /**
2424+ * List of accepted mime types
2525+ */
2626+ accept?: string[];
2727+ /**
2828+ * Maximum size in bytes
2929+ */
3030+ maxSize?: number;
3131+};
3232+3333+export type { BlobRef, LegacyBlobRef };
3434+3535+export type BlobSchemaOutput<Options> = Options extends { allowLegacy: true }
3636+ ? BlobRef | LegacyBlobRef
3737+ : BlobRef;
3838+3939+export class BlobSchema<O extends BlobSchemaOptions> extends Validator<
4040+ BlobSchemaOutput<O>
4141+> {
4242+ override readonly lexiconType = "blob" as const;
4343+4444+ constructor(readonly options: O) {
4545+ super();
4646+ }
4747+4848+ override validateInContext(
4949+ input: unknown,
5050+ ctx: ValidatorContext,
5151+ ): ValidationResult<BlobSchemaOutput<O>> {
5252+ if (!isBlob(input, this.options)) {
5353+ return ctx.issueInvalidType(input, "blob");
5454+ }
5555+5656+ // @NOTE Historically, we did not enforce constraints on blob references
5757+ // https://github.com/bluesky-social/atproto/blob/4c15fb47cec26060bff2e710e95869a90c9d7fdd/packages/lexicon/src/validators/blob.ts#L5-L19
5858+5959+ // const { accept } = this.options
6060+ // if (accept && !accept.includes(input.mimeType)) {
6161+ // return ctx.issueInvalidValue(input, accept)
6262+ // }
6363+6464+ // const { maxSize } = this.options
6565+ // if (maxSize != null && input.size != -1 && input.size > maxSize) {
6666+ // return ctx.issueTooBig(input, 'blob', maxSize, input.size)
6767+ // }
6868+6969+ return ctx.success(input);
7070+ }
7171+}
7272+7373+function isBlob<O extends BlobSchemaOptions>(
7474+ input: unknown,
7575+ options: O,
7676+): input is BlobSchemaOutput<O> {
7777+ if ((input as { $type: string })?.$type !== undefined) {
7878+ return isBlobRef(input, options);
7979+ }
8080+8181+ if (options.allowLegacy === true) {
8282+ return isLegacyBlobRef(input);
8383+ }
8484+8585+ return false;
8686+}
···11+import { isPlainObject } from "@atp/data";
22+import {
33+ type Infer,
44+ type ValidationResult,
55+ Validator,
66+ type ValidatorContext,
77+} from "../validation.ts";
88+99+export type DictSchemaOutput<
1010+ KeySchema extends Validator,
1111+ ValueSchema extends Validator,
1212+> = Infer<KeySchema> extends never ? Record<string, never>
1313+ : Record<Infer<KeySchema> & string, Infer<ValueSchema>>;
1414+1515+/**
1616+ * @note There is no dictionary in Lexicon schemas. This is a custom extension
1717+ * to allow map-like objects when using the lex library programmatically (i.e.
1818+ * not code generated from a lexicon schema).
1919+ */
2020+export class DictSchema<
2121+ const KeySchema extends Validator,
2222+ const ValueSchema extends Validator,
2323+> extends Validator<DictSchemaOutput<KeySchema, ValueSchema>> {
2424+ constructor(
2525+ readonly keySchema: KeySchema,
2626+ readonly valueSchema: ValueSchema,
2727+ ) {
2828+ super();
2929+ }
3030+3131+ override validateInContext(
3232+ input: unknown,
3333+ ctx: ValidatorContext,
3434+ options?: { ignoredKeys?: { has(k: string): boolean } },
3535+ ): ValidationResult<DictSchemaOutput<KeySchema, ValueSchema>> {
3636+ if (!isPlainObject(input)) {
3737+ return ctx.issueInvalidType(input, "dict");
3838+ }
3939+4040+ let copy: undefined | Record<string, unknown>;
4141+4242+ for (const key in input) {
4343+ if (options?.ignoredKeys?.has(key)) continue;
4444+4545+ const keyResult = ctx.validate(key, this.keySchema);
4646+ if (!keyResult.success) return keyResult;
4747+ if (keyResult.value !== key) {
4848+ // We can't safely "move" the key to a different name in the output
4949+ // object (because there may already be something there), so we issue a
5050+ // "required key" error if the key validation changes the key
5151+ return ctx.issueRequiredKey(input, key);
5252+ }
5353+5454+ const valueResult = ctx.validateChild(input, key, this.valueSchema);
5555+ if (!valueResult.success) return valueResult;
5656+5757+ if (valueResult.value !== input[key]) {
5858+ copy ??= { ...input };
5959+ copy[key] = valueResult.value;
6060+ }
6161+ }
6262+6363+ return ctx.success(
6464+ (copy ?? input) as DictSchemaOutput<KeySchema, ValueSchema>,
6565+ );
6666+ }
6767+}
+143
lex/schema/schema/discriminated-union.ts
···11+import { isPlainObject } from "@atp/data";
22+import type { ArrayContaining } from "../core.ts";
33+import {
44+ ValidationError,
55+ type ValidationFailure,
66+ type ValidationResult,
77+ Validator,
88+ type ValidatorContext,
99+} from "../validation.ts";
1010+import { EnumSchema } from "./enum.ts";
1111+import { LiteralSchema } from "./literal.ts";
1212+import type { ObjectSchema } from "./object.ts";
1313+1414+export type DiscriminatedUnionSchemaVariant<Discriminator extends string> =
1515+ ObjectSchema<
1616+ { [_ in Discriminator]: Validator },
1717+ { required: ArrayContaining<Discriminator, string> }
1818+ >;
1919+2020+export type DiscriminatedUnionSchemaVariants<Discriminator extends string> =
2121+ readonly [
2222+ DiscriminatedUnionSchemaVariant<Discriminator>,
2323+ ...DiscriminatedUnionSchemaVariant<Discriminator>[],
2424+ ];
2525+2626+export type DiscriminatedUnionSchemaOutput<
2727+ Options extends readonly Validator[],
2828+> = Options extends readonly [Validator<infer V>] ? V
2929+ : Options extends readonly [
3030+ Validator<infer V>,
3131+ ...infer Rest extends Validator[],
3232+ ] ? V | DiscriminatedUnionSchemaOutput<Rest>
3333+ : never;
3434+3535+/**
3636+ * @note There is no discriminated union in Lexicon schemas. This is a custom
3737+ * extension to allow optimized validation of union of objects when using the
3838+ * lex library programmatically (i.e. not code generated from a lexicon schema).
3939+ */
4040+export class DiscriminatedUnionSchema<
4141+ const Discriminator extends string = string,
4242+ const Options extends DiscriminatedUnionSchemaVariants<Discriminator> =
4343+ DiscriminatedUnionSchemaVariants<Discriminator>,
4444+> extends Validator<DiscriminatedUnionSchemaOutput<Options>> {
4545+ constructor(
4646+ readonly discriminator: Discriminator,
4747+ readonly variants: Options,
4848+ ) {
4949+ super();
5050+ }
5151+5252+ /**
5353+ * If all variants have a literal or enum for the discriminator property,
5454+ * and there are no overlapping values, returns a map of discriminator values
5555+ * to variants. Otherwise, returns null.
5656+ */
5757+ protected get variantsMap() {
5858+ const map = new Map<
5959+ unknown,
6060+ DiscriminatedUnionSchemaVariant<Discriminator>
6161+ >();
6262+ for (const variant of this.variants) {
6363+ const schema = variant.validators[this.discriminator];
6464+ if (schema instanceof LiteralSchema) {
6565+ if (map.has(schema.value)) return null; // overlapping value
6666+ map.set(schema.value, variant);
6767+ } else if (schema instanceof EnumSchema) {
6868+ for (const val of schema.values) {
6969+ if (map.has(val)) return null; // overlapping value
7070+ map.set(val, variant);
7171+ }
7272+ } else {
7373+ return null; // not a literal or enum
7474+ }
7575+ }
7676+7777+ // Cache the map on the instance (to avoid re-computing)
7878+ Object.defineProperty(this, "variantsMap", {
7979+ value: map,
8080+ writable: false,
8181+ enumerable: false,
8282+ configurable: true,
8383+ });
8484+8585+ return map;
8686+ }
8787+8888+ override validateInContext(
8989+ input: unknown,
9090+ ctx: ValidatorContext,
9191+ ): ValidationResult<DiscriminatedUnionSchemaOutput<Options>> {
9292+ if (!isPlainObject(input)) {
9393+ return ctx.issueInvalidType(input, "object");
9494+ }
9595+9696+ if (!Object.hasOwn(input, this.discriminator)) {
9797+ return ctx.issueRequiredKey(input, this.discriminator);
9898+ }
9999+100100+ // Fast path: if we have a mapping of discriminator values to variants,
101101+ // we can directly select the correct variant to validate against. This also
102102+ // outputs a better error (with a single failure issue) when the discriminator.
103103+ if (this.variantsMap) {
104104+ const variant = this.variantsMap.get(input[this.discriminator]);
105105+ if (!variant) {
106106+ return ctx.issueInvalidPropertyValue(input, this.discriminator, [
107107+ ...this.variantsMap.keys(),
108108+ ]);
109109+ }
110110+111111+ return ctx.validate(input, variant) as ValidationResult<
112112+ DiscriminatedUnionSchemaOutput<Options>
113113+ >;
114114+ }
115115+116116+ // Slow path: try validating against each variant and return the first
117117+ // successful one (or aggregate all failures if none match).
118118+ const failures: ValidationFailure[] = [];
119119+120120+ for (const variant of this.variants) {
121121+ const discSchema = variant.validators[this.discriminator];
122122+ const discResult = ctx.validateChild(
123123+ input,
124124+ this.discriminator,
125125+ discSchema,
126126+ );
127127+128128+ if (!discResult.success) {
129129+ failures.push(discResult);
130130+ continue;
131131+ }
132132+133133+ return ctx.validate(input, variant) as ValidationResult<
134134+ DiscriminatedUnionSchemaOutput<Options>
135135+ >;
136136+ }
137137+138138+ return {
139139+ success: false,
140140+ error: ValidationError.fromFailures(failures),
141141+ };
142142+ }
143143+}
+24
lex/schema/schema/enum.ts
···11+import {
22+ type ValidationResult,
33+ Validator,
44+ type ValidatorContext,
55+} from "../validation.ts";
66+77+export class EnumSchema<
88+ Output extends null | string | number | boolean = string,
99+> extends Validator<Output> {
1010+ constructor(readonly values: readonly Output[]) {
1111+ super();
1212+ }
1313+1414+ override validateInContext(
1515+ input: unknown,
1616+ ctx: ValidatorContext,
1717+ ): ValidationResult<Output> {
1818+ if (!(this.values as readonly unknown[]).includes(input)) {
1919+ return ctx.issueInvalidValue(input, this.values);
2020+ }
2121+2222+ return ctx.success(input as Output);
2323+ }
2424+}
+45
lex/schema/schema/integer.ts
···11+import {
22+ type ValidationResult,
33+ Validator,
44+ type ValidatorContext,
55+} from "../validation.ts";
66+77+export type IntegerSchemaOptions = {
88+ default?: number;
99+ minimum?: number;
1010+ maximum?: number;
1111+};
1212+1313+export class IntegerSchema extends Validator<number> {
1414+ override readonly lexiconType = "integer" as const;
1515+1616+ constructor(readonly options: IntegerSchemaOptions) {
1717+ super();
1818+ }
1919+2020+ override validateInContext(
2121+ input: unknown = this.options.default,
2222+ ctx: ValidatorContext,
2323+ ): ValidationResult<number> {
2424+ if (!isInteger(input)) {
2525+ return ctx.issueInvalidType(input, "integer");
2626+ }
2727+2828+ if (this.options.minimum !== undefined && input < this.options.minimum) {
2929+ return ctx.issueTooSmall(input, "integer", this.options.minimum, input);
3030+ }
3131+3232+ if (this.options.maximum !== undefined && input > this.options.maximum) {
3333+ return ctx.issueTooBig(input, "integer", this.options.maximum, input);
3434+ }
3535+3636+ return ctx.success(input);
3737+ }
3838+}
3939+4040+/**
4141+ * Simple wrapper around {@link Number.isInteger} that acts as a type guard.
4242+ */
4343+function isInteger(input: unknown): input is number {
4444+ return Number.isInteger(input);
4545+}
+56
lex/schema/schema/intersection.ts
···11+import {
22+ type Infer,
33+ type ValidationResult,
44+ Validator,
55+ type ValidatorContext,
66+} from "../validation.ts";
77+88+export type IntersectionSchemaValidators = readonly [
99+ Validator,
1010+ Validator,
1111+ ...Validator[],
1212+];
1313+export type IntersectionSchemaOutput<
1414+ V extends readonly Validator[],
1515+ Base = unknown,
1616+> = V extends readonly [
1717+ infer First extends Validator,
1818+ ...infer Rest extends Validator[],
1919+] ? IntersectionSchemaOutput<Rest, Base & Infer<First>>
2020+ : Base;
2121+2222+export class IntersectionSchema<
2323+ V extends IntersectionSchemaValidators = IntersectionSchemaValidators,
2424+> extends Validator<IntersectionSchemaOutput<V>> {
2525+ constructor(protected readonly validators: V) {
2626+ super();
2727+ }
2828+2929+ override validateInContext(
3030+ input: unknown,
3131+ ctx: ValidatorContext,
3232+ ): ValidationResult<IntersectionSchemaOutput<V>> {
3333+ for (let i = 0; i < this.validators.length; i++) {
3434+ const result = ctx.validate(input, this.validators[i]);
3535+3636+ if (!result.success) {
3737+ return result;
3838+ }
3939+4040+ // @NOTE because transforming the value could make it invalid for previous
4141+ // validators, we need to ensure the input remains unchanged only gets
4242+ // transformed by the first validator.
4343+ if (i !== 0 && input !== result.value) {
4444+ // The alternative would be to allow transforms on a first pass
4545+ // (ignoring errors) and then re-validate the final value against all
4646+ // validators (without allowing further transforms). This would be way
4747+ // less efficient (we could make this optional).
4848+ return ctx.issueInvalidValue(input, [result.value]);
4949+ }
5050+5151+ input = result.value;
5252+ }
5353+5454+ return ctx.success(input as IntersectionSchemaOutput<V>);
5555+ }
5656+}
···11+import { assertEquals } from "@std/assert";
22+import { BooleanSchema } from "../schema/boolean.ts";
33+import { DictSchema } from "../schema/dict.ts";
44+import { EnumSchema } from "../schema/enum.ts";
55+import { IntegerSchema } from "../schema/integer.ts";
66+import { ObjectSchema } from "../schema/object.ts";
77+import { StringSchema } from "../schema/string.ts";
88+99+const simpleSchema = new ObjectSchema(
1010+ {
1111+ name: new StringSchema({}),
1212+ age: new IntegerSchema({}),
1313+ gender: new EnumSchema(["male", "female"]),
1414+ },
1515+ {
1616+ required: ["name"],
1717+ nullable: ["gender"],
1818+ },
1919+);
2020+2121+Deno.test("ObjectSchema simple schema validates plain objects", () => {
2222+ const result = simpleSchema.validate({
2323+ name: "Alice",
2424+ age: 30,
2525+ gender: "female",
2626+ });
2727+ assertEquals(result.success, true);
2828+});
2929+3030+Deno.test("ObjectSchema simple schema rejects non-objects", () => {
3131+ const result = simpleSchema.validate("not an object");
3232+ assertEquals(result.success, false);
3333+});
3434+3535+Deno.test("ObjectSchema simple schema rejects missing properties", () => {
3636+ const result = simpleSchema.validate({
3737+ age: 30,
3838+ gender: "female",
3939+ });
4040+ assertEquals(result.success, false);
4141+});
4242+4343+Deno.test("ObjectSchema simple schema validates optional properties", () => {
4444+ const result = simpleSchema.validate({
4545+ name: "Alice",
4646+ });
4747+ assertEquals(result.success, true);
4848+});
4949+5050+Deno.test("ObjectSchema simple schema validates nullable properties", () => {
5151+ const result = simpleSchema.validate({
5252+ name: "Alice",
5353+ gender: null,
5454+ });
5555+ assertEquals(result.success, true);
5656+});
5757+5858+Deno.test("ObjectSchema simple schema rejects invalid property types", () => {
5959+ const result = simpleSchema.validate({
6060+ name: "Alice",
6161+ age: "thirty",
6262+ });
6363+ assertEquals(result.success, false);
6464+});
6565+6666+Deno.test("ObjectSchema simple schema ignores extra properties", () => {
6767+ const result = simpleSchema.validate({
6868+ name: "Alice",
6969+ age: 30,
7070+ extra: "value",
7171+ });
7272+ assertEquals(result.success, true);
7373+});
7474+7575+const strictSchema = new ObjectSchema(
7676+ {
7777+ id: new StringSchema({}),
7878+ score: new IntegerSchema({}),
7979+ },
8080+ {
8181+ required: ["id", "score"],
8282+ unknownProperties: "strict",
8383+ },
8484+);
8585+8686+Deno.test("ObjectSchema strict schema rejects extra properties", () => {
8787+ const result = strictSchema.validate({
8888+ id: "item1",
8989+ score: 100,
9090+ extra: "not allowed",
9191+ });
9292+ assertEquals(result.success, false);
9393+});
9494+9595+Deno.test("ObjectSchema strict schema accepts only defined properties", () => {
9696+ const result = strictSchema.validate({
9797+ id: "item1",
9898+ score: 100,
9999+ });
100100+ assertEquals(result.success, true);
101101+});
102102+103103+const unknownPropertiesSchema = new ObjectSchema(
104104+ {
105105+ title: new StringSchema({}),
106106+ },
107107+ {
108108+ required: ["title"],
109109+ unknownProperties: new DictSchema(
110110+ new EnumSchema(["tag1", "tag2"]),
111111+ new BooleanSchema({}),
112112+ ),
113113+ },
114114+);
115115+116116+Deno.test("schema with unknownProperties validator validates extra properties with the provided validator", () => {
117117+ const result = unknownPropertiesSchema.validate({
118118+ title: "My Post",
119119+ tag1: true,
120120+ tag2: false,
121121+ });
122122+ assertEquals(result.success, true);
123123+});
124124+125125+Deno.test("schema with unknownProperties rejects extra properties that fail the provided validator", () => {
126126+ const result = unknownPropertiesSchema.validate({
127127+ title: "My Post",
128128+ tag1: "not a boolean",
129129+ });
130130+ assertEquals(result.success, false);
131131+});
+43
lex/schema/util/array-agg.ts
···11+/**
22+ * Aggregates items in an array based on a comparison function and an aggregation function.
33+ *
44+ * @param arr - The input array to aggregate.
55+ * @param cmp - A comparison function that determines if two items belong to the same group.
66+ * @param agg - An aggregation function that combines items in a group into a single item.
77+ * @returns An array of aggregated items.
88+ * @example
99+ * ```ts
1010+ * const input = [1, 1, 2, 2, 3, 3, 3]
1111+ * const result = arrayAgg(
1212+ * input,
1313+ * (a, b) => a === b,
1414+ * (items) => { value: items[0], sum: items.reduce((sum, item) => sum + item, 0) },
1515+ * )
1616+ * // result is [{ value: 1, sum: 2 }, { value: 2, sum: 4 }, { value: 3, sum: 6 }]
1717+ * ```
1818+ */
1919+export function arrayAgg<T, O>(
2020+ arr: readonly T[],
2121+ cmp: (a: T, b: T) => boolean,
2222+ agg: (items: [T, ...T[]]) => O,
2323+): O[] {
2424+ if (arr.length === 0) return [];
2525+2626+ const groups: [T, ...T[]][] = [[arr[0]]];
2727+ const skipped = Array<undefined | boolean>(arr.length);
2828+2929+ outer: for (let i = 1; i < arr.length; i++) {
3030+ if (skipped[i]) continue;
3131+ const item = arr[i];
3232+ for (let j = 0; j < groups.length; j++) {
3333+ if (cmp(item, groups[j][0])) {
3434+ groups[j].push(item);
3535+ skipped[i] = true;
3636+ continue outer;
3737+ }
3838+ }
3939+ groups.push([item]);
4040+ }
4141+4242+ return groups.map(agg);
4343+}
+4
lex/schema/validation.ts
···11+export * from "./validation/property-key.ts";
22+export * from "./validation/validation-error.ts";
33+export * from "./validation/validation-issue.ts";
44+export * from "./validation/validator.ts";
···11+import {
22+ failure,
33+ type ResultFailure,
44+ type ResultSuccess,
55+ success,
66+} from "../core.ts";
77+import type { PropertyKey } from "./property-key.ts";
88+import { ValidationError } from "./validation-error.ts";
99+import type {
1010+ IssueTooBig,
1111+ IssueTooSmall,
1212+ ValidationIssue,
1313+} from "./validation-issue.ts";
1414+1515+export type ValidationSuccess<Value = unknown> = ResultSuccess<Value>;
1616+export type ValidationFailure = ResultFailure<ValidationError>;
1717+export type ValidationResult<Value = unknown> =
1818+ | ValidationSuccess<Value>
1919+ | ValidationFailure;
2020+2121+type ValidationOptions = {
2222+ path?: PropertyKey[];
2323+2424+ /** @default true */
2525+ allowTransform?: boolean;
2626+};
2727+2828+export type Infer<T extends Validator> = T["_lex"]["output"];
2929+3030+export abstract class Validator<Output = unknown> {
3131+ /**
3232+ * This property is used for type inference purposes and does not actually
3333+ * exist at runtime.
3434+ *
3535+ * @deprecated For internal use only (not actually deprecated)
3636+ */
3737+ _lex!: { output: Output };
3838+3939+ readonly lexiconType?: string;
4040+4141+ /**
4242+ * @internal **INTERNAL API, DO NOT USE**.
4343+ *
4444+ * Use {@link Validator.assert assert}, {@link Validator.check check},
4545+ * {@link Validator.parse parse} or {@link Validator.validate validate}
4646+ * instead.
4747+ *
4848+ * This method is implemented by subclasses to perform transformation and
4949+ * validation of the input value. Do not call this method directly; as the
5050+ * {@link ValidatorContext.options.allowTransform} option will **not** be
5151+ * enforced. See {@link ValidatorContext.validate} for details. When
5252+ * delegating validation from one validator sub-class implementation to
5353+ * another schema, {@link ValidatorContext.validate} should be used instead
5454+ * of calling {@link Validator.validateInContext}. This will allow to stop the
5555+ * validation process if the value was transformed (by the other schema) but
5656+ * transformations are not allowed.
5757+ *
5858+ * By convention, the {@link ValidationResult} must return the original input
5959+ * value if validation was successful and no transformation was applied (i.e.
6060+ * the input already conformed to the schema). If a default value, or any
6161+ * other transformation was applied, the returned value c&an be different from
6262+ * the input.
6363+ *
6464+ * This convention allows the {@link Validator.check check} and
6565+ * {@link Validator.assert assert} methods to check whether the input value
6666+ * exactly matches the schema (without defaults or transformations), by
6767+ * checking if the returned value is strictly equal to the input.
6868+ *
6969+ * @see {@link ValidatorContext.validate}
7070+ */
7171+ abstract validateInContext(
7272+ input: unknown,
7373+ ctx: ValidatorContext,
7474+ ): ValidationResult<Output>;
7575+7676+ assert(input: unknown): asserts input is Output {
7777+ const result = this.validate(input, { allowTransform: false });
7878+ if (!result.success) throw result.error;
7979+ }
8080+8181+ check(input: unknown): input is Output {
8282+ const result = this.validate(input, { allowTransform: false });
8383+ return result.success;
8484+ }
8585+8686+ maybe<I>(input: I): (I & Output) | undefined {
8787+ return this.check(input) ? input : undefined;
8888+ }
8989+9090+ parse<I>(
9191+ input: I,
9292+ options: ValidationOptions & { allowTransform: false },
9393+ ): I & Output;
9494+ parse(input: unknown, options?: ValidationOptions): Output;
9595+ parse(input: unknown, options?: ValidationOptions): Output {
9696+ const result = ValidatorContext.validate(input, this, options);
9797+ if (!result.success) throw result.error;
9898+ return result.value;
9999+ }
100100+101101+ validate<I>(
102102+ input: I,
103103+ options: ValidationOptions & { allowTransform: false },
104104+ ): ValidationResult<I & Output>;
105105+ validate(
106106+ input: unknown,
107107+ options?: ValidationOptions,
108108+ ): ValidationResult<Output>;
109109+ validate(
110110+ input: unknown,
111111+ options?: ValidationOptions,
112112+ ): ValidationResult<Output> {
113113+ return ValidatorContext.validate(input, this, options);
114114+ }
115115+116116+ // @NOTE The built lexicons namespaces will export utility functions that
117117+ // allow accessing the schema's methods without the need to specify ".main."
118118+ // as part of the namespace. This way, a utility for a particular record type
119119+ // can be called like "app.bsky.feed.post.<utility>()" instead of
120120+ // "app.bsky.feed.post.main.<utility>()". Because those utilities could
121121+ // conflict with other schemas (e.g. if there is a lexicon definition at
122122+ // "#<utility>"), those exported utilities will be prefixed with "$". In order
123123+ // to be able to consistently call the utilities, when using the "main" and
124124+ // non "main" definitions, we also expose the same methods with a "$" prefix.
125125+ // Thanks to this, both of the following call will be possible:
126126+ //
127127+ // - "app.bsky.feed.post.$parse(...)" // calls a utility function created by "lex build"
128128+ // - "app.bsky.feed.defs.postView.$parse(...)" // uses the alias defined below on the schema instance
129129+130130+ $assert(input: unknown): asserts input is Output {
131131+ return this.assert(input);
132132+ }
133133+134134+ $check(input: unknown): input is Output {
135135+ return this.check(input);
136136+ }
137137+138138+ $maybe<I>(input: I): (I & Output) | undefined {
139139+ return this.maybe(input);
140140+ }
141141+142142+ $parse(input: unknown, options?: ValidationOptions): Output {
143143+ return this.parse(input, options);
144144+ }
145145+146146+ $validate(
147147+ input: unknown,
148148+ options?: ValidationOptions,
149149+ ): ValidationResult<Output> {
150150+ return this.validate(input, options);
151151+ }
152152+}
153153+154154+export type ContextualIssue = {
155155+ [Code in ValidationIssue["code"]]:
156156+ & Omit<
157157+ Extract<ValidationIssue, { code: Code }>,
158158+ "path"
159159+ >
160160+ & { path?: PropertyKey | readonly PropertyKey[] };
161161+}[ValidationIssue["code"]];
162162+163163+const asIssue = (
164164+ { path, ...issue }: ContextualIssue,
165165+ currentPath: readonly PropertyKey[],
166166+): ValidationIssue & { path: PropertyKey[] } => ({
167167+ ...issue,
168168+ path: path != null ? currentPath.concat(path) : [...currentPath],
169169+});
170170+171171+export class ValidatorContext {
172172+ /**
173173+ * Creates a new validation context and validates the input using the
174174+ * provided validator.
175175+ */
176176+ static validate<V>(
177177+ input: unknown,
178178+ validator: Validator<V>,
179179+ options: ValidationOptions = {},
180180+ ): ValidationResult<V> {
181181+ const context = new ValidatorContext(options);
182182+ return context.validate(input, validator);
183183+ }
184184+185185+ private readonly currentPath: PropertyKey[];
186186+ private readonly issues: ValidationIssue[] = [];
187187+188188+ protected constructor(readonly options: ValidationOptions) {
189189+ // Create a copy because we will be mutating the array during validation.
190190+ this.currentPath = options?.path != null ? [...options.path] : [];
191191+ }
192192+193193+ get path() {
194194+ return [...this.currentPath];
195195+ }
196196+197197+ get allowTransform() {
198198+ // Default to true
199199+ return this.options?.allowTransform !== false;
200200+ }
201201+202202+ /**
203203+ * This is basically the entry point for validation within a context. Use this
204204+ * method instead of {@link Validator.validateInContext} directly, because
205205+ * this method enforces the {@link ValidationOptions.allowTransform} option.
206206+ */
207207+ validate<V>(input: unknown, validator: Validator<V>): ValidationResult<V> {
208208+ const result = validator.validateInContext(input, this);
209209+210210+ if (result.success) {
211211+ if (!this.allowTransform && !Object.is(result.value, input)) {
212212+ // If the value changed, it means that a default (or some other
213213+ // transformation) was applied, meaning that the original value did
214214+ // *not* match the (output) schema. When "allowTransform" is false, we
215215+ // consider this a failure.
216216+217217+ // This check is the reason why Validator.validateInContext should not
218218+ // be used directly, and ValidatorContext.validate should be used
219219+ // instead, even when delegating validation from one validator to
220220+ // another.
221221+222222+ // This if block comes before the next one because 'this.issues' will
223223+ // end-up being appended to the returned ValidationError (see the
224224+ // "failure" method below), resulting in a more complete error report.
225225+ return this.issueInvalidValue(input, [result.value]);
226226+ }
227227+228228+ if (this.issues.length > 0) {
229229+ // Validator returned a success but issues were added via the context.
230230+ // This means the overall validation failed.
231231+ return { success: false, error: new ValidationError(this.issues) };
232232+ }
233233+ }
234234+235235+ return result;
236236+ }
237237+238238+ validateChild<I extends object, K extends PropertyKey & keyof I, V>(
239239+ input: I,
240240+ key: K,
241241+ validator: Validator<V>,
242242+ ): ValidationResult<V> {
243243+ // Instead of creating a new context, we just push/pop the path segment.
244244+ this.currentPath.push(key);
245245+ try {
246246+ return this.validate(input[key], validator);
247247+ } finally {
248248+ this.currentPath.length--;
249249+ }
250250+ }
251251+252252+ addIssue(issue: ContextualIssue): void {
253253+ this.issues.push(asIssue(issue, this.currentPath));
254254+ }
255255+256256+ success<V>(value: V): ValidationResult<V> {
257257+ return success(value);
258258+ }
259259+260260+ failure(issue: ContextualIssue): ValidationFailure {
261261+ return failure(
262262+ new ValidationError([...this.issues, asIssue(issue, this.currentPath)]),
263263+ );
264264+ }
265265+266266+ issueInvalidValue(
267267+ input: unknown,
268268+ values: readonly unknown[],
269269+ path?: PropertyKey | readonly PropertyKey[],
270270+ ) {
271271+ return this.failure({
272272+ code: "invalid_value",
273273+ input,
274274+ values,
275275+ path,
276276+ });
277277+ }
278278+279279+ issueInvalidType(
280280+ input: unknown,
281281+ expected: string | readonly string[],
282282+ path?: PropertyKey | readonly PropertyKey[],
283283+ ) {
284284+ return this.failure({
285285+ code: "invalid_type",
286286+ input,
287287+ expected: Array.isArray(expected) ? expected : [expected],
288288+ path,
289289+ });
290290+ }
291291+292292+ issueInvalidPropertyValue<I>(
293293+ input: I,
294294+ property: keyof I & PropertyKey,
295295+ values: readonly unknown[],
296296+ ) {
297297+ return this.issueInvalidValue(input[property], values, property);
298298+ }
299299+300300+ issueInvalidPropertyType<I>(
301301+ input: I,
302302+ property: keyof I & PropertyKey,
303303+ expected: string | readonly string[],
304304+ ) {
305305+ return this.issueInvalidType(input[property], expected, property);
306306+ }
307307+308308+ issueRequiredKey(input: object, key: PropertyKey) {
309309+ return this.failure({
310310+ code: "required_key",
311311+ key,
312312+ input,
313313+ path: key,
314314+ });
315315+ }
316316+317317+ issueInvalidFormat(input: unknown, format: string, message?: string) {
318318+ return this.failure({
319319+ code: "invalid_format",
320320+ message,
321321+ format,
322322+ input,
323323+ });
324324+ }
325325+326326+ issueTooBig(
327327+ input: unknown,
328328+ type: IssueTooBig["type"],
329329+ maximum: number,
330330+ actual: number,
331331+ ) {
332332+ return this.failure({
333333+ code: "too_big",
334334+ type,
335335+ maximum,
336336+ actual,
337337+ input,
338338+ });
339339+ }
340340+341341+ issueTooSmall(
342342+ input: unknown,
343343+ type: IssueTooSmall["type"],
344344+ minimum: number,
345345+ actual: number,
346346+ ) {
347347+ return this.failure({
348348+ code: "too_small",
349349+ type,
350350+ minimum,
351351+ actual,
352352+ input,
353353+ });
354354+ }
355355+356356+ custom(
357357+ input: unknown,
358358+ message: string,
359359+ path?: PropertyKey | readonly PropertyKey[],
360360+ ) {
361361+ return this.failure({
362362+ code: "custom",
363363+ input,
364364+ message,
365365+ path,
366366+ });
367367+ }
368368+}