···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-}