Suite of AT Protocol TypeScript libraries built on web standards
21
fork

Configure Feed

Select the types of activity you want to include in your feed.

legacy lexgen improvements and remove new lex for now

+192 -4559
+3
.vscode/settings.json
··· 1 + { 2 + "git.enabled": false 3 + }
-99
data/blob.ts
··· 1 - import { CID, RAW_BIN_MULTICODEC, SHA2_256_MULTIHASH_CODE } from "./cid.ts"; 2 - import { isPlainObject } from "./object.ts"; 3 - 4 - export type BlobRef = { 5 - $type: "blob"; 6 - mimeType: string; 7 - ref: CID; 8 - size: number; 9 - }; 10 - 11 - export function isBlobRef( 12 - input: unknown, 13 - options?: { strict?: boolean }, 14 - ): input is BlobRef { 15 - if (!isPlainObject(input)) { 16 - return false; 17 - } 18 - 19 - if (input?.$type !== "blob") { 20 - return false; 21 - } 22 - 23 - const { mimeType, size, ref } = input; 24 - if (typeof mimeType !== "string") { 25 - return false; 26 - } 27 - 28 - if (typeof size !== "number" || size < 0 || !Number.isInteger(size)) { 29 - return false; 30 - } 31 - 32 - if (typeof ref !== "object" || ref === null) { 33 - return false; 34 - } 35 - 36 - for (const key in input) { 37 - if ( 38 - key !== "$type" && 39 - key !== "mimeType" && 40 - key !== "ref" && 41 - key !== "size" 42 - ) { 43 - return false; 44 - } 45 - } 46 - 47 - const cid = CID.asCID(ref); 48 - if (!cid) { 49 - return false; 50 - } 51 - 52 - if (options?.strict) { 53 - if (cid.version !== 1) { 54 - return false; 55 - } 56 - if (cid.code !== RAW_BIN_MULTICODEC) { 57 - return false; 58 - } 59 - if (cid.multihash.code !== SHA2_256_MULTIHASH_CODE) { 60 - return false; 61 - } 62 - } 63 - 64 - return true; 65 - } 66 - 67 - export type LegacyBlobRef = { 68 - cid: string; 69 - mimeType: string; 70 - }; 71 - 72 - export function isLegacyBlobRef(input: unknown): input is LegacyBlobRef { 73 - if (!isPlainObject(input)) { 74 - return false; 75 - } 76 - 77 - const { cid, mimeType } = input; 78 - if (typeof cid !== "string") { 79 - return false; 80 - } 81 - 82 - if (typeof mimeType !== "string") { 83 - return false; 84 - } 85 - 86 - for (const key in input) { 87 - if (key !== "cid" && key !== "mimeType") { 88 - return false; 89 - } 90 - } 91 - 92 - try { 93 - CID.parse(cid); 94 - } catch { 95 - return false; 96 - } 97 - 98 - return true; 99 - }
-50
data/cid.ts
··· 1 - import { CID } from "multiformats/cid"; 2 - 3 - export const DAG_CBOR_MULTICODEC = 0x71; 4 - export const RAW_BIN_MULTICODEC = 0x55; 5 - 6 - export const SHA2_256_MULTIHASH_CODE = 0x12; 7 - 8 - export { CID }; 9 - 10 - export function isCid( 11 - value: unknown, 12 - options?: { strict?: boolean }, 13 - ): value is CID { 14 - const cid = CID.asCID(value); 15 - if (!cid) { 16 - return false; 17 - } 18 - 19 - if (options?.strict) { 20 - if (cid.version !== 1) { 21 - return false; 22 - } 23 - if (cid.code !== RAW_BIN_MULTICODEC && cid.code !== DAG_CBOR_MULTICODEC) { 24 - return false; 25 - } 26 - if (cid.multihash.code !== SHA2_256_MULTIHASH_CODE) { 27 - return false; 28 - } 29 - } 30 - 31 - return true; 32 - } 33 - 34 - export function validateCidString(input: string): boolean { 35 - return parseCidString(input)?.toString() === input; 36 - } 37 - 38 - export function parseCidString(input: string): CID | undefined { 39 - try { 40 - return CID.parse(input); 41 - } catch { 42 - return undefined; 43 - } 44 - } 45 - 46 - export function ensureValidCidString(input: string): void { 47 - if (!validateCidString(input)) { 48 - throw new Error(`Invalid CID string`); 49 - } 50 - }
-9
data/deno.json
··· 1 - { 2 - "name": "@atp/data", 3 - "version": "0.1.0-alpha.1", 4 - "exports": "./mod.ts", 5 - "license": "MIT", 6 - "imports": { 7 - "multiformats": "npm:multiformats@^13.4.1" 8 - } 9 - }
-40
data/language.ts
··· 1 - const BCP47_REGEXP = 2 - /^((?<grandfathered>(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?<language>([A-Za-z]{2,3}(-(?<extlang>[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-(?<script>[A-Za-z]{4}))?(-(?<region>[A-Za-z]{2}|[0-9]{3}))?(-(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-(?<extension>[0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(?<privateUseA>x(-[A-Za-z0-9]{1,8})+))?)|(?<privateUseB>x(-[A-Za-z0-9]{1,8})+))$/; 3 - 4 - export type LanguageTag = { 5 - grandfathered?: string; 6 - language?: string; 7 - extlang?: string; 8 - script?: string; 9 - region?: string; 10 - variant?: string; 11 - extension?: string; 12 - privateUse?: string; 13 - }; 14 - 15 - export function parseLanguage(input: string): LanguageTag | null { 16 - const parsed = input.match(BCP47_REGEXP); 17 - if (!parsed?.groups) return null; 18 - 19 - const { groups } = parsed; 20 - const result: LanguageTag = {}; 21 - if (groups.grandfathered) result.grandfathered = groups.grandfathered; 22 - if (groups.language) result.language = groups.language; 23 - if (groups.extlang) result.extlang = groups.extlang; 24 - if (groups.script) result.script = groups.script; 25 - if (groups.region) result.region = groups.region; 26 - if (groups.variant) result.variant = groups.variant; 27 - if (groups.extension) result.extension = groups.extension; 28 - const privateUse = groups.privateUseA || groups.privateUseB; 29 - if (privateUse) result.privateUse = privateUse; 30 - return result; 31 - } 32 - 33 - /** 34 - * Validates well-formed BCP 47 syntax 35 - * 36 - * @see {@link https://www.rfc-editor.org/rfc/rfc5646.html#section-2.1} 37 - */ 38 - export function isLanguage(input: string): boolean { 39 - return BCP47_REGEXP.test(input); 40 - }
-160
data/lex.ts
··· 1 - import { CID, isCid } from "./cid.ts"; 2 - import { isPlainObject } from "./object.ts"; 3 - import { ui8Equals } from "./uint8array.ts"; 4 - 5 - // @NOTE BlobRef is just a special case of LexMap. 6 - 7 - export type LexScalar = number | string | boolean | null | CID | Uint8Array; 8 - export type LexValue = LexScalar | LexValue[] | { [_ in string]?: LexValue }; 9 - export type LexMap = { [_ in string]?: LexValue }; 10 - export type LexArray = LexValue[]; 11 - 12 - export function isLexMap(value: unknown): value is LexMap { 13 - if (!isPlainObject(value)) return false; 14 - for (const key in value) { 15 - if (!isLexValue(value[key])) return false; 16 - } 17 - return true; 18 - } 19 - 20 - export function isLexArray(value: unknown): value is LexArray { 21 - if (!Array.isArray(value)) return false; 22 - for (let i = 0; i < value.length; i++) { 23 - if (!isLexValue(value[i])) return false; 24 - } 25 - return true; 26 - } 27 - 28 - export function isLexScalar(value: unknown): value is LexScalar { 29 - switch (typeof value) { 30 - case "object": 31 - if (value === null) return true; 32 - return value instanceof Uint8Array || isCid(value); 33 - case "string": 34 - case "boolean": 35 - return true; 36 - case "number": 37 - if (Number.isInteger(value)) return true; 38 - throw new TypeError(`Invalid Lex value: ${value}`); 39 - default: 40 - throw new TypeError(`Invalid Lex value: ${typeof value}`); 41 - } 42 - } 43 - 44 - export function isLexValue(value: unknown): value is LexValue { 45 - switch (typeof value) { 46 - case "number": 47 - if (!Number.isInteger(value)) return false; 48 - // fallthrough 49 - case "string": 50 - case "boolean": 51 - return true; 52 - case "object": 53 - if (value === null) return true; 54 - if (Array.isArray(value)) { 55 - for (let i = 0; i < value.length; i++) { 56 - if (!isLexValue(value[i])) return false; 57 - } 58 - return true; 59 - } 60 - if (isPlainObject(value)) { 61 - for (const key in value) { 62 - if (!isLexValue(value[key])) return false; 63 - } 64 - return true; 65 - } 66 - if (value instanceof Uint8Array) return true; 67 - if (isCid(value)) return true; 68 - // fallthrough 69 - default: 70 - return false; 71 - } 72 - } 73 - 74 - export type TypedLexMap = LexMap & { $type: string }; 75 - export function isTypedLexMap(value: LexValue): value is TypedLexMap { 76 - return ( 77 - isLexMap(value) && typeof value.$type === "string" && value.$type.length > 0 78 - ); 79 - } 80 - 81 - export function lexEquals(a: LexValue, b: LexValue): boolean { 82 - if (Object.is(a, b)) { 83 - return true; 84 - } 85 - 86 - if ( 87 - a == null || 88 - b == null || 89 - typeof a !== "object" || 90 - typeof b !== "object" 91 - ) { 92 - return false; 93 - } 94 - 95 - if (Array.isArray(a)) { 96 - if (!Array.isArray(b)) { 97 - return false; 98 - } 99 - if (a.length !== b.length) { 100 - return false; 101 - } 102 - for (let i = 0; i < a.length; i++) { 103 - if (!lexEquals(a[i], b[i])) { 104 - return false; 105 - } 106 - } 107 - return true; 108 - } else if (Array.isArray(b)) { 109 - return false; 110 - } 111 - 112 - if (ArrayBuffer.isView(a)) { 113 - if (!ArrayBuffer.isView(b)) return false; 114 - return ui8Equals(a as Uint8Array, b as Uint8Array); 115 - } else if (ArrayBuffer.isView(b)) { 116 - return false; 117 - } 118 - 119 - if (isCid(a)) { 120 - // @NOTE CID.equals returns its argument when it is falsy (e.g. null or 121 - // undefined) so we need to explicitly check that the output is "true". 122 - return CID.asCID(a)!.equals(CID.asCID(b)) === true; 123 - } else if (isCid(b)) { 124 - return false; 125 - } 126 - 127 - if (!isPlainObject(a) || !isPlainObject(b)) { 128 - // Foolproof (should never happen) 129 - throw new TypeError( 130 - "Invalid LexValue (expected CID, Uint8Array, or LexMap)", 131 - ); 132 - } 133 - 134 - const aKeys = Object.keys(a); 135 - const bKeys = Object.keys(b); 136 - 137 - if (aKeys.length !== bKeys.length) { 138 - return false; 139 - } 140 - 141 - for (const key of aKeys) { 142 - const aVal = a[key]; 143 - const bVal = b[key]; 144 - 145 - // Needed because of the optional index signature in the Lex object type 146 - // though, in practice, aVal should never be undefined here. 147 - if (aVal === undefined) { 148 - if (bVal === undefined && bKeys.includes(key)) continue; 149 - return false; 150 - } else if (bVal === undefined) { 151 - return false; 152 - } 153 - 154 - if (!lexEquals(aVal, bVal)) { 155 - return false; 156 - } 157 - } 158 - 159 - return true; 160 - }
-7
data/mod.ts
··· 1 - export * from "./blob.ts"; 2 - export * from "./cid.ts"; 3 - export * from "./language.ts"; 4 - export * from "./lex.ts"; 5 - export * from "./object.ts"; 6 - export * from "./uint8array.ts"; 7 - export * from "./utf8.ts";
-21
data/object.ts
··· 1 - export function isObject(input: unknown): input is object { 2 - return input != null && typeof input === "object"; 3 - } 4 - 5 - const ObjectProto = Object.prototype; 6 - const ObjectToString = Object.prototype.toString; 7 - 8 - export function isPlainObject( 9 - input: unknown, 10 - ): input is object & Record<string, unknown> { 11 - if (!input || typeof input !== "object") return false; 12 - const proto = Object.getPrototypeOf(input); 13 - if (proto === null) return true; 14 - return ( 15 - (proto === ObjectProto || 16 - // Needed to support NodeJS's `runInNewContext` which produces objects 17 - // with a different prototype 18 - Object.getPrototypeOf(proto) === null) && 19 - ObjectToString.call(input) === "[object Object]" 20 - ); 21 - }
-183
data/tests/blob_test.ts
··· 1 - import { assert, assertEquals, assertFalse } from "@std/assert"; 2 - import { isBlobRef, isLegacyBlobRef } from "../blob.ts"; 3 - import { CID } from "../cid.ts"; 4 - 5 - // await cidForRawBytes(Buffer.from('Hello, World!')) 6 - const blobCid = CID.parse( 7 - "bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4", 8 - ); 9 - // await cidForLex(Buffer.from('Hello, World!')) 10 - const lexCid = CID.parse( 11 - "bafyreic52vzks7wdklat4evp3vimohl55i2unzqpshz2ytka5omzr7exdy", 12 - ); 13 - 14 - Deno.test("isBlobRef tests valid blobCid and lexCid", () => { 15 - assertEquals(blobCid.code, 0x55); // raw 16 - assertEquals(blobCid.multihash.code, 0x12); // sha2-256 17 - assertEquals(lexCid.code, 0x71); // dag-cbor 18 - assertEquals(lexCid.multihash.code, 0x12); // sha2-256 19 - }); 20 - 21 - Deno.test("isBlobRef parses valid blob", () => { 22 - assert( 23 - isBlobRef({ 24 - $type: "blob", 25 - ref: blobCid, 26 - mimeType: "image/jpeg", 27 - size: 10000, 28 - }), 29 - ); 30 - 31 - assert( 32 - isBlobRef( 33 - { 34 - $type: "blob", 35 - ref: lexCid, 36 - mimeType: "image/jpeg", 37 - size: 10000, 38 - }, 39 - // In non-strict mode, any CID should be accepted 40 - { strict: false }, 41 - ), 42 - ); 43 - }); 44 - 45 - Deno.test("isBlobRef rejects invalid inputs", () => { 46 - assertFalse( 47 - isBlobRef({ 48 - $type: "blob", 49 - ref: { $link: blobCid.toString() }, 50 - mimeType: "image/jpeg", 51 - size: "10000", 52 - }), 53 - ); 54 - assertFalse( 55 - isBlobRef( 56 - { 57 - $type: "blob", 58 - ref: { $link: blobCid.toString() }, 59 - mimeType: "image/jpeg", 60 - size: "10000", 61 - }, 62 - { strict: true }, 63 - ), 64 - ); 65 - 66 - assertFalse( 67 - isBlobRef({ 68 - $type: "blob", 69 - mimeType: "image/jpeg", 70 - size: 10000, 71 - }), 72 - ); 73 - 74 - assertFalse( 75 - isBlobRef( 76 - { 77 - $type: "blob", 78 - mimeType: "image/jpeg", 79 - size: 10000, 80 - }, 81 - { strict: true }, 82 - ), 83 - ); 84 - }); 85 - 86 - Deno.test("isBlobRef rejects invalid CID/multihash code", () => { 87 - assert( 88 - isBlobRef( 89 - { 90 - $type: "blob", 91 - ref: blobCid, 92 - mimeType: "image/jpeg", 93 - size: 10000, 94 - }, 95 - { strict: true }, 96 - ), 97 - ); 98 - 99 - assertFalse( 100 - isBlobRef( 101 - { 102 - $type: "blob", 103 - ref: lexCid, 104 - mimeType: "image/jpeg", 105 - size: 10000, 106 - }, 107 - { strict: true }, 108 - ), 109 - ); 110 - }); 111 - 112 - Deno.test("isBlobRef rejects extra keys", () => { 113 - assertFalse( 114 - isBlobRef({ 115 - $type: "blob", 116 - ref: blobCid, 117 - mimeType: "image/jpeg", 118 - size: 10000, 119 - extra: "not allowed", 120 - }), 121 - ); 122 - 123 - assertFalse( 124 - isBlobRef( 125 - { 126 - $type: "blob", 127 - ref: blobCid, 128 - mimeType: "image/jpeg", 129 - size: 10000, 130 - extra: "not allowed", 131 - }, 132 - { strict: true }, 133 - ), 134 - ); 135 - }); 136 - 137 - Deno.test("isLegacyBlobRef parses valid legacy blob", () => { 138 - assert( 139 - isLegacyBlobRef({ 140 - cid: blobCid.toString(), 141 - mimeType: "image/jpeg", 142 - }), 143 - ); 144 - 145 - assert( 146 - isLegacyBlobRef({ 147 - cid: lexCid.toString(), 148 - mimeType: "image/jpeg", 149 - }), 150 - ); 151 - }); 152 - 153 - Deno.test("isLegacyBlobRef rejects invalid inputs", () => { 154 - assertFalse( 155 - isLegacyBlobRef({ 156 - cid: "babbaaa", 157 - mimeType: "image/jpeg", 158 - }), 159 - ); 160 - 161 - assertFalse( 162 - isLegacyBlobRef({ 163 - cid: 12345, 164 - mimeType: "image/jpeg", 165 - }), 166 - ); 167 - 168 - assertFalse( 169 - isLegacyBlobRef({ 170 - mimeType: "image/jpeg", 171 - }), 172 - ); 173 - }); 174 - 175 - Deno.test("isLegacyBlobRef rejects extra keys", () => { 176 - assertFalse( 177 - isLegacyBlobRef({ 178 - cid: blobCid.toString(), 179 - mimeType: "image/jpeg", 180 - extra: "not allowed", 181 - }), 182 - ); 183 - });
-80
data/tests/language_test.ts
··· 1 - import { assert, assertEquals, assertFalse } from "@std/assert"; 2 - import { isLanguage, parseLanguage } from "../language.ts"; 3 - 4 - Deno.test("languages validates BCP 47", () => { 5 - // valid 6 - assert(isLanguage("de")); 7 - assert(isLanguage("de-CH")); 8 - assert(isLanguage("de-DE-1901")); 9 - assert(isLanguage("es-419")); 10 - assert(isLanguage("sl-IT-nedis")); 11 - assert(isLanguage("mn-Cyrl-MN")); 12 - assert(isLanguage("x-fr-CH")); 13 - assert(isLanguage("en-GB-boont-r-extended-sequence-x-private")); 14 - assert(isLanguage("sr-Cyrl")); 15 - assert(isLanguage("hy-Latn-IT-arevela")); 16 - assert(isLanguage("i-klingon")); 17 - // invalid 18 - assertFalse(isLanguage("")); 19 - assertFalse(isLanguage("x")); 20 - assertFalse(isLanguage("de-CH-")); 21 - assertFalse(isLanguage("i-bad-grandfathered")); 22 - }); 23 - 24 - Deno.test("languages parses BCP 47", () => { 25 - // valid 26 - assertEquals(parseLanguage("de"), { 27 - language: "de", 28 - }); 29 - assertEquals(parseLanguage("de-CH"), { 30 - language: "de", 31 - region: "CH", 32 - }); 33 - assertEquals(parseLanguage("de-DE-1901"), { 34 - language: "de", 35 - region: "DE", 36 - variant: "1901", 37 - }); 38 - assertEquals(parseLanguage("es-419"), { 39 - language: "es", 40 - region: "419", 41 - }); 42 - assertEquals(parseLanguage("sl-IT-nedis"), { 43 - language: "sl", 44 - region: "IT", 45 - variant: "nedis", 46 - }); 47 - assertEquals(parseLanguage("mn-Cyrl-MN"), { 48 - language: "mn", 49 - script: "Cyrl", 50 - region: "MN", 51 - }); 52 - assertEquals(parseLanguage("x-fr-CH"), { 53 - privateUse: "x-fr-CH", 54 - }); 55 - assertEquals(parseLanguage("en-GB-boont-r-extended-sequence-x-private"), { 56 - language: "en", 57 - region: "GB", 58 - variant: "boont", 59 - extension: "r-extended-sequence", 60 - privateUse: "x-private", 61 - }); 62 - assertEquals(parseLanguage("sr-Cyrl"), { 63 - language: "sr", 64 - script: "Cyrl", 65 - }); 66 - assertEquals(parseLanguage("hy-Latn-IT-arevela"), { 67 - language: "hy", 68 - script: "Latn", 69 - region: "IT", 70 - variant: "arevela", 71 - }); 72 - assertEquals(parseLanguage("i-klingon"), { 73 - grandfathered: "i-klingon", 74 - }); 75 - // invalid 76 - assertEquals(parseLanguage(""), null); 77 - assertEquals(parseLanguage("x"), null); 78 - assertEquals(parseLanguage("de-CH-"), null); 79 - assertEquals(parseLanguage("i-bad-grandfathered"), null); 80 - });
-150
data/tests/lex-equals_test.ts
··· 1 - import { assertEquals } from "@std/assert"; 2 - import { CID } from "../cid.ts"; 3 - import { lexEquals } from "../lex.ts"; 4 - import type { LexValue } from "../lex.ts"; 5 - import { assertThrows } from "@std/assert/throws"; 6 - 7 - function expectLexEqual(a: LexValue, b: LexValue, expected: boolean) { 8 - assertEquals(lexEquals(a, b), expected); 9 - assertEquals(lexEquals(b, a), expected); 10 - } 11 - 12 - Deno.test("lexEquals compares primitive values", () => { 13 - expectLexEqual(null, null, true); 14 - expectLexEqual(true, true, true); 15 - expectLexEqual(false, false, true); 16 - expectLexEqual(42, 42, true); 17 - expectLexEqual("hello", "hello", true); 18 - 19 - expectLexEqual(null, false, false); 20 - expectLexEqual(false, null, false); 21 - expectLexEqual(true, false, false); 22 - expectLexEqual(false, true, false); 23 - expectLexEqual(42, 43, false); 24 - expectLexEqual("hello", "world", false); 25 - }); 26 - 27 - Deno.test("lexEquals compares NaN and Infinity correctly", () => { 28 - expectLexEqual(NaN, NaN, true); 29 - expectLexEqual(Infinity, Infinity, true); 30 - expectLexEqual(-Infinity, -Infinity, true); 31 - 32 - expectLexEqual(NaN, 0, false); 33 - expectLexEqual(NaN, null, false); 34 - expectLexEqual(Infinity, -Infinity, false); 35 - }); 36 - 37 - Deno.test("lexEquals compares arrays", () => { 38 - expectLexEqual([1, 2, 3], [1, 2, 3], true); 39 - expectLexEqual([1, 2, 3], [1, 2, 4], false); 40 - expectLexEqual([1, 2, 3], [1, 2], false); 41 - expectLexEqual([1, 2, 3], "not an array", false); 42 - }); 43 - 44 - Deno.test("lexEquals compares Uint8Arrays", () => { 45 - expectLexEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]), true); 46 - expectLexEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4]), false); 47 - expectLexEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2]), false); 48 - expectLexEqual(new Uint8Array([1, 2, 3]), "not a Uint8Array", false); 49 - }); 50 - 51 - Deno.test("lexEquals compares CIDs", () => { 52 - const cid1 = CID.parse( 53 - "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 54 - ); 55 - const cid2 = CID.parse(cid1.toString()); 56 - const cid3 = CID.parse(cid1.toString()); 57 - 58 - expectLexEqual(cid1, cid2, true); 59 - expectLexEqual(cid1, cid3, true); 60 - expectLexEqual(cid2, cid3, true); 61 - 62 - expectLexEqual(cid1, cid1.toString(), false); 63 - }); 64 - 65 - Deno.test("lexEquals compares objects", () => { 66 - expectLexEqual({ a: 1, b: 2 }, { a: 1, b: 2 }, true); 67 - expectLexEqual( 68 - { a: 1, b: { unicode: "a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧" } }, 69 - { a: 1, b: { unicode: "a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧" } }, 70 - true, 71 - ); 72 - 73 - expectLexEqual({ a: 1, b: 2 }, { a: 1, b: 3 }, false); 74 - expectLexEqual({ a: 1, b: 2 }, { a: 1 }, false); 75 - expectLexEqual({ a: 1, b: 2 }, "not an object", false); 76 - expectLexEqual({ a: 1, b: 2 }, null, false); 77 - }); 78 - 79 - Deno.test("lexEquals compares nested structures", () => { 80 - const lex1 = { 81 - foo: [1, 2, { bar: new Uint8Array([3, 4, 5]) }], 82 - baz: CID.parse( 83 - "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 84 - ), 85 - }; 86 - const lex2 = { 87 - foo: [1, 2, { bar: new Uint8Array([3, 4, 5]) }], 88 - baz: CID.parse( 89 - "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 90 - ), 91 - }; 92 - const lex3 = { 93 - foo: [1, 2, { bar: new Uint8Array([3, 4, 5 + 1]) }], 94 - baz: CID.parse( 95 - "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 96 - ), 97 - }; 98 - 99 - expectLexEqual(lex1, lex2, true); 100 - expectLexEqual(lex1, lex3, false); 101 - expectLexEqual(lex2, lex3, false); 102 - }); 103 - 104 - Deno.test("lexEquals allows comparing invalid numbers (floats, NaN, Infinity)", () => { 105 - expectLexEqual(3.14, 2.71, false); 106 - expectLexEqual(NaN, 0, false); 107 - expectLexEqual(Infinity, -Infinity, false); 108 - }); 109 - 110 - for (const value of [3.14, NaN, Infinity, -Infinity]) { 111 - Deno.test(`reference equality returns true for identical references of ${String(value)}`, () => { 112 - expectLexEqual(value, value, true); 113 - expectLexEqual([value], [value], true); 114 - expectLexEqual({ foo: value }, { foo: value }, true); 115 - expectLexEqual([{ foo: value }], [{ foo: value }], true); 116 - }); 117 - } 118 - 119 - Deno.test("lexEquals returns true for identical references", () => { 120 - const arr = [1, 2, 3]; 121 - expectLexEqual(arr, arr, true); 122 - 123 - const obj = { a: 1, b: 2 }; 124 - expectLexEqual(obj, obj, true); 125 - 126 - const u8 = new Uint8Array([1, 2, 3]); 127 - expectLexEqual(u8, u8, true); 128 - 129 - const cid = CID.parse( 130 - "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 131 - ); 132 - expectLexEqual(cid, cid, true); 133 - }); 134 - 135 - Deno.test("throws when comparing plain object with non-allowed class instance", () => { 136 - // @ts-expect-error should throw 137 - assertThrows(() => lexEquals({}, new Map())); 138 - // @ts-expect-error should throw 139 - assertThrows(() => lexEquals(new Map(), {})); 140 - // @ts-expect-error should throw 141 - assertThrows(() => lexEquals({ foo: {} }, { foo: new Map() })); 142 - // @ts-expect-error should throw 143 - assertThrows(() => lexEquals({ foo: new Map() }, { foo: {} })); 144 - 145 - assertThrows(() => lexEquals({ foo: {} }, { foo: new (class {})() })); 146 - 147 - assertThrows(() => lexEquals({ foo: new (class {})() }, { foo: {} })); 148 - 149 - assertThrows(() => lexEquals({ foo: {} }, { foo: new (class Object {})() })); 150 - });
-122
data/tests/lex_test.ts
··· 1 - import { assert } from "@std/assert"; 2 - import { isTypedLexMap } from "../lex.ts"; 3 - import { assertFalse } from "@std/assert/false"; 4 - 5 - Deno.test("isLexMap returns true for valid LexMap", () => { 6 - const record = { 7 - a: 123, 8 - b: "blah", 9 - c: true, 10 - d: null, 11 - e: new Uint8Array([1, 2, 3]), 12 - f: { 13 - nested: "value", 14 - }, 15 - g: [1, 2, 3], 16 - }; 17 - assertFalse(isTypedLexMap(record)); 18 - }); 19 - 20 - Deno.test("isLexMap returns false for non-records", () => { 21 - const values = [ 22 - 123, 23 - "blah", 24 - true, 25 - null, 26 - new Uint8Array([1, 2, 3]), 27 - [1, 2, 3], 28 - ]; 29 - for (const value of values) { 30 - assertFalse(isTypedLexMap(value)); 31 - } 32 - }); 33 - 34 - Deno.test("isLexMap returns false for records with non-Lex values", () => { 35 - assertFalse( 36 - // @ts-expect-error value passed is not a LexMap 37 - isTypedLexMap({ 38 - a: 123, 39 - b: () => {}, 40 - }), 41 - ); 42 - assertFalse( 43 - isTypedLexMap({ 44 - a: 123, 45 - b: undefined, 46 - }), 47 - ); 48 - }); 49 - 50 - Deno.test("isTypedLexMap valid records", () => { 51 - for ( 52 - const { json } of [ 53 - { 54 - note: "trivial record", 55 - json: { 56 - $type: "com.example.blah", 57 - a: 123, 58 - b: "blah", 59 - }, 60 - }, 61 - { 62 - note: "float, but integer-like", 63 - json: { 64 - $type: "com.example.blah", 65 - a: 123.0, 66 - b: "blah", 67 - }, 68 - }, 69 - { 70 - note: "empty list and object", 71 - json: { 72 - $type: "com.example.blah", 73 - a: [], 74 - b: {}, 75 - }, 76 - }, 77 - ] 78 - ) { 79 - assert(isTypedLexMap(json)); 80 - } 81 - }); 82 - 83 - Deno.test("isTypedLexMap invalid records", () => { 84 - for ( 85 - const { json } of [ 86 - { 87 - note: "float", 88 - json: { 89 - $type: "com.example.blah", 90 - a: 123.456, 91 - b: "blah", 92 - }, 93 - }, 94 - { 95 - note: "record with $type null", 96 - json: { 97 - $type: null, 98 - a: 123, 99 - b: "blah", 100 - }, 101 - }, 102 - { 103 - note: "record with $type wrong type", 104 - json: { 105 - $type: 123, 106 - a: 123, 107 - b: "blah", 108 - }, 109 - }, 110 - { 111 - note: "record with empty $type string", 112 - json: { 113 - $type: "", 114 - a: 123, 115 - b: "blah", 116 - }, 117 - }, 118 - ] 119 - ) { 120 - assertFalse(isTypedLexMap(json)); 121 - } 122 - });
-76
data/tests/object_test.ts
··· 1 - import { assert, assertFalse } from "@std/assert"; 2 - import { CID } from "../cid.ts"; 3 - import { isObject, isPlainObject } from "../object.ts"; 4 - 5 - Deno.test("isObject returns true for plain objects", () => { 6 - assert(isObject({})); 7 - assert(isObject({ a: 1 })); 8 - }); 9 - 10 - Deno.test("isObject returns true for CIDs", () => { 11 - const cid = CID.parse( 12 - "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 13 - ); 14 - assert(isObject(cid)); 15 - }); 16 - 17 - Deno.test("isObject returns true for class instances", () => { 18 - class MyClass {} 19 - assert(isObject(new MyClass())); 20 - }); 21 - 22 - Deno.test("isObject returns true for arrays", () => { 23 - assert(isObject([])); 24 - assert(isObject([1, 2, 3])); 25 - }); 26 - 27 - Deno.test("isObject returns false for null", () => { 28 - assertFalse(isObject(null)); 29 - }); 30 - 31 - Deno.test("isObject returns false for non-objects", () => { 32 - assertFalse(isObject(42)); 33 - assertFalse(isObject("string")); 34 - assertFalse(isObject(undefined)); 35 - assertFalse(isObject(true)); 36 - }); 37 - 38 - Deno.test("isPlainObject returns true for plain objects", () => { 39 - assert(isPlainObject({})); 40 - assert(isPlainObject({ a: 1 })); 41 - }); 42 - 43 - Deno.test("isPlainObject returns true for objects with null prototype", () => { 44 - const obj = Object.create(null); 45 - obj.a = 1; 46 - assert(isPlainObject(obj)); 47 - assert(isPlainObject({ __proto__: null, foo: "bar" })); 48 - }); 49 - 50 - Deno.test("isPlainObject returns false for class instances", () => { 51 - class MyClass {} 52 - assertFalse(isPlainObject(new MyClass())); 53 - }); 54 - 55 - Deno.test("isPlainObject returns false for CIDs", () => { 56 - const cid = CID.parse( 57 - "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 58 - ); 59 - assertFalse(isPlainObject(cid)); 60 - }); 61 - 62 - Deno.test("isPlainObject returns false for arrays", () => { 63 - assertFalse(isPlainObject([])); 64 - assertFalse(isPlainObject([1, 2, 3])); 65 - }); 66 - 67 - Deno.test("isPlainObject returns false for null", () => { 68 - assertFalse(isPlainObject(null)); 69 - }); 70 - 71 - Deno.test("isPlainObject returns false for non-objects", () => { 72 - assertFalse(isPlainObject(42)); 73 - assertFalse(isPlainObject("string")); 74 - assertFalse(isPlainObject(undefined)); 75 - assertFalse(isPlainObject(true)); 76 - });
-32
data/tests/utf8_test.ts
··· 1 - import { utf8Len } from "@atp/data"; 2 - import { graphemeLen } from "../utf8.ts"; 3 - import { assertEquals } from "@std/assert"; 4 - 5 - Deno.test("graphemeLen computes grapheme length", () => { 6 - assertEquals(graphemeLen("a"), 1); 7 - assertEquals(graphemeLen("~"), 1); 8 - assertEquals(graphemeLen("ö"), 1); 9 - assertEquals(graphemeLen("ñ"), 1); 10 - assertEquals(graphemeLen("©"), 1); 11 - assertEquals(graphemeLen("⽘"), 1); 12 - assertEquals(graphemeLen("☎"), 1); 13 - assertEquals(graphemeLen("𓋓"), 1); 14 - assertEquals(graphemeLen("😀"), 1); 15 - assertEquals(graphemeLen("👨‍👩‍👧‍👧"), 1); 16 - assertEquals(graphemeLen("a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧"), 10); 17 - // https://github.com/bluesky-social/atproto/issues/4321 18 - assertEquals(graphemeLen("नमस्ते"), 3); 19 - }); 20 - 21 - Deno.test("utf8Len computes utf8 string length", () => { 22 - assertEquals(utf8Len("a"), 1); 23 - assertEquals(utf8Len("~"), 1); 24 - assertEquals(utf8Len("ö"), 2); 25 - assertEquals(utf8Len("ñ"), 2); 26 - assertEquals(utf8Len("©"), 2); 27 - assertEquals(utf8Len("⽘"), 3); 28 - assertEquals(utf8Len("☎"), 3); 29 - assertEquals(utf8Len("𓋓"), 4); 30 - assertEquals(utf8Len("😀"), 4); 31 - assertEquals(utf8Len("👨‍👩‍👧‍👧"), 25); 32 - });
-38
data/uint8array.ts
··· 1 - /** 2 - * Coerces various binary data representations into a Uint8Array. 3 - * 4 - * @return `undefined` if the input could not be coerced into a {@link Uint8Array}. 5 - */ 6 - export function asUint8Array(input: unknown): Uint8Array | undefined { 7 - if (input instanceof Uint8Array) { 8 - return input; 9 - } 10 - 11 - if (ArrayBuffer.isView(input)) { 12 - return new Uint8Array( 13 - input.buffer, 14 - input.byteOffset, 15 - input.byteLength / Uint8Array.BYTES_PER_ELEMENT, 16 - ); 17 - } 18 - 19 - if (input instanceof ArrayBuffer) { 20 - return new Uint8Array(input); 21 - } 22 - 23 - return undefined; 24 - } 25 - 26 - export function ui8Equals(a: Uint8Array, b: Uint8Array): boolean { 27 - if (a.byteLength !== b.byteLength) { 28 - return false; 29 - } 30 - 31 - for (let i = 0; i < a.byteLength; i++) { 32 - if (a[i] !== b[i]) { 33 - return false; 34 - } 35 - } 36 - 37 - return true; 38 - }
-46
data/utf8.ts
··· 1 - const segmenter = new Intl.Segmenter(); 2 - 3 - export function graphemeLen(str: string): number { 4 - let length = 0; 5 - for (const _ of segmenter.segment(str)) length++; 6 - return length; 7 - } 8 - 9 - export function utf8Len(string: string): number { 10 - // similar to TextEncoder's implementation of UTF-8 encoding. 11 - // However, using TextEncoder to get the byte length is slower 12 - // as it requires allocating a new Uint8Array and copying data: 13 - 14 - // return new TextEncoder().encode(string).byteLength 15 - 16 - // The base length is the string length (all ASCII) 17 - let len = string.length; 18 - let code: number; 19 - 20 - // The loop calculates the number of additional bytes needed for 21 - // non-ASCII characters 22 - for (let i = 0; i < string.length; i += 1) { 23 - code = string.charCodeAt(i); 24 - 25 - if (code <= 0x7f) { 26 - // ASCII, 1 byte 27 - } else if (code <= 0x7ff) { 28 - // 2 bytes char 29 - len += 1; 30 - } else { 31 - // 3 bytes char 32 - len += 2; 33 - // If the current char is a high surrogate, and the next char is a low 34 - // surrogate, skip the next char as the total is a 4 bytes char 35 - // (represented as a surrogate pair in UTF-16) and was already accounted 36 - // for. 37 - if (code >= 0xd800 && code <= 0xdbff) { 38 - code = string.charCodeAt(i + 1); 39 - if (code >= 0xdc00 && code <= 0xdfff) { 40 - i++; 41 - } 42 - } 43 - } 44 - } 45 - return len; 46 - }
+80 -1
deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 + "jsr:@cliffy/ansi@^1.0.0-rc.8": "1.0.0-rc.8", 5 + "jsr:@cliffy/command@^1.0.0-rc.8": "1.0.0-rc.8", 6 + "jsr:@cliffy/flags@1.0.0-rc.8": "1.0.0-rc.8", 7 + "jsr:@cliffy/internal@1.0.0-rc.8": "1.0.0-rc.8", 8 + "jsr:@cliffy/table@1.0.0-rc.8": "1.0.0-rc.8", 9 + "jsr:@david/code-block-writer@13": "13.0.3", 10 + "jsr:@hono/hono@*": "4.9.9", 4 11 "jsr:@hono/hono@^4.9.8": "4.9.9", 5 12 "jsr:@logtape/file@^1.2.0-dev.344+834f24a9": "1.2.0-dev.344+834f24a9", 6 13 "jsr:@logtape/logtape@^1.2.0-dev.344+834f24a9": "1.2.0-dev.344+834f24a9", ··· 11 18 "jsr:@std/bytes@^1.0.5": "1.0.6", 12 19 "jsr:@std/cbor@~0.1.8": "0.1.8", 13 20 "jsr:@std/encoding@^1.0.10": "1.0.10", 21 + "jsr:@std/fmt@~1.0.2": "1.0.8", 22 + "jsr:@std/fs@1": "1.0.20", 14 23 "jsr:@std/fs@^1.0.19": "1.0.20", 15 24 "jsr:@std/internal@^1.0.12": "1.0.12", 16 25 "jsr:@std/json@^1.0.2": "1.0.2", 17 26 "jsr:@std/jsonc@^1.0.1": "1.0.2", 27 + "jsr:@std/path@1": "1.1.3", 28 + "jsr:@std/path@^1.1.2": "1.1.3", 29 + "jsr:@std/path@^1.1.3": "1.1.3", 18 30 "jsr:@std/streams@^1.0.9": "1.0.13", 31 + "jsr:@std/text@~1.0.7": "1.0.16", 32 + "jsr:@ts-morph/common@0.27": "0.27.0", 33 + "jsr:@ts-morph/ts-morph@26": "26.0.0", 19 34 "jsr:@zod/zod@^4.1.11": "4.1.11", 20 35 "npm:@atproto/crypto@*": "0.4.4", 21 36 "npm:@did-plc/lib@^0.0.4": "0.0.4", ··· 31 46 "npm:zod@^4.1.11": "4.1.11" 32 47 }, 33 48 "jsr": { 49 + "@cliffy/ansi@1.0.0-rc.8": { 50 + "integrity": "ba37f10ce55bbfbdd8ddd987f91f029b17bce88385b98ba3058870f3b007b80c", 51 + "dependencies": [ 52 + "jsr:@std/fmt" 53 + ] 54 + }, 55 + "@cliffy/command@1.0.0-rc.8": { 56 + "integrity": "758147790797c74a707e5294cc7285df665422a13d2a483437092ffce40b5557", 57 + "dependencies": [ 58 + "jsr:@cliffy/flags", 59 + "jsr:@cliffy/internal", 60 + "jsr:@cliffy/table", 61 + "jsr:@std/fmt", 62 + "jsr:@std/text" 63 + ] 64 + }, 65 + "@cliffy/flags@1.0.0-rc.8": { 66 + "integrity": "0f1043ce6ef037ba1cb5fe6b1bcecb25dc2f29371a1c17f278ab0f45e4b6f46c", 67 + "dependencies": [ 68 + "jsr:@std/text" 69 + ] 70 + }, 71 + "@cliffy/internal@1.0.0-rc.8": { 72 + "integrity": "34cdf2fad9b084b5aed493b138d573f52d4e988767215f7460daf0b918ff43d8" 73 + }, 74 + "@cliffy/table@1.0.0-rc.8": { 75 + "integrity": "8bbcdc2ba5e0061b4b13810a24e6f5c6ab19c09f0cce9eb691ccd76c7c6c9db5", 76 + "dependencies": [ 77 + "jsr:@std/fmt" 78 + ] 79 + }, 80 + "@david/code-block-writer@13.0.3": { 81 + "integrity": "f98c77d320f5957899a61bfb7a9bead7c6d83ad1515daee92dbacc861e13bb7f" 82 + }, 34 83 "@hono/hono@4.9.8": { 35 84 "integrity": "908150f13e90181a051a3af3bf15203aff00190682afedfd38824d0cb9299a95" 36 85 }, ··· 74 123 "@std/encoding@1.0.10": { 75 124 "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" 76 125 }, 126 + "@std/fmt@1.0.8": { 127 + "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" 128 + }, 77 129 "@std/fs@1.0.20": { 78 - "integrity": "e953206aae48d46ee65e8783ded459f23bec7dd1f3879512911c35e5484ea187" 130 + "integrity": "e953206aae48d46ee65e8783ded459f23bec7dd1f3879512911c35e5484ea187", 131 + "dependencies": [ 132 + "jsr:@std/internal", 133 + "jsr:@std/path@^1.1.3" 134 + ] 79 135 }, 80 136 "@std/internal@1.0.12": { 81 137 "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" ··· 89 145 "jsr:@std/json" 90 146 ] 91 147 }, 148 + "@std/path@1.1.3": { 149 + "integrity": "b015962d82a5e6daea980c32b82d2c40142149639968549c649031a230b1afb3", 150 + "dependencies": [ 151 + "jsr:@std/internal" 152 + ] 153 + }, 92 154 "@std/streams@1.0.10": { 93 155 "integrity": "75c0b1431873cd0d8b3d679015220204d36d3c7420d93b60acfc379eb0dc30af" 94 156 }, 95 157 "@std/streams@1.0.13": { 96 158 "integrity": "772d208cd0d3e5dac7c1d9e6cdb25842846d136eea4a41a62e44ed4ab0c8dd9e" 159 + }, 160 + "@std/text@1.0.16": { 161 + "integrity": "ddb9853b75119a2473857d691cf1ec02ad90793a2e8b4a4ac49d7354281a0cf8" 162 + }, 163 + "@ts-morph/common@0.27.0": { 164 + "integrity": "c7b73592d78ce8479b356fd4f3d6ec3c460d77753a8680ff196effea7a939052", 165 + "dependencies": [ 166 + "jsr:@std/fs@1", 167 + "jsr:@std/path@1" 168 + ] 169 + }, 170 + "@ts-morph/ts-morph@26.0.0": { 171 + "integrity": "f2b1ca67b4d1a6332d00c00dd48496b20879c899a702c1b92bcce1c552a168df", 172 + "dependencies": [ 173 + "jsr:@david/code-block-writer", 174 + "jsr:@ts-morph/common" 175 + ] 97 176 }, 98 177 "@zod/zod@4.1.5": { 99 178 "integrity": "e995ca7d588a835ce333de626c940e242c55b6763c5190e8cbb9fefb7d0fb4ef"
+9 -2
lex-gen/cmd/gen-api.ts
··· 4 4 genFileDiff, 5 5 printFileDiff, 6 6 readAllLexicons, 7 + shouldPullLexicons, 7 8 } from "../util.ts"; 8 9 import { genClientApi } from "../codegen/client.ts"; 9 10 import { formatGeneratedFiles } from "../codegen/util.ts"; ··· 43 44 } 44 45 } 45 46 46 - if (config?.pull) { 47 + const filesProvidedViaCli = input !== undefined; 48 + const needsPull = shouldPullLexicons( 49 + config, 50 + filesProvidedViaCli, 51 + finalInput, 52 + ); 53 + if (needsPull && config?.pull) { 47 54 await pullLexicons(config.pull); 48 55 } 49 56 ··· 65 72 } 66 73 console.log("API generated."); 67 74 68 - if (config?.pull) { 75 + if (needsPull && config?.pull) { 69 76 cleanupPullDirectory(config.pull); 70 77 } 71 78 },
+9 -3
lex-gen/cmd/gen-md.ts
··· 1 1 import { Command } from "@cliffy/command"; 2 - import { readAllLexicons } from "../util.ts"; 2 + import { readAllLexicons, shouldPullLexicons } from "../util.ts"; 3 3 import * as mdGen from "../mdgen/index.ts"; 4 4 import { loadLexiconConfig } from "../config.ts"; 5 5 import { cleanupPullDirectory, pullLexicons } from "../pull.ts"; ··· 50 50 } 51 51 } 52 52 53 - if (config?.pull) { 53 + const filesProvidedViaCli = input !== undefined; 54 + const needsPull = shouldPullLexicons( 55 + config, 56 + filesProvidedViaCli, 57 + [finalInput], 58 + ); 59 + if (needsPull && config?.pull) { 54 60 await pullLexicons(config.pull); 55 61 } 56 62 57 63 const lexicons = readAllLexicons(finalInput); 58 64 await mdGen.process(finalOutput, lexicons); 59 65 60 - if (config?.pull) { 66 + if (needsPull && config?.pull) { 61 67 cleanupPullDirectory(config.pull); 62 68 } 63 69 },
+9 -2
lex-gen/cmd/gen-server.ts
··· 4 4 genFileDiff, 5 5 printFileDiff, 6 6 readAllLexicons, 7 + shouldPullLexicons, 7 8 } from "../util.ts"; 8 9 import { formatGeneratedFiles } from "../codegen/util.ts"; 9 10 import { genServerApi } from "../codegen/server.ts"; ··· 45 46 } 46 47 } 47 48 48 - if (config?.pull) { 49 + const filesProvidedViaCli = input !== undefined; 50 + const needsPull = shouldPullLexicons( 51 + config, 52 + filesProvidedViaCli, 53 + finalInput, 54 + ); 55 + if (needsPull && config?.pull) { 49 56 await pullLexicons(config.pull); 50 57 } 51 58 ··· 69 76 } 70 77 console.log("API generated."); 71 78 72 - if (config?.pull) { 79 + if (needsPull && config?.pull) { 73 80 cleanupPullDirectory(config.pull); 74 81 } 75 82 },
+16 -3
lex-gen/cmd/gen-ts-obj.ts
··· 1 1 import { Command } from "@cliffy/command"; 2 - import { genTsObj, readAllLexicons } from "../util.ts"; 2 + import { 3 + genTsObj, 4 + readAllLexicons, 5 + shouldPullLexicons, 6 + } from "../util.ts"; 3 7 import { loadLexiconConfig } from "../config.ts"; 4 8 import { cleanupPullDirectory, pullLexicons } from "../pull.ts"; 5 9 import process from "node:process"; ··· 25 29 } 26 30 } 27 31 28 - if (config?.pull) { 32 + const filesProvidedViaCli = input !== undefined; 33 + const finalInputArray = Array.isArray(finalInput) 34 + ? finalInput 35 + : [finalInput]; 36 + const needsPull = shouldPullLexicons( 37 + config, 38 + filesProvidedViaCli, 39 + finalInputArray, 40 + ); 41 + if (needsPull && config?.pull) { 29 42 await pullLexicons(config.pull); 30 43 } 31 44 32 45 const lexicons = readAllLexicons(finalInput); 33 46 console.log(genTsObj(lexicons)); 34 47 35 - if (config?.pull) { 48 + if (needsPull && config?.pull) { 36 49 cleanupPullDirectory(config.pull); 37 50 } 38 51 });
+66 -14
lex-gen/util.ts
··· 9 9 import { colors } from "@cliffy/ansi/colors"; 10 10 import { ZodError } from "zod"; 11 11 import { type LexiconDoc, parseLexiconDoc } from "@atp/lexicon"; 12 - import type { FileDiff, GeneratedAPI } from "./types.ts"; 12 + import type { FileDiff, GeneratedAPI, LexiconConfig } from "./types.ts"; 13 13 import process from "node:process"; 14 14 15 15 type RecursiveZodError = { ··· 23 23 24 24 function walkDir( 25 25 dir: string, 26 - relativePath: string, 26 + relativeToCwd: string, 27 27 regex: RegExp, 28 28 files: string[], 29 29 ): void { ··· 32 32 const entries = Array.from(readDirSync(dir)); 33 33 for (const entry of entries) { 34 34 const fullPath = join(dir, entry.name); 35 - const relPath = relativePath 36 - ? join(relativePath, entry.name) 35 + const relToCwd = relativeToCwd 36 + ? join(relativeToCwd, entry.name) 37 37 : entry.name; 38 38 if (statSync(fullPath).isDirectory) { 39 - walkDir(fullPath, relPath, regex, files); 39 + walkDir(fullPath, relToCwd, regex, files); 40 40 } else if (entry.name.endsWith(".json")) { 41 - const testPath = relPath.startsWith("/") ? relPath : `/${relPath}`; 42 - if (regex.test(testPath) || regex.test(relPath)) { 41 + const testPath = relToCwd.startsWith("/") ? relToCwd : `/${relToCwd}`; 42 + if (regex.test(testPath) || regex.test(relToCwd)) { 43 43 files.push(fullPath); 44 44 } 45 45 } ··· 59 59 }); 60 60 const basePath = normalizedPattern.split("*")[0] || 61 61 normalizedPattern.split("?")[0] || ""; 62 - const searchDir = basePath.includes("/") 63 - ? join( 64 - cwd, 65 - basePath.substring(0, basePath.lastIndexOf("/") || basePath.length), 66 - ) 67 - : cwd; 62 + let searchDir = cwd; 63 + let relativeToCwd = ""; 64 + if (basePath.includes("/")) { 65 + const lastSlashIndex = basePath.lastIndexOf("/"); 66 + if (lastSlashIndex >= 0) { 67 + const baseDir = basePath.substring(0, lastSlashIndex); 68 + searchDir = join(cwd, baseDir); 69 + relativeToCwd = baseDir; 70 + } 71 + } 68 72 69 - walkDir(searchDir, "", regex, files); 73 + walkDir(searchDir, relativeToCwd, regex, files); 70 74 } 71 75 72 76 return Array.from(new Set(files)); ··· 236 240 function dedup(arr: string[]): string[] { 237 241 return Array.from(new Set(arr)); 238 242 } 243 + 244 + export function shouldPullLexicons( 245 + config: LexiconConfig | null, 246 + filesProvidedViaCli: boolean, 247 + files: string[], 248 + ): boolean { 249 + if (!config?.pull) { 250 + return false; 251 + } 252 + 253 + if (filesProvidedViaCli) { 254 + return false; 255 + } 256 + 257 + const cwd = typeof Deno !== "undefined" ? Deno.cwd() : process.cwd(); 258 + 259 + for (const filePattern of files) { 260 + const normalizedPattern = filePattern.startsWith("./") 261 + ? filePattern.slice(2) 262 + : filePattern; 263 + const filePath = normalizedPattern.startsWith("/") 264 + ? normalizedPattern 265 + : join(cwd, normalizedPattern); 266 + 267 + if (filePattern.includes("*") || filePattern.includes("?")) { 268 + const expanded = expandGlobPatterns([filePattern]); 269 + if (expanded.length === 0) { 270 + return true; 271 + } 272 + let allExist = true; 273 + for (const file of expanded) { 274 + if (!existsSync(file)) { 275 + allExist = false; 276 + break; 277 + } 278 + } 279 + if (!allExist) { 280 + return true; 281 + } 282 + } else { 283 + if (!existsSync(filePath)) { 284 + return true; 285 + } 286 + } 287 + } 288 + 289 + return false; 290 + }
-9
lex/deno.json
··· 1 - { 2 - "name": "@atp/lex", 3 - "version": "0.1.0-alpha.1", 4 - "exports": "./mod.ts", 5 - "license": "MIT", 6 - "imports": { 7 - "multiformats": "npm:multiformats@^13.4.1" 8 - } 9 - }
-5
lex/schema/core.ts
··· 1 - export * from "./core/$type.ts"; 2 - export * from "./core/record-key.ts"; 3 - export * from "./core/result.ts"; 4 - export * from "./core/string-format.ts"; 5 - export * from "./core/types.ts";
-16
lex/schema/core/$type.ts
··· 1 - import type { Nsid } from "./string-format.ts"; 2 - 3 - export type $Type< 4 - N extends Nsid = Nsid, 5 - H extends string = string, 6 - > = N extends Nsid ? string extends H ? N | `${N}#${string}` 7 - : H extends "main" ? N 8 - : `${N}#${H}` 9 - : never; 10 - 11 - export function $type<N extends Nsid, H extends string>( 12 - nsid: N, 13 - hash: H, 14 - ): $Type<N, H> { 15 - return (hash === "main" ? nsid : `${nsid}#${hash}`) as $Type<N, H>; 16 - }
-15
lex/schema/core/record-key.ts
··· 1 - export type RecordKey = "any" | "nsid" | "tid" | `literal:${string}`; 2 - 3 - export function isRecordKey<T>(key: T): key is T & RecordKey { 4 - return ( 5 - key === "any" || 6 - key === "nsid" || 7 - key === "tid" || 8 - (typeof key === "string" && key.startsWith("literal:")) 9 - ); 10 - } 11 - 12 - export function asRecordKey(key: unknown): RecordKey { 13 - if (isRecordKey(key)) return key; 14 - throw new Error(`Invalid record key: ${String(key)}`); 15 - }
-75
lex/schema/core/result.ts
··· 1 - export type ResultSuccess<V = unknown> = { success: true; value: V }; 2 - export type ResultFailure<E = Error> = { success: false; error: E }; 3 - 4 - export type Result<V = unknown, E = Error> = 5 - | ResultSuccess<V> 6 - | ResultFailure<E>; 7 - 8 - export function success<V>(value: V): ResultSuccess<V> { 9 - return { success: true, value }; 10 - } 11 - 12 - export function failure<E>(error: E): ResultFailure<E> { 13 - return { success: false, error }; 14 - } 15 - 16 - export function failureError<T>(result: ResultFailure<T>): T { 17 - return result.error; 18 - } 19 - 20 - export function successValue<T>(result: ResultSuccess<T>): T { 21 - return result.value; 22 - } 23 - 24 - /** 25 - * Catches any error and wraps it in a {@link ResultFailure<Error>}. 26 - * 27 - * @param err - The error to catch. 28 - * @returns A {@link ResultFailure<Error>} containing the caught error. 29 - * @example 30 - * 31 - * ```ts 32 - * declare function someFunction(): Promise<ResultSuccess<string>> 33 - * 34 - * const result = await someFunction().catch(catchall) 35 - * if (result.success) { 36 - * console.log(result.value) // string 37 - * } else { 38 - * console.error(result.error instanceof Error) // true 39 - * console.error(result.error.message) // string 40 - * } 41 - * ``` 42 - */ 43 - export function catchall(err: unknown): ResultFailure<Error> { 44 - if (err instanceof Error) return failure(err); 45 - return failure(new Error("Unknown error", { cause: err })); 46 - } 47 - 48 - /** 49 - * Creates a catcher function for the given constructor that wraps caught errors 50 - * in a {@link ResultFailure}. 51 - * 52 - * @example 53 - * 54 - * ```ts 55 - * class FooError extends Error {} 56 - * class BarError extends Error {} 57 - * 58 - * declare function someFunction(): Promise<ResultSuccess<string>> 59 - * 60 - * const result = await someFunction() 61 - * .catch(createCatcher(FooError)) 62 - * .catch(createCatcher(BarError)) 63 - * 64 - * if (result.success) { 65 - * console.log(result.value) // string 66 - * } else { 67 - * console.error(result.error) // FooError | BarError 68 - * } 69 - */ 70 - export function createCatcher<T>(Ctor: new (...args: unknown[]) => T) { 71 - return (err: unknown): ResultFailure<T> => { 72 - if (err instanceof Ctor) return failure(err); 73 - throw err; 74 - }; 75 - }
-121
lex/schema/core/string-format.ts
··· 1 - import { ensureValidCidString, isLanguage } from "@atp/data"; 2 - import { 3 - ensureValidAtUri, 4 - ensureValidDatetime, 5 - ensureValidDid, 6 - ensureValidHandle, 7 - ensureValidNsid, 8 - ensureValidRecordKey, 9 - ensureValidTid, 10 - } from "@atp/syntax"; 11 - 12 - // Allow (date as Date).toISOString() to be used where datetime format is expected 13 - declare global { 14 - interface Date { 15 - toISOString(): `${string}T${string}Z`; 16 - } 17 - } 18 - 19 - export const STRING_FORMATS = Object.freeze( 20 - [ 21 - "datetime", 22 - "uri", 23 - "at-uri", 24 - "did", 25 - "handle", 26 - "at-identifier", 27 - "nsid", 28 - "cid", 29 - "language", 30 - "tid", 31 - "record-key", 32 - ] as const, 33 - ); 34 - 35 - export type StringFormat = (typeof STRING_FORMATS)[number]; 36 - 37 - export type Did<M extends string = string> = `did:${M}:${string}`; 38 - export type Uri = `${string}:${string}`; 39 - export type Nsid = `${string}.${string}.${string}`; 40 - /** An ISO 8601 formatted datetime string (YYYY-MM-DDTHH:mm:ss.sssZ) */ 41 - export type Datetime = `${string}T${string}`; 42 - export type Handle = `${string}.${string}`; 43 - export type AtIdentifier = Did | Handle; 44 - export type AtUri = `at://${AtIdentifier}/${Nsid}/${string}`; 45 - 46 - export type InferStringFormat<F> = 47 - // 48 - F extends "datetime" ? Datetime 49 - : F extends "uri" ? Uri 50 - : F extends "at-uri" ? AtUri 51 - : F extends "did" ? Did 52 - : F extends "handle" ? Handle 53 - : F extends "at-identifier" ? AtIdentifier 54 - : F extends "nsid" ? Nsid 55 - : string; 56 - 57 - type AssertFn<T> = <I extends string>(input: I) => asserts input is I & T; 58 - 59 - // Re-export utility typed as assertion functions so that TypeScript can 60 - // infer the narrowed type after calling them. 61 - 62 - export const assertDid: AssertFn<Did> = ensureValidDid; 63 - export const assertAtUri: AssertFn<AtUri> = ensureValidAtUri; 64 - export const assertNsid: AssertFn<Nsid> = ensureValidNsid; 65 - export const assertTid: AssertFn<string> = ensureValidTid; 66 - export const assertRecordKey: AssertFn<string> = ensureValidRecordKey; 67 - export const assertDatetime: AssertFn<Datetime> = ensureValidDatetime; 68 - export const assertCidString: AssertFn<string> = ensureValidCidString; 69 - export const assertHandle: AssertFn<Handle> = ensureValidHandle; 70 - 71 - // Export utilities for formats missing from @atproto/syntax 72 - 73 - export const assertUri: AssertFn<Uri> = (input) => { 74 - if (!/^\w+:(?:\/\/)?[^\s/][^\s]*$/.test(input)) { 75 - throw new Error("Invalid URI"); 76 - } 77 - }; 78 - export const assertLanguage: AssertFn<string> = (input) => { 79 - if (!isLanguage(input)) { 80 - throw new Error("Invalid BCP 47 string"); 81 - } 82 - }; 83 - export const assertAtIdentifier: AssertFn<AtIdentifier> = (input) => { 84 - if (input.startsWith("did:web:") || input.startsWith("did:plc:")) { 85 - assertDid(input); 86 - } else if (input.startsWith("did:")) { 87 - throw new Error("Invalid DID method"); 88 - } else { 89 - try { 90 - assertHandle(input); 91 - } catch (cause) { 92 - throw new Error("Invalid DID or handle", { cause }); 93 - } 94 - } 95 - }; 96 - 97 - const formatters = /*#__PURE__*/ new Map<StringFormat, (str: string) => void>( 98 - [ 99 - ["datetime", assertDatetime], 100 - ["uri", assertUri], 101 - ["at-uri", assertAtUri], 102 - ["did", assertDid], 103 - ["handle", assertHandle], 104 - ["at-identifier", assertAtIdentifier], 105 - ["nsid", assertNsid], 106 - ["cid", assertCidString], 107 - ["language", assertLanguage], 108 - ["tid", assertTid], 109 - ["record-key", assertRecordKey], 110 - ] as const, 111 - ); 112 - 113 - export function assertStringFormat<F extends StringFormat>( 114 - input: string, 115 - format: F, 116 - ): asserts input is InferStringFormat<F> { 117 - const assertFn = formatters.get(format); 118 - if (assertFn) assertFn(input); 119 - // Fool-proof 120 - else throw new Error(`Unknown string format: ${format}`); 121 - }
-22
lex/schema/core/types.ts
··· 1 - /** 2 - * Same as {@link string} but prevents TypeScript allowing union types to 3 - * be widened to `string` in IDEs. 4 - */ 5 - export type UnknownString = string & NonNullable<unknown>; 6 - 7 - export type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>; 8 - 9 - // @NOTE there is no way to express "array containing at least one P", so we use 10 - // "array that contains P at first or last position" as a workaround. 11 - export type ArrayContaining<T, Items = unknown> = 12 - | readonly [T, ...Items[]] 13 - | readonly [...Items[], T]; 14 - 15 - declare const __restricted: unique symbol; 16 - /** 17 - * A type that represents a value that cannot be used, with a custom 18 - * message explaining the restriction. 19 - */ 20 - export type Restricted<Message extends string> = typeof __restricted & { 21 - [__restricted]: Message; 22 - };
-363
lex/schema/external.ts
··· 1 - import { type $Type, $type, type Nsid, type RecordKey } from "./core.ts"; 2 - import { 3 - ArraySchema, 4 - type ArraySchemaOptions, 5 - BlobSchema, 6 - type BlobSchemaOptions, 7 - BooleanSchema, 8 - type BooleanSchemaOptions, 9 - BytesSchema, 10 - type BytesSchemaOptions, 11 - CidSchema, 12 - type CustomAssertion, 13 - CustomSchema, 14 - DictSchema, 15 - DiscriminatedUnionSchema, 16 - type DiscriminatedUnionSchemaVariants, 17 - EnumSchema, 18 - IntegerSchema, 19 - type IntegerSchemaOptions, 20 - IntersectionSchema, 21 - type IntersectionSchemaValidators, 22 - LiteralSchema, 23 - NeverSchema, 24 - NullSchema, 25 - ObjectSchema, 26 - type ObjectSchemaOptions, 27 - type ObjectSchemaProperties, 28 - ParamsSchema, 29 - type ParamsSchemaOptions, 30 - type ParamsSchemaProperties, 31 - Payload, 32 - type PayloadBody, 33 - Permission, 34 - type PermissionOptions, 35 - PermissionSet, 36 - type PermissionSetOptions, 37 - Procedure, 38 - Query, 39 - RecordSchema, 40 - RefSchema, 41 - type RefSchemaGetter, 42 - StringSchema, 43 - type StringSchemaOptions, 44 - Subscription, 45 - TokenSchema, 46 - TypedObjectSchema, 47 - type TypedRefGetter, 48 - TypedRefSchema, 49 - TypedUnionSchema, 50 - UnionSchema, 51 - type UnionSchemaValidators, 52 - type UnknownObjectOutput, 53 - UnknownObjectSchema, 54 - UnknownSchema, 55 - } from "./schema.ts"; 56 - import type { Infer, PropertyKey, Validator } from "./validation.ts"; 57 - 58 - export * from "./core.ts"; 59 - export * from "./schema.ts"; 60 - export * from "./validation.ts"; 61 - 62 - /*@__NO_SIDE_EFFECTS__*/ 63 - export function never() { 64 - return new NeverSchema(); 65 - } 66 - 67 - /*@__NO_SIDE_EFFECTS__*/ 68 - export function unknown() { 69 - return new UnknownSchema(); 70 - } 71 - 72 - /*@__NO_SIDE_EFFECTS__*/ 73 - export function _null() { 74 - return new NullSchema(); 75 - } 76 - 77 - export { _null as null }; 78 - 79 - /*@__NO_SIDE_EFFECTS__*/ 80 - export function literal<const V extends null | string | number | boolean>( 81 - value: V, 82 - ) { 83 - return new LiteralSchema<V>(value); 84 - } 85 - 86 - /*@__NO_SIDE_EFFECTS__*/ 87 - export function _enum<const V extends null | string | number | boolean>( 88 - value: readonly V[], 89 - ) { 90 - return new EnumSchema<V>(value); 91 - } 92 - 93 - // @NOTE "enum" is a reserved keyword in JS/TS 94 - export { _enum as enum }; 95 - 96 - /*@__NO_SIDE_EFFECTS__*/ 97 - export function boolean(options: BooleanSchemaOptions = {}) { 98 - return new BooleanSchema(options); 99 - } 100 - 101 - /*@__NO_SIDE_EFFECTS__*/ 102 - export function integer(options: IntegerSchemaOptions = {}) { 103 - return new IntegerSchema(options); 104 - } 105 - 106 - /*@__NO_SIDE_EFFECTS__*/ 107 - export function cidLink() { 108 - return new CidSchema(); 109 - } 110 - 111 - /*@__NO_SIDE_EFFECTS__*/ 112 - export function bytes(options: BytesSchemaOptions = {}) { 113 - return new BytesSchema(options); 114 - } 115 - 116 - /*@__NO_SIDE_EFFECTS__*/ 117 - export function blob(options: BlobSchemaOptions = {}) { 118 - return new BlobSchema(options); 119 - } 120 - 121 - /*@__NO_SIDE_EFFECTS__*/ 122 - export function string< 123 - const O extends StringSchemaOptions = NonNullable<unknown>, 124 - >(options: StringSchemaOptions & O = {} as O) { 125 - return new StringSchema<O>(options); 126 - } 127 - 128 - /*@__NO_SIDE_EFFECTS__*/ 129 - export function array<const T>( 130 - items: Validator<T>, 131 - options: ArraySchemaOptions = {}, 132 - ) { 133 - return new ArraySchema(items, options); 134 - } 135 - 136 - /*@__NO_SIDE_EFFECTS__*/ 137 - export function object< 138 - const P extends ObjectSchemaProperties, 139 - const O extends ObjectSchemaOptions = NonNullable<unknown>, 140 - >( 141 - properties: ObjectSchemaProperties & P, 142 - options: ObjectSchemaOptions & O = {} as O, 143 - ) { 144 - return new ObjectSchema<P, O>(properties, options); 145 - } 146 - 147 - /*@__NO_SIDE_EFFECTS__*/ 148 - export function dict<const K extends Validator, const V extends Validator>( 149 - key: K, 150 - value: V, 151 - ) { 152 - return new DictSchema<K, V>(key, value); 153 - } 154 - 155 - // Utility 156 - export type { UnknownObjectOutput as UnknownObject }; 157 - 158 - /*@__NO_SIDE_EFFECTS__*/ 159 - export function unknownObject() { 160 - return new UnknownObjectSchema(); 161 - } 162 - 163 - /*@__NO_SIDE_EFFECTS__*/ 164 - export function ref<T>(get: RefSchemaGetter<T>) { 165 - return new RefSchema<T>(get); 166 - } 167 - 168 - /*@__NO_SIDE_EFFECTS__*/ 169 - export function custom<T>( 170 - assertion: CustomAssertion<T>, 171 - message: string, 172 - path?: PropertyKey | readonly PropertyKey[], 173 - ) { 174 - return new CustomSchema<T>(assertion, message, path); 175 - } 176 - 177 - /*@__NO_SIDE_EFFECTS__*/ 178 - export function union<const V extends UnionSchemaValidators>(validators: V) { 179 - return new UnionSchema<V>(validators); 180 - } 181 - 182 - /*@__NO_SIDE_EFFECTS__*/ 183 - export function intersection<const V extends IntersectionSchemaValidators>( 184 - validators: V, 185 - ) { 186 - return new IntersectionSchema<V>(validators); 187 - } 188 - 189 - /*@__NO_SIDE_EFFECTS__*/ 190 - export function discriminatedUnion< 191 - const Discriminator extends string, 192 - const Options extends DiscriminatedUnionSchemaVariants<Discriminator>, 193 - >(discriminator: Discriminator, variants: Options) { 194 - return new DiscriminatedUnionSchema<Discriminator, Options>( 195 - discriminator, 196 - variants, 197 - ); 198 - } 199 - 200 - /*@__NO_SIDE_EFFECTS__*/ 201 - export function token<const N extends Nsid, const H extends string>( 202 - nsid: N, 203 - hash: H, 204 - ) { 205 - return new TokenSchema($type(nsid, hash)); 206 - } 207 - 208 - /*@__NO_SIDE_EFFECTS__*/ 209 - export function typedRef<const V extends { $type?: string }>( 210 - get: TypedRefGetter<V>, 211 - ) { 212 - return new TypedRefSchema<V>(get); 213 - } 214 - 215 - /*@__NO_SIDE_EFFECTS__*/ 216 - export function typedUnion< 217 - const R extends readonly TypedRefSchema[], 218 - const C extends boolean, 219 - >(refs: R, closed: C) { 220 - return new TypedUnionSchema<R, C>(refs, closed); 221 - } 222 - 223 - /** 224 - * This function offers two overloads: 225 - * - One that allows creating a {@link TypedObjectSchema}, and infer the output 226 - * type from the provided arguments, without requiring to specify any of the 227 - * generics. This is useful when you want to define a record without 228 - * explicitly defining its interface. This version does not support circular 229 - * references, as TypeScript cannot infer types in such cases. 230 - * - One allows creating a {@link TypedObjectSchema} with an explicitly defined 231 - * interface. This will typically be used by codegen (`lex build`) to generate 232 - * schemas that work even if they contain circular references. 233 - */ 234 - export function typedObject< 235 - const N extends Nsid, 236 - const H extends string, 237 - const Schema extends Validator<{ [_ in string]?: unknown }>, 238 - >(nsid: N, hash: H, schema: Schema): TypedObjectSchema<$Type<N, H>, Schema>; 239 - export function typedObject<const V extends { $type?: $Type }>( 240 - nsid: V extends { $type?: infer T extends string } 241 - ? T extends `${infer N}#${string}` ? N 242 - : T // (T is a "main" type, so already an NSID) 243 - : never, 244 - hash: V extends { $type?: infer T extends string } 245 - ? T extends `${string}#${infer H}` ? H 246 - : "main" 247 - : never, 248 - schema: Validator<Omit<V, "$type">>, 249 - ): TypedObjectSchema<NonNullable<V["$type"]>, typeof schema, V>; 250 - /*@__NO_SIDE_EFFECTS__*/ 251 - export function typedObject< 252 - const N extends Nsid, 253 - const H extends string, 254 - const Schema extends Validator<{ [_ in string]?: unknown }>, 255 - >(nsid: N, hash: H, schema: Schema) { 256 - return new TypedObjectSchema<$Type<N, H>, Schema>($type(nsid, hash), schema); 257 - } 258 - 259 - /** 260 - * Ensures that a `$type` used in a record is a valid NSID (i.e. no fragment). 261 - */ 262 - type AsNsid<T> = T extends `${string}#${string}` ? never : T; 263 - 264 - /** 265 - * This function offers two overloads: 266 - * - One that allows creating a {@link RecordSchema}, and infer the output type 267 - * from the provided arguments, without requiring to specify any of the 268 - * generics. This is useful when you want to define a record without 269 - * explicitly defining its interface. This version does not support circular 270 - * references, as TypeScript cannot infer types in such cases. 271 - * - One allows creating a {@link RecordSchema} with an explicitly defined 272 - * interface. This will typically be used by codegen (`lex build`) to generate 273 - * schemas that work even if they contain circular references. 274 - */ 275 - export function record< 276 - const K extends RecordKey, 277 - const T extends Nsid, 278 - const S extends Validator<{ [_ in string]?: unknown }>, 279 - >( 280 - key: K, 281 - type: AsNsid<T>, 282 - schema: S, 283 - ): RecordSchema<K, T, S, Infer<S> & { $type: T }>; 284 - export function record< 285 - const K extends RecordKey, 286 - const V extends { $type: Nsid }, 287 - >( 288 - key: K, 289 - type: AsNsid<V["$type"]>, 290 - schema: Validator<Omit<V, "$type">>, 291 - ): RecordSchema<K, V["$type"], typeof schema, V>; 292 - /*@__NO_SIDE_EFFECTS__*/ 293 - export function record< 294 - const K extends RecordKey, 295 - const T extends Nsid, 296 - const S extends Validator<{ [_ in string]?: unknown }>, 297 - >(key: K, type: T, schema: S) { 298 - return new RecordSchema<K, T, S, Infer<S> & { $type: T }>(key, type, schema); 299 - } 300 - 301 - /*@__NO_SIDE_EFFECTS__*/ 302 - export function params< 303 - const P extends ParamsSchemaProperties = NonNullable<unknown>, 304 - const O extends ParamsSchemaOptions = ParamsSchemaOptions, 305 - >(properties: P = {} as P, options: ParamsSchemaOptions & O = {} as O) { 306 - return new ParamsSchema<P, O>(properties, options); 307 - } 308 - 309 - /*@__NO_SIDE_EFFECTS__*/ 310 - export function payload< 311 - const E extends string | undefined = undefined, 312 - const S extends PayloadBody<E> = undefined, 313 - >(encoding: E = undefined as E, schema: S = undefined as S) { 314 - return new Payload<E, S>(encoding, schema); 315 - } 316 - 317 - /*@__NO_SIDE_EFFECTS__*/ 318 - export function query< 319 - const N extends Nsid, 320 - const P extends ParamsSchema, 321 - const O extends Payload, 322 - const E extends undefined | readonly string[] = undefined, 323 - >(nsid: N, parameters: P, output: O, errors: E = undefined as E) { 324 - return new Query<N, P, O, E>(nsid, parameters, output, errors); 325 - } 326 - 327 - /*@__NO_SIDE_EFFECTS__*/ 328 - export function procedure< 329 - const N extends Nsid, 330 - const P extends ParamsSchema, 331 - const I extends Payload, 332 - const O extends Payload, 333 - const E extends undefined | readonly string[] = undefined, 334 - >(nsid: N, parameters: P, input: I, output: O, errors: E = undefined as E) { 335 - return new Procedure<N, P, I, O, E>(nsid, parameters, input, output, errors); 336 - } 337 - 338 - /*@__NO_SIDE_EFFECTS__*/ 339 - export function subscription< 340 - const N extends string, 341 - const P extends ParamsSchema, 342 - const M extends undefined | RefSchema | TypedUnionSchema | ObjectSchema, 343 - const E extends undefined | readonly string[] = undefined, 344 - >(nsid: N, parameters: P, message: M, errors: E = undefined as E) { 345 - return new Subscription<N, P, M, E>(nsid, parameters, message, errors); 346 - } 347 - 348 - /*@__NO_SIDE_EFFECTS__*/ 349 - export function permission< 350 - const R extends string, 351 - const O extends PermissionOptions, 352 - >(resource: R, options: PermissionOptions & O = {} as O) { 353 - return new Permission<R, O>(resource, options); 354 - } 355 - 356 - /*@__NO_SIDE_EFFECTS__*/ 357 - export function permissionSet< 358 - const N extends string, 359 - const P extends readonly Permission[], 360 - const O extends PermissionSetOptions, 361 - >(nsid: N, permissions: P, options: PermissionSetOptions & O = {} as O) { 362 - return new PermissionSet<N, P, O>(nsid, permissions, options); 363 - }
-3
lex/schema/index.ts
··· 1 - import * as l from "./external.ts"; 2 - export * from "./external.ts"; 3 - export { l };
-40
lex/schema/schema.ts
··· 1 - // Utilities (that depend on *and* are used by schemas) 2 - export * from "./schema/_parameters.ts"; 3 - 4 - // Concrete Types 5 - export * from "./schema/array.ts"; 6 - export * from "./schema/blob.ts"; 7 - export * from "./schema/boolean.ts"; 8 - export * from "./schema/bytes.ts"; 9 - export * from "./schema/cid.ts"; 10 - export * from "./schema/dict.ts"; 11 - export * from "./schema/enum.ts"; 12 - export * from "./schema/integer.ts"; 13 - export * from "./schema/literal.ts"; 14 - export * from "./schema/never.ts"; 15 - export * from "./schema/null.ts"; 16 - export * from "./schema/object.ts"; 17 - export * from "./schema/string.ts"; 18 - export * from "./schema/unknown-object.ts"; 19 - export * from "./schema/unknown.ts"; 20 - 21 - // Composite Types 22 - export * from "./schema/custom.ts"; 23 - export * from "./schema/discriminated-union.ts"; 24 - export * from "./schema/intersection.ts"; 25 - export * from "./schema/ref.ts"; 26 - export * from "./schema/union.ts"; 27 - 28 - // Lexicon specific Types 29 - export * from "./schema/params.ts"; 30 - export * from "./schema/payload.ts"; 31 - export * from "./schema/permission-set.ts"; 32 - export * from "./schema/permission.ts"; 33 - export * from "./schema/procedure.ts"; 34 - export * from "./schema/query.ts"; 35 - export * from "./schema/record.ts"; 36 - export * from "./schema/subscription.ts"; 37 - export * from "./schema/token.ts"; 38 - export * from "./schema/typed-object.ts"; 39 - export * from "./schema/typed-ref.ts"; 40 - export * from "./schema/typed-union.ts";
-26
lex/schema/schema/_parameters.ts
··· 1 - import type { Infer, Validator } from "../validation.ts"; 2 - import { ArraySchema } from "./array.ts"; 3 - import { BooleanSchema } from "./boolean.ts"; 4 - import { DictSchema } from "./dict.ts"; 5 - import { IntegerSchema } from "./integer.ts"; 6 - import { StringSchema } from "./string.ts"; 7 - import { UnionSchema } from "./union.ts"; 8 - 9 - export type ParamScalar = Infer<typeof paramScalarSchema>; 10 - const paramScalarSchema = new UnionSchema([ 11 - new BooleanSchema({}), 12 - new IntegerSchema({}), 13 - new StringSchema({}), 14 - ]); 15 - 16 - export type Param = Infer<typeof paramSchema>; 17 - export const paramSchema = new UnionSchema([ 18 - paramScalarSchema, 19 - new ArraySchema(paramScalarSchema, {}), 20 - ]); 21 - 22 - export type Params = { [_: string]: undefined | Param }; 23 - export const paramsSchema = new DictSchema( 24 - new StringSchema({}), 25 - paramSchema, 26 - ) satisfies Validator<Params>;
-55
lex/schema/schema/array.ts
··· 1 - import { 2 - type ValidationResult, 3 - Validator, 4 - type ValidatorContext, 5 - } from "../validation.ts"; 6 - 7 - export type ArraySchemaOptions = { 8 - minLength?: number; 9 - maxLength?: number; 10 - }; 11 - 12 - export class ArraySchema<Item = unknown> extends Validator<Array<Item>> { 13 - override readonly lexiconType = "array" as const; 14 - 15 - constructor( 16 - readonly items: Validator<Item>, 17 - readonly options: ArraySchemaOptions, 18 - ) { 19 - super(); 20 - } 21 - 22 - override validateInContext( 23 - input: unknown, 24 - ctx: ValidatorContext, 25 - ): ValidationResult<Array<Item>> { 26 - if (!Array.isArray(input)) { 27 - return ctx.issueInvalidType(input, "array"); 28 - } 29 - 30 - const { minLength, maxLength } = this.options; 31 - 32 - if (minLength != null && input.length < minLength) { 33 - return ctx.issueTooSmall(input, "array", minLength, input.length); 34 - } 35 - 36 - if (maxLength != null && input.length > maxLength) { 37 - return ctx.issueTooBig(input, "array", maxLength, input.length); 38 - } 39 - 40 - let copy: undefined | Array<Item>; 41 - 42 - for (let i = 0; i < input.length; i++) { 43 - const result = ctx.validateChild(input, i, this.items); 44 - if (!result.success) return result; 45 - 46 - if (result.value !== input[i]) { 47 - // Copy on write (but only if we did not already make a copy) 48 - copy ??= Array.from(input); 49 - copy[i] = result.value; 50 - } 51 - } 52 - 53 - return ctx.success(copy ?? input) as ValidationResult<Array<Item>>; 54 - } 55 - }
-86
lex/schema/schema/blob.ts
··· 1 - import { 2 - type BlobRef, 3 - isBlobRef, 4 - isLegacyBlobRef, 5 - type LegacyBlobRef, 6 - } from "@atp/data"; 7 - import { 8 - type ValidationResult, 9 - Validator, 10 - type ValidatorContext, 11 - } from "../validation.ts"; 12 - 13 - export type BlobSchemaOptions = { 14 - /** 15 - * Whether to allow legacy blob references format 16 - * @see {@link LegacyBlobRef} 17 - */ 18 - allowLegacy?: boolean; 19 - /** 20 - * Whether to enforce strict validation on the blob reference (CID version, codec, hash function) 21 - */ 22 - strict?: boolean; 23 - /** 24 - * List of accepted mime types 25 - */ 26 - accept?: string[]; 27 - /** 28 - * Maximum size in bytes 29 - */ 30 - maxSize?: number; 31 - }; 32 - 33 - export type { BlobRef, LegacyBlobRef }; 34 - 35 - export type BlobSchemaOutput<Options> = Options extends { allowLegacy: true } 36 - ? BlobRef | LegacyBlobRef 37 - : BlobRef; 38 - 39 - export class BlobSchema<O extends BlobSchemaOptions> extends Validator< 40 - BlobSchemaOutput<O> 41 - > { 42 - override readonly lexiconType = "blob" as const; 43 - 44 - constructor(readonly options: O) { 45 - super(); 46 - } 47 - 48 - override validateInContext( 49 - input: unknown, 50 - ctx: ValidatorContext, 51 - ): ValidationResult<BlobSchemaOutput<O>> { 52 - if (!isBlob(input, this.options)) { 53 - return ctx.issueInvalidType(input, "blob"); 54 - } 55 - 56 - // @NOTE Historically, we did not enforce constraints on blob references 57 - // https://github.com/bluesky-social/atproto/blob/4c15fb47cec26060bff2e710e95869a90c9d7fdd/packages/lexicon/src/validators/blob.ts#L5-L19 58 - 59 - // const { accept } = this.options 60 - // if (accept && !accept.includes(input.mimeType)) { 61 - // return ctx.issueInvalidValue(input, accept) 62 - // } 63 - 64 - // const { maxSize } = this.options 65 - // if (maxSize != null && input.size != -1 && input.size > maxSize) { 66 - // return ctx.issueTooBig(input, 'blob', maxSize, input.size) 67 - // } 68 - 69 - return ctx.success(input); 70 - } 71 - } 72 - 73 - function isBlob<O extends BlobSchemaOptions>( 74 - input: unknown, 75 - options: O, 76 - ): input is BlobSchemaOutput<O> { 77 - if ((input as { $type: string })?.$type !== undefined) { 78 - return isBlobRef(input, options); 79 - } 80 - 81 - if (options.allowLegacy === true) { 82 - return isLegacyBlobRef(input); 83 - } 84 - 85 - return false; 86 - }
-28
lex/schema/schema/boolean.ts
··· 1 - import { 2 - type ValidationResult, 3 - Validator, 4 - type ValidatorContext, 5 - } from "../validation.ts"; 6 - 7 - export type BooleanSchemaOptions = { 8 - default?: boolean; 9 - }; 10 - 11 - export class BooleanSchema extends Validator<boolean> { 12 - override readonly lexiconType = "boolean" as const; 13 - 14 - constructor(readonly options: BooleanSchemaOptions) { 15 - super(); 16 - } 17 - 18 - override validateInContext( 19 - input: unknown = this.options.default, 20 - ctx: ValidatorContext, 21 - ): ValidationResult<boolean> { 22 - if (typeof input === "boolean") { 23 - return ctx.success(input); 24 - } 25 - 26 - return ctx.issueInvalidType(input, "boolean"); 27 - } 28 - }
-42
lex/schema/schema/bytes.ts
··· 1 - import { asUint8Array } from "@atp/data"; 2 - import { 3 - type ValidationResult, 4 - Validator, 5 - type ValidatorContext, 6 - } from "../validation.ts"; 7 - 8 - export type BytesSchemaOptions = { 9 - minLength?: number; 10 - maxLength?: number; 11 - }; 12 - 13 - export class BytesSchema extends Validator<Uint8Array> { 14 - override readonly lexiconType = "bytes" as const; 15 - 16 - constructor(readonly options: BytesSchemaOptions) { 17 - super(); 18 - } 19 - 20 - override validateInContext( 21 - input: unknown, 22 - ctx: ValidatorContext, 23 - ): ValidationResult<Uint8Array> { 24 - // Coerce different binary formats into Uint8Array 25 - const bytes = asUint8Array(input); 26 - if (!bytes) { 27 - return ctx.issueInvalidType(input, "bytes"); 28 - } 29 - 30 - const { minLength } = this.options; 31 - if (minLength != null && bytes.length < minLength) { 32 - return ctx.issueTooSmall(bytes, "bytes", minLength, bytes.length); 33 - } 34 - 35 - const { maxLength } = this.options; 36 - if (maxLength != null && bytes.length > maxLength) { 37 - return ctx.issueTooBig(bytes, "bytes", maxLength, bytes.length); 38 - } 39 - 40 - return ctx.success(bytes); 41 - } 42 - }
-31
lex/schema/schema/cid.ts
··· 1 - import { CID, isCid } from "@atp/data"; 2 - import { 3 - type ValidationResult, 4 - Validator, 5 - type ValidatorContext, 6 - } from "../validation.ts"; 7 - 8 - export { CID }; 9 - 10 - export type CidSchemaOptions = { 11 - strict?: boolean; 12 - }; 13 - 14 - export class CidSchema extends Validator<CID> { 15 - override readonly lexiconType = "cid-link" as const; 16 - 17 - constructor(readonly options: CidSchemaOptions = {}) { 18 - super(); 19 - } 20 - 21 - override validateInContext( 22 - input: unknown, 23 - ctx: ValidatorContext, 24 - ): ValidationResult<CID> { 25 - if (!isCid(input, this.options)) { 26 - return ctx.issueInvalidType(input, "cid"); 27 - } 28 - 29 - return ctx.success(input); 30 - } 31 - }
-36
lex/schema/schema/custom.ts
··· 1 - import type { PropertyKey } from "../validation/property-key.ts"; 2 - import { 3 - type ContextualIssue, 4 - type ValidationResult, 5 - Validator, 6 - type ValidatorContext, 7 - } from "../validation/validator.ts"; 8 - 9 - export type CustomAssertionContext = { 10 - path: PropertyKey[]; 11 - addIssue(issue: ContextualIssue): void; 12 - }; 13 - 14 - export type CustomAssertion<T = unknown> = ( 15 - this: null, 16 - input: unknown, 17 - ctx: CustomAssertionContext, 18 - ) => input is T; 19 - 20 - export class CustomSchema<T = unknown> extends Validator<T> { 21 - constructor( 22 - private readonly assertion: CustomAssertion<T>, 23 - private readonly message: string, 24 - private readonly path?: PropertyKey | readonly PropertyKey[], 25 - ) { 26 - super(); 27 - } 28 - 29 - override validateInContext( 30 - input: unknown, 31 - ctx: ValidatorContext, 32 - ): ValidationResult<T> { 33 - if (this.assertion.call(null, input, ctx)) return ctx.success(input as T); 34 - return ctx.custom(input, this.message, this.path); 35 - } 36 - }
-67
lex/schema/schema/dict.ts
··· 1 - import { isPlainObject } from "@atp/data"; 2 - import { 3 - type Infer, 4 - type ValidationResult, 5 - Validator, 6 - type ValidatorContext, 7 - } from "../validation.ts"; 8 - 9 - export type DictSchemaOutput< 10 - KeySchema extends Validator, 11 - ValueSchema extends Validator, 12 - > = Infer<KeySchema> extends never ? Record<string, never> 13 - : Record<Infer<KeySchema> & string, Infer<ValueSchema>>; 14 - 15 - /** 16 - * @note There is no dictionary in Lexicon schemas. This is a custom extension 17 - * to allow map-like objects when using the lex library programmatically (i.e. 18 - * not code generated from a lexicon schema). 19 - */ 20 - export class DictSchema< 21 - const KeySchema extends Validator, 22 - const ValueSchema extends Validator, 23 - > extends Validator<DictSchemaOutput<KeySchema, ValueSchema>> { 24 - constructor( 25 - readonly keySchema: KeySchema, 26 - readonly valueSchema: ValueSchema, 27 - ) { 28 - super(); 29 - } 30 - 31 - override validateInContext( 32 - input: unknown, 33 - ctx: ValidatorContext, 34 - options?: { ignoredKeys?: { has(k: string): boolean } }, 35 - ): ValidationResult<DictSchemaOutput<KeySchema, ValueSchema>> { 36 - if (!isPlainObject(input)) { 37 - return ctx.issueInvalidType(input, "dict"); 38 - } 39 - 40 - let copy: undefined | Record<string, unknown>; 41 - 42 - for (const key in input) { 43 - if (options?.ignoredKeys?.has(key)) continue; 44 - 45 - const keyResult = ctx.validate(key, this.keySchema); 46 - if (!keyResult.success) return keyResult; 47 - if (keyResult.value !== key) { 48 - // We can't safely "move" the key to a different name in the output 49 - // object (because there may already be something there), so we issue a 50 - // "required key" error if the key validation changes the key 51 - return ctx.issueRequiredKey(input, key); 52 - } 53 - 54 - const valueResult = ctx.validateChild(input, key, this.valueSchema); 55 - if (!valueResult.success) return valueResult; 56 - 57 - if (valueResult.value !== input[key]) { 58 - copy ??= { ...input }; 59 - copy[key] = valueResult.value; 60 - } 61 - } 62 - 63 - return ctx.success( 64 - (copy ?? input) as DictSchemaOutput<KeySchema, ValueSchema>, 65 - ); 66 - } 67 - }
-143
lex/schema/schema/discriminated-union.ts
··· 1 - import { isPlainObject } from "@atp/data"; 2 - import type { ArrayContaining } from "../core.ts"; 3 - import { 4 - ValidationError, 5 - type ValidationFailure, 6 - type ValidationResult, 7 - Validator, 8 - type ValidatorContext, 9 - } from "../validation.ts"; 10 - import { EnumSchema } from "./enum.ts"; 11 - import { LiteralSchema } from "./literal.ts"; 12 - import type { ObjectSchema } from "./object.ts"; 13 - 14 - export type DiscriminatedUnionSchemaVariant<Discriminator extends string> = 15 - ObjectSchema< 16 - { [_ in Discriminator]: Validator }, 17 - { required: ArrayContaining<Discriminator, string> } 18 - >; 19 - 20 - export type DiscriminatedUnionSchemaVariants<Discriminator extends string> = 21 - readonly [ 22 - DiscriminatedUnionSchemaVariant<Discriminator>, 23 - ...DiscriminatedUnionSchemaVariant<Discriminator>[], 24 - ]; 25 - 26 - export type DiscriminatedUnionSchemaOutput< 27 - Options extends readonly Validator[], 28 - > = Options extends readonly [Validator<infer V>] ? V 29 - : Options extends readonly [ 30 - Validator<infer V>, 31 - ...infer Rest extends Validator[], 32 - ] ? V | DiscriminatedUnionSchemaOutput<Rest> 33 - : never; 34 - 35 - /** 36 - * @note There is no discriminated union in Lexicon schemas. This is a custom 37 - * extension to allow optimized validation of union of objects when using the 38 - * lex library programmatically (i.e. not code generated from a lexicon schema). 39 - */ 40 - export class DiscriminatedUnionSchema< 41 - const Discriminator extends string = string, 42 - const Options extends DiscriminatedUnionSchemaVariants<Discriminator> = 43 - DiscriminatedUnionSchemaVariants<Discriminator>, 44 - > extends Validator<DiscriminatedUnionSchemaOutput<Options>> { 45 - constructor( 46 - readonly discriminator: Discriminator, 47 - readonly variants: Options, 48 - ) { 49 - super(); 50 - } 51 - 52 - /** 53 - * If all variants have a literal or enum for the discriminator property, 54 - * and there are no overlapping values, returns a map of discriminator values 55 - * to variants. Otherwise, returns null. 56 - */ 57 - protected get variantsMap() { 58 - const map = new Map< 59 - unknown, 60 - DiscriminatedUnionSchemaVariant<Discriminator> 61 - >(); 62 - for (const variant of this.variants) { 63 - const schema = variant.validators[this.discriminator]; 64 - if (schema instanceof LiteralSchema) { 65 - if (map.has(schema.value)) return null; // overlapping value 66 - map.set(schema.value, variant); 67 - } else if (schema instanceof EnumSchema) { 68 - for (const val of schema.values) { 69 - if (map.has(val)) return null; // overlapping value 70 - map.set(val, variant); 71 - } 72 - } else { 73 - return null; // not a literal or enum 74 - } 75 - } 76 - 77 - // Cache the map on the instance (to avoid re-computing) 78 - Object.defineProperty(this, "variantsMap", { 79 - value: map, 80 - writable: false, 81 - enumerable: false, 82 - configurable: true, 83 - }); 84 - 85 - return map; 86 - } 87 - 88 - override validateInContext( 89 - input: unknown, 90 - ctx: ValidatorContext, 91 - ): ValidationResult<DiscriminatedUnionSchemaOutput<Options>> { 92 - if (!isPlainObject(input)) { 93 - return ctx.issueInvalidType(input, "object"); 94 - } 95 - 96 - if (!Object.hasOwn(input, this.discriminator)) { 97 - return ctx.issueRequiredKey(input, this.discriminator); 98 - } 99 - 100 - // Fast path: if we have a mapping of discriminator values to variants, 101 - // we can directly select the correct variant to validate against. This also 102 - // outputs a better error (with a single failure issue) when the discriminator. 103 - if (this.variantsMap) { 104 - const variant = this.variantsMap.get(input[this.discriminator]); 105 - if (!variant) { 106 - return ctx.issueInvalidPropertyValue(input, this.discriminator, [ 107 - ...this.variantsMap.keys(), 108 - ]); 109 - } 110 - 111 - return ctx.validate(input, variant) as ValidationResult< 112 - DiscriminatedUnionSchemaOutput<Options> 113 - >; 114 - } 115 - 116 - // Slow path: try validating against each variant and return the first 117 - // successful one (or aggregate all failures if none match). 118 - const failures: ValidationFailure[] = []; 119 - 120 - for (const variant of this.variants) { 121 - const discSchema = variant.validators[this.discriminator]; 122 - const discResult = ctx.validateChild( 123 - input, 124 - this.discriminator, 125 - discSchema, 126 - ); 127 - 128 - if (!discResult.success) { 129 - failures.push(discResult); 130 - continue; 131 - } 132 - 133 - return ctx.validate(input, variant) as ValidationResult< 134 - DiscriminatedUnionSchemaOutput<Options> 135 - >; 136 - } 137 - 138 - return { 139 - success: false, 140 - error: ValidationError.fromFailures(failures), 141 - }; 142 - } 143 - }
-24
lex/schema/schema/enum.ts
··· 1 - import { 2 - type ValidationResult, 3 - Validator, 4 - type ValidatorContext, 5 - } from "../validation.ts"; 6 - 7 - export class EnumSchema< 8 - Output extends null | string | number | boolean = string, 9 - > extends Validator<Output> { 10 - constructor(readonly values: readonly Output[]) { 11 - super(); 12 - } 13 - 14 - override validateInContext( 15 - input: unknown, 16 - ctx: ValidatorContext, 17 - ): ValidationResult<Output> { 18 - if (!(this.values as readonly unknown[]).includes(input)) { 19 - return ctx.issueInvalidValue(input, this.values); 20 - } 21 - 22 - return ctx.success(input as Output); 23 - } 24 - }
-45
lex/schema/schema/integer.ts
··· 1 - import { 2 - type ValidationResult, 3 - Validator, 4 - type ValidatorContext, 5 - } from "../validation.ts"; 6 - 7 - export type IntegerSchemaOptions = { 8 - default?: number; 9 - minimum?: number; 10 - maximum?: number; 11 - }; 12 - 13 - export class IntegerSchema extends Validator<number> { 14 - override readonly lexiconType = "integer" as const; 15 - 16 - constructor(readonly options: IntegerSchemaOptions) { 17 - super(); 18 - } 19 - 20 - override validateInContext( 21 - input: unknown = this.options.default, 22 - ctx: ValidatorContext, 23 - ): ValidationResult<number> { 24 - if (!isInteger(input)) { 25 - return ctx.issueInvalidType(input, "integer"); 26 - } 27 - 28 - if (this.options.minimum !== undefined && input < this.options.minimum) { 29 - return ctx.issueTooSmall(input, "integer", this.options.minimum, input); 30 - } 31 - 32 - if (this.options.maximum !== undefined && input > this.options.maximum) { 33 - return ctx.issueTooBig(input, "integer", this.options.maximum, input); 34 - } 35 - 36 - return ctx.success(input); 37 - } 38 - } 39 - 40 - /** 41 - * Simple wrapper around {@link Number.isInteger} that acts as a type guard. 42 - */ 43 - function isInteger(input: unknown): input is number { 44 - return Number.isInteger(input); 45 - }
-56
lex/schema/schema/intersection.ts
··· 1 - import { 2 - type Infer, 3 - type ValidationResult, 4 - Validator, 5 - type ValidatorContext, 6 - } from "../validation.ts"; 7 - 8 - export type IntersectionSchemaValidators = readonly [ 9 - Validator, 10 - Validator, 11 - ...Validator[], 12 - ]; 13 - export type IntersectionSchemaOutput< 14 - V extends readonly Validator[], 15 - Base = unknown, 16 - > = V extends readonly [ 17 - infer First extends Validator, 18 - ...infer Rest extends Validator[], 19 - ] ? IntersectionSchemaOutput<Rest, Base & Infer<First>> 20 - : Base; 21 - 22 - export class IntersectionSchema< 23 - V extends IntersectionSchemaValidators = IntersectionSchemaValidators, 24 - > extends Validator<IntersectionSchemaOutput<V>> { 25 - constructor(protected readonly validators: V) { 26 - super(); 27 - } 28 - 29 - override validateInContext( 30 - input: unknown, 31 - ctx: ValidatorContext, 32 - ): ValidationResult<IntersectionSchemaOutput<V>> { 33 - for (let i = 0; i < this.validators.length; i++) { 34 - const result = ctx.validate(input, this.validators[i]); 35 - 36 - if (!result.success) { 37 - return result; 38 - } 39 - 40 - // @NOTE because transforming the value could make it invalid for previous 41 - // validators, we need to ensure the input remains unchanged only gets 42 - // transformed by the first validator. 43 - if (i !== 0 && input !== result.value) { 44 - // The alternative would be to allow transforms on a first pass 45 - // (ignoring errors) and then re-validate the final value against all 46 - // validators (without allowing further transforms). This would be way 47 - // less efficient (we could make this optional). 48 - return ctx.issueInvalidValue(input, [result.value]); 49 - } 50 - 51 - input = result.value; 52 - } 53 - 54 - return ctx.success(input as IntersectionSchemaOutput<V>); 55 - } 56 - }
-24
lex/schema/schema/literal.ts
··· 1 - import { 2 - type ValidationResult, 3 - Validator, 4 - type ValidatorContext, 5 - } from "../validation.ts"; 6 - 7 - export class LiteralSchema< 8 - Output extends null | string | number | boolean = string, 9 - > extends Validator<Output> { 10 - constructor(readonly value: Output) { 11 - super(); 12 - } 13 - 14 - override validateInContext( 15 - input: unknown, 16 - ctx: ValidatorContext, 17 - ): ValidationResult<Output> { 18 - if (input !== this.value) { 19 - return ctx.issueInvalidValue(input, [this.value]); 20 - } 21 - 22 - return ctx.success(this.value); 23 - } 24 - }
-14
lex/schema/schema/never.ts
··· 1 - import { 2 - type ValidationFailure, 3 - Validator, 4 - type ValidatorContext, 5 - } from "../validation.ts"; 6 - 7 - export class NeverSchema extends Validator<never> { 8 - override validateInContext( 9 - input: unknown, 10 - ctx: ValidatorContext, 11 - ): ValidationFailure { 12 - return ctx.issueInvalidType(input, "never"); 13 - } 14 - }
-24
lex/schema/schema/null.ts
··· 1 - import { 2 - type ValidationResult, 3 - Validator, 4 - type ValidatorContext, 5 - } from "../validation.ts"; 6 - 7 - export class NullSchema extends Validator<null> { 8 - override readonly lexiconType = "null" as const; 9 - 10 - constructor() { 11 - super(); 12 - } 13 - 14 - override validateInContext( 15 - input: unknown, 16 - ctx: ValidatorContext, 17 - ): ValidationResult<null> { 18 - if (input !== null) { 19 - return ctx.issueInvalidType(input, "null"); 20 - } 21 - 22 - return ctx.success(null); 23 - } 24 - }
-178
lex/schema/schema/object.ts
··· 1 - import { isPlainObject } from "@atp/data"; 2 - import type { Simplify } from "../core.ts"; 3 - import { 4 - type Infer, 5 - type ValidationResult, 6 - Validator, 7 - type ValidatorContext, 8 - } from "../validation.ts"; 9 - import type { DictSchema } from "./dict.ts"; 10 - 11 - export type ObjectSchemaProperties = { [_ in string]: Validator }; 12 - export type ObjectSchemaOptions = { 13 - required?: readonly string[]; 14 - nullable?: readonly string[]; 15 - unknownProperties?: "strict" | DictSchema<Validator, Validator>; 16 - }; 17 - 18 - export type ObjectSchemaNullValue< 19 - O extends ObjectSchemaOptions, 20 - K extends string, 21 - > = O extends { nullable: readonly (infer N extends string)[] } 22 - ? K extends N ? null 23 - : never 24 - : never; 25 - 26 - export type ObjectSchemaPropertiesOutput< 27 - P extends ObjectSchemaProperties, 28 - O extends ObjectSchemaOptions, 29 - > = O extends { required: readonly (infer R extends string)[] } ? 30 - & { 31 - -readonly [K in string & keyof P & R]-?: 32 - | Infer<P[K]> 33 - | ObjectSchemaNullValue<O, K>; 34 - } 35 - & { 36 - -readonly [K in Exclude<string & keyof P, R>]?: 37 - | Infer<P[K]> 38 - | ObjectSchemaNullValue<O, K>; 39 - } 40 - : { 41 - -readonly [K in string & keyof P]?: 42 - | Infer<P[K]> 43 - | ObjectSchemaNullValue<O, K>; 44 - }; 45 - 46 - /** 47 - * Allows to more accurately represent the intersection of two object types 48 - * where both types may share some keys, and one of them uses an index 49 - * signature. 50 - * 51 - * @see {@link https://www.typescriptlang.org/play/?#code/C4TwDgpgBAglC8UDeUBmB7dAuKByARgIYBOuUAvlAGTJQDaA+lAJYB2UAzsMWwOYC6OVgFcAtvgjEKAKGkATCAGMANiWiL0rLlEI4YsjVuBQA1hBA4uPVrwRQARBnT2Dm7QDdCy4dESE6ZiD8UAD0IVAi4pJQABQcABbowspyUBIORMT2AJSyEAAeYOjExqCQUACSrMCSHErAzJoAPNJQsFAFNaxyHFAASkrFck1WfAA0UMKsJqzoAO6sAHxjrVAAQh35XT39g8TDozYTUzPzSyuLdqtwVKttMYHoqO00j88bnRDdvawQ7pJ3NpQAD860BbRwSHBQLadAA0ix2G91oJ1vDggAfWABcxPF5QOH8aFtci5aRlaAwVDMfIQVKIKo1Yh1RQNZq0Jw4AgkMjkCYoRiIzjcPioyISKTkRayBQqNRQQzaQgAMRpdL01NpclcRignm8EFVWrsKrVchxQVC4XF0SxmSAA Playground link} 52 - */ 53 - type Intersect< 54 - A extends Record<string, unknown>, 55 - B extends Record<string, unknown>, 56 - > = B[keyof B] extends never ? A 57 - : keyof A & keyof B extends never 58 - // If A and B don't overlap, just return A & B 59 - ? A & B 60 - // Otherwise, properly represent the fact that accessing using an 61 - // index signature could return a value from either A or B 62 - : A & { [K in keyof B]: B[K] | A[keyof A & K] }; 63 - 64 - export type ObjectSchemaOutput< 65 - P extends ObjectSchemaProperties, 66 - O extends ObjectSchemaOptions, 67 - > = O extends { 68 - unknownProperties: Validator<infer D extends Record<string, unknown>>; 69 - } ? Simplify<Intersect<ObjectSchemaPropertiesOutput<P, O>, D>> 70 - : Simplify<ObjectSchemaPropertiesOutput<P, O>>; 71 - 72 - export class ObjectSchema< 73 - const Validators extends ObjectSchemaProperties = ObjectSchemaProperties, 74 - const Options extends ObjectSchemaOptions = ObjectSchemaOptions, 75 - const Output extends ObjectSchemaOutput< 76 - Validators, 77 - Options 78 - > = ObjectSchemaOutput<Validators, Options>, 79 - > extends Validator<Output> { 80 - constructor( 81 - readonly validators: Validators, 82 - readonly options: Options, 83 - ) { 84 - super(); 85 - } 86 - 87 - get validatorsMap(): Map<string, Validator> { 88 - const map = new Map(Object.entries(this.validators)); 89 - 90 - // Cache the map on the instance (to avoid re-creating it) 91 - Object.defineProperty(this, "validatorsMap", { 92 - value: map, 93 - writable: false, 94 - enumerable: false, 95 - configurable: true, 96 - }); 97 - 98 - return map; 99 - } 100 - 101 - override validateInContext( 102 - input: unknown, 103 - ctx: ValidatorContext, 104 - ): ValidationResult<Output> { 105 - if (!isPlainObject(input)) { 106 - return ctx.issueInvalidType(input, ["object"]); 107 - } 108 - 109 - // Lazily copy value 110 - let copy: undefined | Record<string, unknown>; 111 - 112 - for (const [key, propDef] of this.validatorsMap) { 113 - if (input[key] === null && this.options.nullable?.includes(key)) { 114 - continue; 115 - } 116 - 117 - const result = ctx.validateChild(input, key, propDef); 118 - if (!result.success) { 119 - // Because default values are provided by child validators, we need to 120 - // run the validator to get the default value and, in case of failure, 121 - // ignore validation error that were caused by missing keys. 122 - if (!(key in input)) { 123 - if (this.options.required?.includes(key)) { 124 - // Transform into "required key" issue 125 - return ctx.issueRequiredKey(input, key); 126 - } 127 - 128 - // Ignore missing non-required key 129 - continue; 130 - } 131 - 132 - return result; 133 - } else if (result.value === undefined) { 134 - // Special case for validators that output "undefined" values (typically 135 - // UnknownSchema) since they cannot differentiate between "missing key" 136 - // and "key with undefined value" 137 - 138 - if (!(key in input)) { 139 - // Input was missing the key (was "undefined") 140 - if (this.options.required?.includes(key)) { 141 - return ctx.issueRequiredKey(input, key); 142 - } 143 - 144 - // Ignore missing non-required key 145 - continue; 146 - } 147 - 148 - // if "key" existed in input (would typically be "undefined"), we keep 149 - // it as-is by continuing processing as if it was any other value. 150 - } 151 - 152 - if (result.value !== input[key]) { 153 - copy ??= { ...input }; 154 - copy[key] = result.value; 155 - } 156 - } 157 - 158 - if (this.options.unknownProperties === "strict") { 159 - for (const key of Object.keys(input)) { 160 - if (!this.validatorsMap.has(key)) { 161 - return ctx.issueInvalidPropertyType(input, key, "undefined"); 162 - } 163 - } 164 - } else if (this.options.unknownProperties) { 165 - const result = this.options.unknownProperties.validateInContext( 166 - copy ?? input, 167 - ctx, 168 - { ignoredKeys: this.validatorsMap }, 169 - ); 170 - if (!result.success) return result; 171 - if (result.value !== input) copy = result.value; 172 - } 173 - 174 - const output = (copy ?? input) as Output; 175 - 176 - return ctx.success(output); 177 - } 178 - }
-160
lex/schema/schema/params.ts
··· 1 - import { isPlainObject } from "@atp/data"; 2 - import { 3 - type ValidationResult, 4 - Validator, 5 - type ValidatorContext, 6 - } from "../validation.ts"; 7 - import { type Param, type ParamScalar, paramSchema } from "./_parameters.ts"; 8 - import type { ObjectSchemaPropertiesOutput } from "./object.ts"; 9 - import { StringSchema } from "./string.ts"; 10 - 11 - export type ParamsSchemaProperties = { 12 - [_ in string]: Validator<Param>; 13 - }; 14 - 15 - export type ParamsSchemaOptions = { 16 - required?: readonly string[]; 17 - }; 18 - 19 - export type ParamsSchemaOutput< 20 - P extends ParamsSchemaProperties, 21 - O extends ParamsSchemaOptions, 22 - > = ObjectSchemaPropertiesOutput<P, O>; 23 - 24 - export type InferParamsSchema<T> = T extends ParamsSchema<infer P, infer O> 25 - ? NonNullable<unknown> extends ParamsSchemaOutput<P, O> 26 - ? ParamsSchemaOutput<P, O> | undefined 27 - : ParamsSchemaOutput<P, O> 28 - : never; 29 - 30 - export class ParamsSchema< 31 - const Validators extends ParamsSchemaProperties = ParamsSchemaProperties, 32 - const Options extends ParamsSchemaOptions = ParamsSchemaOptions, 33 - Output extends ParamsSchemaOutput<Validators, Options> = ParamsSchemaOutput< 34 - Validators, 35 - Options 36 - >, 37 - > extends Validator<Output> { 38 - override readonly lexiconType = "params" as const; 39 - 40 - constructor( 41 - readonly validators: Validators, 42 - readonly options: Options, 43 - ) { 44 - super(); 45 - } 46 - 47 - get validatorsMap(): Map<string, Validator<Param>> { 48 - const map = new Map(Object.entries(this.validators)); 49 - 50 - // Cache the map on the instance (to avoid re-creating it) 51 - Object.defineProperty(this, "validatorsMap", { 52 - value: map, 53 - writable: false, 54 - enumerable: false, 55 - configurable: true, 56 - }); 57 - 58 - return map; 59 - } 60 - 61 - override validateInContext( 62 - input: unknown = {}, 63 - ctx: ValidatorContext, 64 - ): ValidationResult<Output> { 65 - if (!isPlainObject(input)) { 66 - return ctx.issueInvalidType(input, "object"); 67 - } 68 - 69 - // Lazily copy value 70 - let copy: undefined | Record<string, unknown>; 71 - 72 - // Ensure that non-specified params conform to param schema 73 - for (const key in input) { 74 - if (this.validatorsMap.has(key)) continue; 75 - 76 - const result = ctx.validateChild(input, key, paramSchema); 77 - if (!result.success) return result; 78 - 79 - if (result.value !== input[key]) { 80 - copy ??= { ...input }; 81 - copy[key] = result.value; 82 - } 83 - } 84 - 85 - for (const [key, propDef] of this.validatorsMap) { 86 - const result = ctx.validateChild(input, key, propDef); 87 - if (!result.success) { 88 - // Because default values are provided by child validators, we need to 89 - // run the validator to get the default value and, in case of failure, 90 - // ignore validation error that were caused by missing keys. 91 - if (!(key in input)) { 92 - if (!this.options.required?.includes(key)) { 93 - // Ignore missing non-required key 94 - continue; 95 - } else { 96 - // Transform into "required key" issue 97 - return ctx.issueRequiredKey(input, key); 98 - } 99 - } 100 - 101 - return result; 102 - } 103 - 104 - if (result.value !== input[key]) { 105 - // Copy on write 106 - copy ??= { ...input }; 107 - copy![key] = result.value; 108 - } 109 - } 110 - 111 - return ctx.success((copy ?? input) as Output); 112 - } 113 - 114 - fromURLSearchParams(urlSearchParams: URLSearchParams): Output { 115 - const params: Record<string, Param> = {}; 116 - 117 - for (const [key, value] of urlSearchParams.entries()) { 118 - const validator = this.validatorsMap.get(key); 119 - 120 - const coerced: ParamScalar = 121 - validator != null && validator instanceof StringSchema 122 - ? value 123 - : value === "true" 124 - ? true 125 - : value === "false" 126 - ? false 127 - : /^-?\d+$/.test(value) 128 - ? Number(value) 129 - : value; 130 - 131 - if (params[key] === undefined) { 132 - params[key] = coerced; 133 - } else if (Array.isArray(params[key])) { 134 - params[key].push(coerced); 135 - } else { 136 - params[key] = [params[key] as ParamScalar, coerced]; 137 - } 138 - } 139 - 140 - return this.parse(params); 141 - } 142 - 143 - toURLSearchParams(input: Output): URLSearchParams { 144 - const urlSearchParams = new URLSearchParams(); 145 - 146 - if (input !== undefined) { 147 - for (const [key, value] of Object.entries(input)) { 148 - if (Array.isArray(value)) { 149 - for (const v of value) { 150 - urlSearchParams.append(key, String(v)); 151 - } 152 - } else if (value !== undefined) { 153 - urlSearchParams.append(key, String(value)); 154 - } 155 - } 156 - } 157 - 158 - return urlSearchParams; 159 - } 160 - }
-50
lex/schema/schema/payload.ts
··· 1 - import type { LexValue } from "@atp/data"; 2 - import type { Validator } from "../validation.ts"; 3 - 4 - export type LexBody<E extends string = string> = E extends `text/${string}` 5 - ? string // Text encodings always yield string bodies 6 - : E extends "application/json" ? LexValue 7 - : Uint8Array; 8 - 9 - export type InferPayloadEncoding<P extends Payload> = P extends Payload<infer E> 10 - ? E 11 - : undefined; 12 - 13 - export type InferPayloadBody<P extends Payload> = P extends 14 - Payload<string, infer S> ? S extends Validator<infer V> ? V 15 - : P extends Payload<infer E extends string> ? LexBody<E> 16 - : undefined 17 - : undefined; 18 - 19 - export type PayloadOutput< 20 - E extends string | undefined = string, 21 - S extends Validator | undefined = Validator, 22 - > = E extends string ? S extends Validator<infer V> ? { 23 - encoding: E; 24 - body: V; 25 - } 26 - : { 27 - encoding: E; 28 - body: LexBody<E>; 29 - } 30 - : void; 31 - 32 - export type PayloadBody<E extends string | undefined> = E extends undefined 33 - ? undefined 34 - : Validator | undefined; 35 - 36 - export class Payload< 37 - const Encoding extends string | undefined = string | undefined, 38 - const Body extends PayloadBody<Encoding> = PayloadBody<Encoding>, 39 - > { 40 - constructor( 41 - readonly encoding: Encoding, 42 - readonly schema: Body, 43 - ) { 44 - if (encoding === undefined && schema !== undefined) { 45 - throw new TypeError( 46 - "schema cannot be defined when encoding is undefined", 47 - ); 48 - } 49 - } 50 - }
-22
lex/schema/schema/permission-set.ts
··· 1 - import type { Permission } from "./permission.ts"; 2 - 3 - export type PermissionSetOptions = { 4 - title?: string; 5 - "title:lang"?: Record<string, undefined | string>; 6 - detail?: string; 7 - "detail:lang"?: Record<string, undefined | string>; 8 - }; 9 - 10 - export class PermissionSet< 11 - const Nsid extends string = string, 12 - const Permissions extends readonly Permission[] = readonly Permission[], 13 - const Options extends PermissionSetOptions = PermissionSetOptions, 14 - > { 15 - readonly lexiconType = "permission-set" as const; 16 - 17 - constructor( 18 - readonly nsid: Nsid, 19 - readonly permissions: Permissions, 20 - readonly options: Options, 21 - ) {} 22 - }
-15
lex/schema/schema/permission.ts
··· 1 - import type { Params } from "./_parameters.ts"; 2 - 3 - export type PermissionOptions = Params; 4 - 5 - export class Permission< 6 - const Resource extends string = string, 7 - const Options extends PermissionOptions = PermissionOptions, 8 - > { 9 - readonly lexiconType = "permission" as const; 10 - 11 - constructor( 12 - readonly resource: Resource, 13 - readonly options: Options, 14 - ) {} 15 - }
-34
lex/schema/schema/procedure.ts
··· 1 - import type { Nsid } from "../core.ts"; 2 - import type { Infer } from "../validation.ts"; 3 - import type { ParamsSchema } from "./params.ts"; 4 - import type { InferPayloadBody, Payload } from "./payload.ts"; 5 - 6 - export type InferProcedureParameters<Q extends Procedure> = Q extends 7 - Procedure<Nsid, infer P extends ParamsSchema> ? Infer<P> : never; 8 - 9 - export type InferProcedureInputBody<Q extends Procedure> = Q extends 10 - Procedure<Nsid, ParamsSchema, infer I extends Payload> ? InferPayloadBody<I> 11 - : never; 12 - 13 - export type InferProcedureOutputBody<Q extends Procedure> = Q extends 14 - Procedure<Nsid, ParamsSchema, Payload, infer O extends Payload> 15 - ? InferPayloadBody<O> 16 - : never; 17 - 18 - export class Procedure< 19 - N extends Nsid = Nsid, 20 - P extends ParamsSchema = ParamsSchema, 21 - I extends Payload = Payload, 22 - O extends Payload = Payload, 23 - E extends undefined | readonly string[] = undefined, 24 - > { 25 - readonly lexiconType = "procedure" as const; 26 - 27 - constructor( 28 - readonly nsid: N, 29 - readonly parameters: P, 30 - readonly input: I, 31 - readonly output: O, 32 - readonly errors: E, 33 - ) {} 34 - }
-27
lex/schema/schema/query.ts
··· 1 - import type { Nsid } from "../core.ts"; 2 - import type { Infer } from "../validation.ts"; 3 - import type { ParamsSchema } from "./params.ts"; 4 - import type { InferPayloadBody, Payload } from "./payload.ts"; 5 - 6 - export type InferQueryParameters<Q extends Query> = Q extends 7 - Query<Nsid, infer P extends ParamsSchema> ? Infer<P> : never; 8 - 9 - export type InferQueryOutputBody<Q extends Query> = Q extends 10 - Query<Nsid, ParamsSchema, infer O extends Payload> ? InferPayloadBody<O> 11 - : never; 12 - 13 - export class Query< 14 - N extends Nsid = Nsid, 15 - P extends ParamsSchema = ParamsSchema, 16 - O extends Payload = Payload, 17 - E extends undefined | readonly string[] = undefined, 18 - > { 19 - readonly lexiconType = "query" as const; 20 - 21 - constructor( 22 - readonly nsid: N, 23 - readonly parameters: P, 24 - readonly output: O, 25 - readonly errors: E, 26 - ) {} 27 - }
-108
lex/schema/schema/record.ts
··· 1 - import type { Nsid, RecordKey, Simplify } from "../core.ts"; 2 - import { 3 - type Infer, 4 - type ValidationResult, 5 - Validator, 6 - type ValidatorContext, 7 - } from "../validation.ts"; 8 - import { LiteralSchema } from "./literal.ts"; 9 - import { StringSchema } from "./string.ts"; 10 - 11 - export type InferRecordKey<R extends RecordSchema> = R extends RecordSchema< 12 - infer K, 13 - Nsid, 14 - Validator<object>, 15 - Infer<Validator<object>> & { $type: Nsid } 16 - > ? RecordKeySchemaOutput<K> 17 - : never; 18 - 19 - export class RecordSchema< 20 - Key extends RecordKey = RecordKey, 21 - Type extends Nsid = Nsid, 22 - Schema extends Validator<object> = Validator<object>, 23 - Output extends Infer<Schema> & { $type: Type } = Infer<Schema> & { 24 - $type: Type; 25 - }, 26 - > extends Validator<Output> { 27 - override readonly lexiconType = "record" as const; 28 - 29 - keySchema: RecordKeySchema<Key>; 30 - 31 - constructor( 32 - readonly key: Key, 33 - readonly $type: Type, 34 - readonly schema: Schema, 35 - ) { 36 - super(); 37 - this.keySchema = recordKey(key); 38 - } 39 - 40 - isTypeOf<X extends { $type?: unknown }>( 41 - value: X, 42 - ): value is X extends { $type: Type } ? X : never { 43 - return value.$type === this.$type; 44 - } 45 - 46 - build<X extends Omit<Output, "$type">>( 47 - input: X, 48 - ): Simplify<Omit<X, "$type"> & { $type: Type }> { 49 - return { ...input, $type: this.$type }; 50 - } 51 - 52 - $isTypeOf<X extends { $type?: unknown }>(value: X) { 53 - return this.isTypeOf<X>(value); 54 - } 55 - 56 - $build<X extends Omit<Output, "$type">>(input: X) { 57 - return this.build<X>(input); 58 - } 59 - 60 - override validateInContext( 61 - input: unknown, 62 - ctx: ValidatorContext, 63 - ): ValidationResult<Output> { 64 - const result = ctx.validate(input, this.schema) as ValidationResult<Output>; 65 - 66 - if (!result.success) { 67 - return result; 68 - } 69 - 70 - if (this.$type !== result.value.$type) { 71 - return ctx.issueInvalidPropertyValue(result.value, "$type", [this.$type]); 72 - } 73 - 74 - return result; 75 - } 76 - } 77 - 78 - export type RecordKeySchemaOutput<Key extends RecordKey> = Key extends "any" 79 - ? string 80 - : Key extends "tid" ? string 81 - : Key extends "nsid" ? Nsid 82 - : Key extends `literal:${infer L extends string}` ? L 83 - : never; 84 - 85 - export type RecordKeySchema<Key extends RecordKey> = Validator< 86 - RecordKeySchemaOutput<Key> 87 - >; 88 - 89 - const keySchema = new StringSchema({ minLength: 1 }); 90 - const tidSchema = new StringSchema({ format: "tid" }); 91 - const nsidSchema = new StringSchema({ format: "nsid" }); 92 - const selfLiteralSchema = new LiteralSchema("self"); 93 - 94 - function recordKey<Key extends RecordKey>(key: Key): RecordKeySchema<Key> { 95 - // @NOTE Use cached instances for common schemas 96 - if (key === "any") return keySchema as unknown as RecordKeySchema<Key>; 97 - if (key === "tid") return tidSchema as unknown as RecordKeySchema<Key>; 98 - if (key === "nsid") return nsidSchema as unknown as RecordKeySchema<Key>; 99 - if (key.startsWith("literal:")) { 100 - const value = key.slice(8) as RecordKeySchemaOutput<Key>; 101 - if (value === "self") { 102 - return selfLiteralSchema as unknown as RecordKeySchema<Key>; 103 - } 104 - return new LiteralSchema(value); 105 - } 106 - 107 - throw new Error(`Unsupported record key type: ${key}`); 108 - }
-51
lex/schema/schema/ref.ts
··· 1 - import { 2 - type ValidationResult, 3 - Validator, 4 - type ValidatorContext, 5 - } from "../validation.ts"; 6 - 7 - export type RefSchemaGetter<V> = () => Validator<V>; 8 - 9 - export class RefSchema<V = unknown> extends Validator<V> { 10 - override readonly lexiconType = "ref" as const; 11 - 12 - #getter: RefSchemaGetter<V>; 13 - 14 - constructor(getter: RefSchemaGetter<V>) { 15 - // @NOTE In order to avoid circular dependency issues, we don't resolve 16 - // the schema here. Instead, we resolve it lazily when first accessed. 17 - 18 - super(); 19 - 20 - this.#getter = getter; 21 - } 22 - 23 - get schema(): Validator<V> { 24 - const value = this.#getter.call(null); 25 - 26 - // Prevents a getter from depending on itself recursively, also allows GC to 27 - // clean up the getter function. 28 - this.#getter = throwAlreadyCalled; 29 - 30 - // Disable the getter and cache the resolved schema on the instance 31 - Object.defineProperty(this, "schema", { 32 - value, 33 - writable: false, 34 - enumerable: false, 35 - configurable: true, 36 - }); 37 - 38 - return value; 39 - } 40 - 41 - validateInContext( 42 - input: unknown, 43 - ctx: ValidatorContext, 44 - ): ValidationResult<V> { 45 - return ctx.validate(input, this.schema); 46 - } 47 - } 48 - 49 - function throwAlreadyCalled(): never { 50 - throw new Error("RefSchema getter called multiple times"); 51 - }
-143
lex/schema/schema/string.ts
··· 1 - import { CID, graphemeLen, utf8Len } from "@atp/data"; 2 - import { 3 - assertStringFormat, 4 - type InferStringFormat, 5 - type StringFormat, 6 - type UnknownString, 7 - } from "../core.ts"; 8 - import { 9 - type ValidationResult, 10 - Validator, 11 - type ValidatorContext, 12 - } from "../validation.ts"; 13 - import { TokenSchema } from "./token.ts"; 14 - 15 - export type StringSchemaOptions = { 16 - default?: string; 17 - knownValues?: readonly string[]; 18 - format?: StringFormat; 19 - minLength?: number; 20 - maxLength?: number; 21 - minGraphemes?: number; 22 - maxGraphemes?: number; 23 - }; 24 - 25 - export type StringSchemaOutput<Options> = 26 - // 27 - Options extends { format: infer F extends StringFormat } 28 - ? InferStringFormat<F> 29 - : Options extends { knownValues: readonly (infer K extends string)[] } 30 - ? K | UnknownString 31 - : string; 32 - 33 - export class StringSchema< 34 - const Options extends StringSchemaOptions = StringSchemaOptions, 35 - > extends Validator<StringSchemaOutput<Options>> { 36 - override readonly lexiconType = "string" as const; 37 - 38 - constructor(readonly options: Options) { 39 - super(); 40 - } 41 - 42 - override validateInContext( 43 - // @NOTE validation will be applied on the default value as well 44 - input: unknown = this.options.default, 45 - ctx: ValidatorContext, 46 - ): ValidationResult<StringSchemaOutput<Options>> { 47 - const { options } = this; 48 - 49 - const str = coerceToString(input); 50 - if (str == null) { 51 - return ctx.issueInvalidType(input, "string"); 52 - } 53 - 54 - let lazyUtf8Len: number; 55 - 56 - const { minLength } = options; 57 - if (minLength != null) { 58 - if ((lazyUtf8Len ??= utf8Len(str)) < minLength) { 59 - return ctx.issueTooSmall(str, "string", minLength, lazyUtf8Len); 60 - } 61 - } 62 - 63 - const { maxLength } = options; 64 - if (maxLength != null) { 65 - // Optimization: we can avoid computing the UTF-8 length if the maximum 66 - // possible length, in bytes, of the input JS string is smaller than the 67 - // maxLength (in UTF-8 string bytes). 68 - if (str.length * 3 <= maxLength) { 69 - // Input string so small it can't possibly exceed maxLength 70 - } else if ((lazyUtf8Len ??= utf8Len(str)) > maxLength) { 71 - return ctx.issueTooBig(str, "string", maxLength, lazyUtf8Len); 72 - } 73 - } 74 - 75 - let lazyGraphLen: number; 76 - 77 - const { minGraphemes } = options; 78 - if (minGraphemes != null) { 79 - // Optimization: avoid counting graphemes if the length check already fails 80 - if (str.length < minGraphemes) { 81 - return ctx.issueTooSmall(str, "grapheme", minGraphemes, str.length); 82 - } else if ((lazyGraphLen ??= graphemeLen(str)) < minGraphemes) { 83 - return ctx.issueTooSmall(str, "grapheme", minGraphemes, lazyGraphLen); 84 - } 85 - } 86 - 87 - const { maxGraphemes } = options; 88 - if (maxGraphemes != null) { 89 - if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) { 90 - return ctx.issueTooBig(str, "grapheme", maxGraphemes, lazyGraphLen); 91 - } 92 - } 93 - 94 - if (options.format !== undefined) { 95 - try { 96 - // @TODO optimize to avoid throw cost (requires re-writing utilities 97 - // from @atproto/syntax) 98 - assertStringFormat(str, options.format); 99 - } catch (err) { 100 - const message = err instanceof Error ? err.message : undefined; 101 - return ctx.issueInvalidFormat(str, options.format, message); 102 - } 103 - } 104 - 105 - return ctx.success(str as StringSchemaOutput<Options>); 106 - } 107 - } 108 - 109 - export function coerceToString(input: unknown): string | null { 110 - switch (typeof input) { 111 - case "string": 112 - return input; 113 - case "object": { 114 - if (input == null) return null; 115 - 116 - // @NOTE Allow using TokenSchema instances in places expecting strings, 117 - // converting them to their string value. 118 - if (input instanceof TokenSchema) { 119 - return input.toString(); 120 - } 121 - 122 - if (input instanceof Date) { 123 - if (Number.isNaN(input.getTime())) return null; 124 - return input.toISOString(); 125 - } 126 - 127 - if (input instanceof URL) { 128 - return input.toString(); 129 - } 130 - 131 - const cid = CID.asCID(input); 132 - if (cid) return cid.toString(); 133 - 134 - if (input instanceof String) { 135 - return input.valueOf(); 136 - } 137 - } 138 - 139 - // falls through 140 - default: 141 - return null; 142 - } 143 - }
-33
lex/schema/schema/subscription.ts
··· 1 - import type { Infer } from "../validation.ts"; 2 - import type { ObjectSchema } from "./object.ts"; 3 - import type { ParamsSchema } from "./params.ts"; 4 - import type { RefSchema } from "./ref.ts"; 5 - import type { TypedUnionSchema } from "./typed-union.ts"; 6 - 7 - export type InferSubscriptionParameters<S extends Subscription> = S extends 8 - Subscription<string, infer P extends ParamsSchema> ? Infer<P> 9 - : never; 10 - 11 - export type InferSubscriptionMessage<S extends Subscription> = S extends 12 - Subscription< 13 - string, 14 - ParamsSchema, 15 - infer M extends RefSchema | TypedUnionSchema | ObjectSchema 16 - > ? Infer<M> 17 - : unknown; 18 - 19 - export class Subscription< 20 - N extends string = string, 21 - P extends ParamsSchema = ParamsSchema, 22 - M extends undefined | RefSchema | TypedUnionSchema | ObjectSchema = undefined, 23 - E extends undefined | readonly string[] = undefined, 24 - > { 25 - readonly type = "subscription" as const; 26 - 27 - constructor( 28 - readonly nsid: N, 29 - readonly parameters: P, 30 - readonly message: M, 31 - readonly errors: E, 32 - ) {} 33 - }
-45
lex/schema/schema/token.ts
··· 1 - import { 2 - type ValidationResult, 3 - Validator, 4 - type ValidatorContext, 5 - } from "../validation.ts"; 6 - 7 - export class TokenSchema<V extends string = string> extends Validator<V> { 8 - override readonly lexiconType = "token" as const; 9 - 10 - constructor(protected readonly value: V) { 11 - super(); 12 - } 13 - 14 - override validateInContext( 15 - input: unknown, 16 - ctx: ValidatorContext, 17 - ): ValidationResult<V> { 18 - if (input === this.value) { 19 - return ctx.success(this.value); 20 - } 21 - 22 - // @NOTE: allow using the token instance itself (but convert to the actual 23 - // token value) 24 - if (input instanceof TokenSchema && input.value === this.value) { 25 - return ctx.success(this.value); 26 - } 27 - 28 - if (typeof input !== "string") { 29 - return ctx.issueInvalidType(input, "token"); 30 - } 31 - 32 - return ctx.issueInvalidValue(input, [this.value]); 33 - } 34 - 35 - // When using the TokenSchema instance as data, let's serialize it to the 36 - // token value 37 - 38 - toJSON(): string { 39 - return this.value; 40 - } 41 - 42 - override toString(): string { 43 - return this.value; 44 - } 45 - }
-66
lex/schema/schema/typed-object.ts
··· 1 - import { isPlainObject } from "@atp/data"; 2 - import type { $Type, Simplify } from "../core.ts"; 3 - import { 4 - type Infer, 5 - type ValidationResult, 6 - Validator, 7 - type ValidatorContext, 8 - } from "../validation.ts"; 9 - 10 - export class TypedObjectSchema< 11 - Type extends $Type = $Type, 12 - Schema extends Validator<Record<string, unknown>> = Validator< 13 - Record<string, unknown> 14 - >, 15 - Output extends Infer<Schema> & { $type?: Type } = Infer<Schema> & { 16 - $type?: Type; 17 - }, 18 - > extends Validator<Output> { 19 - override readonly lexiconType = "object" as const; 20 - 21 - constructor( 22 - readonly $type: Type, 23 - readonly schema: Schema, 24 - ) { 25 - super(); 26 - } 27 - 28 - isTypeOf<X extends { $type?: unknown }>( 29 - value: X, 30 - ): value is X extends { $type?: Type } ? X : never { 31 - return value.$type === undefined || value.$type === this.$type; 32 - } 33 - 34 - build<X extends Omit<Output, "$type">>( 35 - input: X, 36 - ): Simplify<Omit<X, "$type"> & { $type: Type }> { 37 - return { ...input, $type: this.$type }; 38 - } 39 - 40 - $isTypeOf<X extends { $type?: unknown }>(value: X) { 41 - return this.isTypeOf<X>(value); 42 - } 43 - 44 - $build<X extends Omit<Output, "$type">>(input: X) { 45 - return this.build<X>(input); 46 - } 47 - 48 - override validateInContext( 49 - input: unknown, 50 - ctx: ValidatorContext, 51 - ): ValidationResult<Output> { 52 - if (!isPlainObject(input)) { 53 - return ctx.issueInvalidType(input, "object"); 54 - } 55 - 56 - if ( 57 - "$type" in input && 58 - input.$type !== undefined && 59 - input.$type !== this.$type 60 - ) { 61 - return ctx.issueInvalidPropertyValue(input, "$type", [this.$type]); 62 - } 63 - 64 - return ctx.validate(input, this.schema as Validator<Output>); 65 - } 66 - }
-74
lex/schema/schema/typed-ref.ts
··· 1 - import { 2 - type ValidationResult, 3 - Validator, 4 - type ValidatorContext, 5 - } from "../validation.ts"; 6 - 7 - // Basically a RecordSchema or TypedObjectSchema 8 - export type TypedRefSchemaValidator< 9 - V extends { $type?: string } = { $type?: string }, 10 - > = V extends { $type?: infer T extends string } 11 - ? { $type: T } & Validator<V & { $type?: T }> 12 - : never; 13 - 14 - export type TypedRefGetter<V extends { $type?: string } = { $type?: string }> = 15 - () => TypedRefSchemaValidator<V>; 16 - 17 - export type TypedRefSchemaOutput< 18 - V extends { $type?: string } = { $type?: string }, 19 - > = V extends { $type?: infer T extends string } ? V & { $type: T } : never; 20 - 21 - export class TypedRefSchema< 22 - V extends { $type?: string } = { $type?: string }, 23 - > extends Validator<TypedRefSchemaOutput<V>> { 24 - #getter: TypedRefGetter<V>; 25 - 26 - constructor(getter: TypedRefGetter<V>) { 27 - // @NOTE In order to avoid circular dependency issues, we don't resolve 28 - // the schema here. Instead, we resolve it lazily when first accessed. 29 - 30 - super(); 31 - 32 - this.#getter = getter; 33 - } 34 - 35 - get schema(): TypedRefSchemaValidator<V> { 36 - const value = this.#getter.call(null); 37 - 38 - // Prevents a getter from depending on itself recursively, also allows GC to 39 - // clean up the getter function. 40 - this.#getter = throwAlreadyCalled; 41 - 42 - // Cache the resolved schema on the instance 43 - Object.defineProperty(this, "schema", { 44 - value, 45 - writable: false, 46 - enumerable: false, 47 - configurable: true, 48 - }); 49 - 50 - return value; 51 - } 52 - 53 - get $type(): TypedRefSchemaOutput<V>["$type"] { 54 - return this.schema.$type; 55 - } 56 - 57 - override validateInContext( 58 - input: unknown, 59 - ctx: ValidatorContext, 60 - ): ValidationResult<TypedRefSchemaOutput<V>> { 61 - const result = ctx.validate(input, this.schema); 62 - if (!result.success) return result; 63 - 64 - if (result.value.$type !== this.$type) { 65 - return ctx.issueInvalidPropertyValue(result.value, "$type", [this.$type]); 66 - } 67 - 68 - return result as ValidationResult<TypedRefSchemaOutput<V>>; 69 - } 70 - } 71 - 72 - function throwAlreadyCalled(): never { 73 - throw new Error("TypedRefSchema getter called multiple times"); 74 - }
-107
lex/schema/schema/typed-union.ts
··· 1 - import { isPlainObject } from "@atp/data"; 2 - import type { Restricted, UnknownString } from "../core.ts"; 3 - import { 4 - type Infer, 5 - type ValidationResult, 6 - Validator, 7 - type ValidatorContext, 8 - } from "../validation.ts"; 9 - import type { TypedRefSchema, TypedRefSchemaOutput } from "./typed-ref.ts"; 10 - 11 - export type TypedRef<T extends { $type?: string }> = TypedRefSchemaOutput<T>; 12 - 13 - export type TypedObject = 14 - & { $type: UnknownString } 15 - & { 16 - // In order to prevent places that expect an open union from accepting an 17 - // invalid version of the known typed objects, we need to prevent any other 18 - // properties from being present. 19 - // 20 - // For example, if an open union expects: 21 - // ```ts 22 - // TypedObject | { $type: 'A'; a: number } 23 - // ``` 24 - // we don't want it to accept: 25 - // ```ts 26 - // { $type: 'A' } 27 - // ``` 28 - // Which would be the case as `{ $type: 'A' }` is a valid 29 - // `TypedObject`. By adding an index signature that forbids any 30 - // property, we ensure that only valid known typed objects can be used. 31 - [K in string]: Restricted<"Unknown property">; 32 - }; 33 - 34 - type TypedRefSchemasToUnion<T extends readonly TypedRefSchema[]> = { 35 - [K in keyof T]: Infer<T[K]>; 36 - }[number]; 37 - 38 - export type TypedUnionSchemaOutput< 39 - TypedRefs extends readonly TypedRefSchema[], 40 - Closed extends boolean, 41 - > = Closed extends true ? TypedRefSchemasToUnion<TypedRefs> 42 - : TypedRefSchemasToUnion<TypedRefs> | TypedObject; 43 - 44 - export class TypedUnionSchema< 45 - TypedRefs extends readonly TypedRefSchema[] = TypedRefSchema[], 46 - Closed extends boolean = boolean, 47 - > extends Validator<TypedUnionSchemaOutput<TypedRefs, Closed>> { 48 - override readonly lexiconType = "union" as const; 49 - 50 - constructor( 51 - protected readonly refs: TypedRefs, 52 - public readonly closed: Closed, 53 - ) { 54 - // @NOTE In order to avoid circular dependency issues, we don't access the 55 - // refs's schema (or $type) here. Instead, we access them lazily when first 56 - // needed. 57 - 58 - super(); 59 - } 60 - 61 - protected get refsMap() { 62 - const map = new Map<unknown, TypedRefs[number]>(); 63 - for (const ref of this.refs) map.set(ref.$type, ref); 64 - 65 - // Cache the map on the instance 66 - Object.defineProperty(this, "refsMap", { 67 - value: map, 68 - writable: false, 69 - enumerable: false, 70 - configurable: true, 71 - }); 72 - 73 - return map; 74 - } 75 - 76 - get $types() { 77 - return Array.from(this.refsMap.keys()); 78 - } 79 - 80 - override validateInContext( 81 - input: unknown, 82 - ctx: ValidatorContext, 83 - ): ValidationResult<TypedUnionSchemaOutput<TypedRefs, Closed>> { 84 - if (!isPlainObject(input) || !("$type" in input)) { 85 - return ctx.issueInvalidType(input, "$typed"); 86 - } 87 - 88 - const { $type } = input; 89 - 90 - const def = this.refsMap.get($type); 91 - if (def) { 92 - const result = ctx.validate(input, def); 93 - return result as ValidationResult< 94 - TypedUnionSchemaOutput<TypedRefs, Closed> 95 - >; 96 - } 97 - 98 - if (this.closed) { 99 - return ctx.issueInvalidPropertyValue(input, "$type", this.$types); 100 - } 101 - if (typeof $type !== "string") { 102 - return ctx.issueInvalidPropertyType(input, "$type", "string"); 103 - } 104 - 105 - return ctx.success(input as TypedUnionSchemaOutput<TypedRefs, Closed>); 106 - } 107 - }
-42
lex/schema/schema/union.ts
··· 1 - import { 2 - type Infer, 3 - ValidationError, 4 - type ValidationFailure, 5 - type ValidationResult, 6 - Validator, 7 - type ValidatorContext, 8 - } from "../validation.ts"; 9 - 10 - export type UnionSchemaValidators = readonly [Validator, ...Validator[]]; 11 - export type UnionSchemaOutput<V extends readonly Validator[]> = Infer< 12 - V[number] 13 - >; 14 - 15 - export class UnionSchema< 16 - V extends UnionSchemaValidators = UnionSchemaValidators, 17 - > extends Validator<UnionSchemaOutput<V>> { 18 - constructor(protected readonly validators: V) { 19 - super(); 20 - } 21 - 22 - override validateInContext( 23 - input: unknown, 24 - ctx: ValidatorContext, 25 - ): ValidationResult<UnionSchemaOutput<V>> { 26 - const failures: ValidationFailure[] = []; 27 - 28 - for (const validator of this.validators) { 29 - const result = ctx.validate(input, validator); 30 - if (result.success) { 31 - return result as ValidationResult<UnionSchemaOutput<V>>; 32 - } else { 33 - failures.push(result); 34 - } 35 - } 36 - 37 - return { 38 - success: false, 39 - error: ValidationError.fromFailures(failures), 40 - }; 41 - } 42 - }
-24
lex/schema/schema/unknown-object.ts
··· 1 - import { isLexMap, type LexMap } from "@atp/data"; 2 - import { 3 - type ValidationResult, 4 - Validator, 5 - type ValidatorContext, 6 - } from "../validation.ts"; 7 - 8 - export type { LexMap }; 9 - export type UnknownObjectOutput = LexMap; 10 - 11 - export class UnknownObjectSchema extends Validator<UnknownObjectOutput> { 12 - override readonly lexiconType = "unknown" as const; 13 - 14 - override validateInContext( 15 - input: unknown, 16 - ctx: ValidatorContext, 17 - ): ValidationResult<UnknownObjectOutput> { 18 - if (isLexMap(input)) { 19 - return ctx.success(input); 20 - } 21 - 22 - return ctx.issueInvalidType(input, "unknown"); 23 - } 24 - }
-14
lex/schema/schema/unknown.ts
··· 1 - import { 2 - type ValidationResult, 3 - Validator, 4 - type ValidatorContext, 5 - } from "../validation.ts"; 6 - 7 - export class UnknownSchema extends Validator<unknown> { 8 - override validateInContext( 9 - input: unknown, 10 - ctx: ValidatorContext, 11 - ): ValidationResult<unknown> { 12 - return ctx.success(input); 13 - } 14 - }
-40
lex/schema/tests/array-agg_test.ts
··· 1 - import { assertEquals } from "@std/assert"; 2 - import { arrayAgg } from "../util/array-agg.ts"; 3 - 4 - Deno.test("arrayAgg aggregates items based on comparison and aggregation functions", () => { 5 - const input = [1, 1, 2, 2, 3, 3, 3]; 6 - const result = arrayAgg( 7 - input, 8 - (a, b) => a === b, 9 - (items) => ({ value: items[0], count: items.length }), 10 - ); 11 - assertEquals(result, [ 12 - { value: 1, count: 2 }, 13 - { value: 2, count: 2 }, 14 - { value: 3, count: 3 }, 15 - ]); 16 - }); 17 - 18 - Deno.test("arrayAgg returns an empty array when input is empty", () => { 19 - const input: number[] = []; 20 - const result = arrayAgg( 21 - input, 22 - (a, b) => a === b, 23 - (items) => ({ value: items[0], count: items.length }), 24 - ); 25 - assertEquals(result, []); 26 - }); 27 - 28 - Deno.test("arrayAgg handles non-consecutive grouping", () => { 29 - const input = [1, 2, 1, 2, 3, 1]; 30 - const result = arrayAgg( 31 - input, 32 - (a, b) => a === b, 33 - (items) => ({ value: items[0], count: items.length }), 34 - ); 35 - assertEquals(result, [ 36 - { value: 1, count: 3 }, 37 - { value: 2, count: 2 }, 38 - { value: 3, count: 1 }, 39 - ]); 40 - });
-131
lex/schema/tests/object_test.ts
··· 1 - import { assertEquals } from "@std/assert"; 2 - import { BooleanSchema } from "../schema/boolean.ts"; 3 - import { DictSchema } from "../schema/dict.ts"; 4 - import { EnumSchema } from "../schema/enum.ts"; 5 - import { IntegerSchema } from "../schema/integer.ts"; 6 - import { ObjectSchema } from "../schema/object.ts"; 7 - import { StringSchema } from "../schema/string.ts"; 8 - 9 - const simpleSchema = new ObjectSchema( 10 - { 11 - name: new StringSchema({}), 12 - age: new IntegerSchema({}), 13 - gender: new EnumSchema(["male", "female"]), 14 - }, 15 - { 16 - required: ["name"], 17 - nullable: ["gender"], 18 - }, 19 - ); 20 - 21 - Deno.test("ObjectSchema simple schema validates plain objects", () => { 22 - const result = simpleSchema.validate({ 23 - name: "Alice", 24 - age: 30, 25 - gender: "female", 26 - }); 27 - assertEquals(result.success, true); 28 - }); 29 - 30 - Deno.test("ObjectSchema simple schema rejects non-objects", () => { 31 - const result = simpleSchema.validate("not an object"); 32 - assertEquals(result.success, false); 33 - }); 34 - 35 - Deno.test("ObjectSchema simple schema rejects missing properties", () => { 36 - const result = simpleSchema.validate({ 37 - age: 30, 38 - gender: "female", 39 - }); 40 - assertEquals(result.success, false); 41 - }); 42 - 43 - Deno.test("ObjectSchema simple schema validates optional properties", () => { 44 - const result = simpleSchema.validate({ 45 - name: "Alice", 46 - }); 47 - assertEquals(result.success, true); 48 - }); 49 - 50 - Deno.test("ObjectSchema simple schema validates nullable properties", () => { 51 - const result = simpleSchema.validate({ 52 - name: "Alice", 53 - gender: null, 54 - }); 55 - assertEquals(result.success, true); 56 - }); 57 - 58 - Deno.test("ObjectSchema simple schema rejects invalid property types", () => { 59 - const result = simpleSchema.validate({ 60 - name: "Alice", 61 - age: "thirty", 62 - }); 63 - assertEquals(result.success, false); 64 - }); 65 - 66 - Deno.test("ObjectSchema simple schema ignores extra properties", () => { 67 - const result = simpleSchema.validate({ 68 - name: "Alice", 69 - age: 30, 70 - extra: "value", 71 - }); 72 - assertEquals(result.success, true); 73 - }); 74 - 75 - const strictSchema = new ObjectSchema( 76 - { 77 - id: new StringSchema({}), 78 - score: new IntegerSchema({}), 79 - }, 80 - { 81 - required: ["id", "score"], 82 - unknownProperties: "strict", 83 - }, 84 - ); 85 - 86 - Deno.test("ObjectSchema strict schema rejects extra properties", () => { 87 - const result = strictSchema.validate({ 88 - id: "item1", 89 - score: 100, 90 - extra: "not allowed", 91 - }); 92 - assertEquals(result.success, false); 93 - }); 94 - 95 - Deno.test("ObjectSchema strict schema accepts only defined properties", () => { 96 - const result = strictSchema.validate({ 97 - id: "item1", 98 - score: 100, 99 - }); 100 - assertEquals(result.success, true); 101 - }); 102 - 103 - const unknownPropertiesSchema = new ObjectSchema( 104 - { 105 - title: new StringSchema({}), 106 - }, 107 - { 108 - required: ["title"], 109 - unknownProperties: new DictSchema( 110 - new EnumSchema(["tag1", "tag2"]), 111 - new BooleanSchema({}), 112 - ), 113 - }, 114 - ); 115 - 116 - Deno.test("schema with unknownProperties validator validates extra properties with the provided validator", () => { 117 - const result = unknownPropertiesSchema.validate({ 118 - title: "My Post", 119 - tag1: true, 120 - tag2: false, 121 - }); 122 - assertEquals(result.success, true); 123 - }); 124 - 125 - Deno.test("schema with unknownProperties rejects extra properties that fail the provided validator", () => { 126 - const result = unknownPropertiesSchema.validate({ 127 - title: "My Post", 128 - tag1: "not a boolean", 129 - }); 130 - assertEquals(result.success, false); 131 - });
-43
lex/schema/util/array-agg.ts
··· 1 - /** 2 - * Aggregates items in an array based on a comparison function and an aggregation function. 3 - * 4 - * @param arr - The input array to aggregate. 5 - * @param cmp - A comparison function that determines if two items belong to the same group. 6 - * @param agg - An aggregation function that combines items in a group into a single item. 7 - * @returns An array of aggregated items. 8 - * @example 9 - * ```ts 10 - * const input = [1, 1, 2, 2, 3, 3, 3] 11 - * const result = arrayAgg( 12 - * input, 13 - * (a, b) => a === b, 14 - * (items) => { value: items[0], sum: items.reduce((sum, item) => sum + item, 0) }, 15 - * ) 16 - * // result is [{ value: 1, sum: 2 }, { value: 2, sum: 4 }, { value: 3, sum: 6 }] 17 - * ``` 18 - */ 19 - export function arrayAgg<T, O>( 20 - arr: readonly T[], 21 - cmp: (a: T, b: T) => boolean, 22 - agg: (items: [T, ...T[]]) => O, 23 - ): O[] { 24 - if (arr.length === 0) return []; 25 - 26 - const groups: [T, ...T[]][] = [[arr[0]]]; 27 - const skipped = Array<undefined | boolean>(arr.length); 28 - 29 - outer: for (let i = 1; i < arr.length; i++) { 30 - if (skipped[i]) continue; 31 - const item = arr[i]; 32 - for (let j = 0; j < groups.length; j++) { 33 - if (cmp(item, groups[j][0])) { 34 - groups[j].push(item); 35 - skipped[i] = true; 36 - continue outer; 37 - } 38 - } 39 - groups.push([item]); 40 - } 41 - 42 - return groups.map(agg); 43 - }
-4
lex/schema/validation.ts
··· 1 - export * from "./validation/property-key.ts"; 2 - export * from "./validation/validation-error.ts"; 3 - export * from "./validation/validation-issue.ts"; 4 - export * from "./validation/validator.ts";
-1
lex/schema/validation/property-key.ts
··· 1 - export type PropertyKey = string | number;
-32
lex/schema/validation/validation-error.ts
··· 1 - import { failureError, type ResultFailure } from "../core.ts"; 2 - import { 3 - aggregateIssues, 4 - stringifyIssue, 5 - type ValidationIssue, 6 - } from "./validation-issue.ts"; 7 - 8 - export class ValidationError extends Error { 9 - override name = "ValidationError"; 10 - 11 - constructor( 12 - readonly issues: ValidationIssue[], 13 - options?: ErrorOptions, 14 - ) { 15 - super(issues.map(stringifyIssue).join(", "), options); 16 - } 17 - 18 - static fromFailures( 19 - failures: ResultFailure<ValidationError>[], 20 - ): ValidationError { 21 - if (failures.length === 1) return failures[0].error; 22 - const issues = failures.flatMap(extractFailureIssues); 23 - return new ValidationError(aggregateIssues(issues), { 24 - // Keep the original errors as the cause chain 25 - cause: failures.map(failureError), 26 - }); 27 - } 28 - } 29 - 30 - function extractFailureIssues(result: ResultFailure<ValidationError>) { 31 - return result.error.issues; 32 - }
-239
lex/schema/validation/validation-issue.ts
··· 1 - import { CID, isPlainObject } from "@atp/data"; 2 - import { arrayAgg } from "../util/array-agg.ts"; 3 - import type { PropertyKey } from "./property-key.ts"; 4 - 5 - export interface Issue<I = unknown> { 6 - readonly input: I; 7 - readonly code: string; 8 - readonly message?: string; 9 - readonly path: readonly PropertyKey[]; 10 - } 11 - 12 - export interface IssueCustom extends Issue { 13 - readonly code: "custom"; 14 - readonly message: string; 15 - } 16 - 17 - export interface IssueInvalidFormat extends Issue { 18 - readonly code: "invalid_format"; 19 - readonly format: string; 20 - } 21 - 22 - export interface IssueInvalidType extends Issue { 23 - readonly code: "invalid_type"; 24 - readonly expected: readonly string[]; 25 - } 26 - 27 - export interface IssueInvalidValue extends Issue { 28 - readonly code: "invalid_value"; 29 - readonly values: readonly unknown[]; 30 - } 31 - 32 - export interface IssueRequiredKey extends Issue { 33 - readonly code: "required_key"; 34 - readonly key: PropertyKey; 35 - } 36 - 37 - export interface IssueTooBig extends Issue { 38 - readonly code: "too_big"; 39 - readonly maximum: number; 40 - readonly type: "array" | "string" | "integer" | "grapheme" | "bytes" | "blob"; 41 - readonly actual: number; 42 - } 43 - 44 - export interface IssueTooSmall extends Issue { 45 - readonly code: "too_small"; 46 - readonly minimum: number; 47 - readonly type: "array" | "string" | "integer" | "grapheme" | "bytes"; 48 - readonly actual: number; 49 - } 50 - 51 - export type ValidationIssue = 52 - | IssueCustom 53 - | IssueInvalidFormat 54 - | IssueInvalidType 55 - | IssueInvalidValue 56 - | IssueRequiredKey 57 - | IssueTooBig 58 - | IssueTooSmall; 59 - 60 - export function stringifyIssue(issue: ValidationIssue): string { 61 - const pathStr = issue.path.length ? ` at ${buildJsonPath(issue.path)}` : ""; 62 - 63 - switch (issue.code) { 64 - case "invalid_format": 65 - return `Invalid ${stringifyStringFormat(issue.format)} format${ 66 - issue.message ? ` (${issue.message})` : "" 67 - }${pathStr} (got ${stringifyValue(issue.input)})`; 68 - case "invalid_type": 69 - return `Expected ${ 70 - oneOf(issue.expected.map(stringifyExpectedType)) 71 - } value type${pathStr} (got ${stringifyType(issue.input)})`; 72 - case "invalid_value": 73 - return `Expected ${ 74 - oneOf(issue.values.map(stringifyValue)) 75 - }${pathStr} (got ${stringifyValue(issue.input)})`; 76 - case "required_key": 77 - return `Missing required key "${String(issue.key)}"${pathStr}`; 78 - case "too_big": 79 - return `${issue.type} too big (maximum ${issue.maximum})${pathStr} (got ${issue.actual})`; 80 - case "too_small": 81 - return `${issue.type} too small (minimum ${issue.minimum})${pathStr} (got ${issue.actual})`; 82 - case "custom": 83 - return `${issue.message}${pathStr}`; 84 - default: 85 - // @ts-expect-error fool-proofing 86 - return `${issue.code} validation error${pathStr}`; 87 - } 88 - } 89 - 90 - function stringifyExpectedType(expected: string): string { 91 - if (expected === "$typed") { 92 - return 'an object or record which includes a "$type" property'; 93 - } 94 - 95 - return expected; 96 - } 97 - 98 - function buildJsonPath(path: readonly PropertyKey[]): string { 99 - let jsonPath = "$"; 100 - for (const segment of path) { 101 - if (typeof segment === "number") { 102 - jsonPath += `[${segment}]`; 103 - } else if (/^[a-zA-Z_$][a-zA-Z0-9_]*$/.test(segment as string)) { 104 - jsonPath += `.${segment}`; 105 - } else { 106 - jsonPath += `[${JSON.stringify(segment)}]`; 107 - } 108 - } 109 - return jsonPath; 110 - } 111 - 112 - function oneOf(arr: readonly string[]): string { 113 - if (arr.length === 0) return ""; 114 - if (arr.length === 1) return arr[0]; 115 - return `one of ${arr.slice(0, -1).join(", ")} or ${arr.at(-1)}`; 116 - } 117 - 118 - function stringifyStringFormat(format: string): string { 119 - switch (format) { 120 - case "datetime": 121 - return "datetime"; 122 - case "language": 123 - return "language"; 124 - case "at-identifier": 125 - return `AT identifier`; 126 - case "did": 127 - return `DID`; 128 - case "handle": 129 - return `handle`; 130 - case "nsid": 131 - return `NSID`; 132 - case "cid": 133 - return `CID string`; 134 - case "tid": 135 - return `TID string`; 136 - case "record-key": 137 - return `record key`; 138 - default: 139 - return format; 140 - } 141 - } 142 - 143 - export function stringifyType(value: unknown): string { 144 - switch (typeof value) { 145 - case "object": 146 - if (value === null) return "null"; 147 - if (Array.isArray(value)) return "array"; 148 - if (CID.asCID(value)) return "cid"; 149 - if (value instanceof Date) return "date"; 150 - if (value instanceof RegExp) return "regexp"; 151 - if (value instanceof Map) return "map"; 152 - if (value instanceof Set) return "set"; 153 - return "object"; 154 - case "number": 155 - if (Number.isInteger(value)) return "integer"; 156 - if (Number.isNaN(value)) return "NaN"; 157 - return "float"; 158 - default: 159 - return typeof value; 160 - } 161 - } 162 - 163 - export function stringifyValue(value: unknown): string { 164 - switch (typeof value) { 165 - case "bigint": 166 - return `${value}n`; 167 - case "number": 168 - case "string": 169 - case "boolean": 170 - return JSON.stringify(value); 171 - case "object": 172 - if (Array.isArray(value)) { 173 - return `[${stringifyArray(value, stringifyValue)}]`; 174 - } 175 - if (isPlainObject(value)) { 176 - return `{${ 177 - stringifyArray(Object.entries(value), stringifyObjectEntry) 178 - }}`; 179 - } 180 - // fallthrough 181 - default: 182 - return stringifyType(value); 183 - } 184 - } 185 - 186 - function stringifyObjectEntry([key, _value]: [PropertyKey, unknown]): string { 187 - return `${JSON.stringify(key)}: ...`; 188 - } 189 - 190 - function stringifyArray<T>( 191 - arr: readonly T[], 192 - fn: (item: T) => string, 193 - n = 2, 194 - ): string { 195 - return arr.slice(0, n).map(fn).join(", ") + (arr.length > n ? ", ..." : ""); 196 - } 197 - 198 - export function aggregateIssues(issues: ValidationIssue[]): ValidationIssue[] { 199 - // Quick path for common cases 200 - if (issues.length <= 1) return issues; 201 - if (issues.length === 2 && issues[0].code !== issues[1].code) return issues; 202 - 203 - return [ 204 - // Aggregate invalid_type with identical paths 205 - ...arrayAgg( 206 - issues.filter((issue) => issue.code === "invalid_type"), 207 - (a, b) => comparePropertyPaths(a.path, b.path), 208 - (issues) => ({ 209 - ...issues[0], 210 - expected: Array.from(new Set(issues.flatMap((iss) => iss.expected))), 211 - }), 212 - ), 213 - // Aggregate invalid_value with identical paths 214 - ...arrayAgg( 215 - issues.filter((issue) => issue.code === "invalid_value"), 216 - (a, b) => comparePropertyPaths(a.path, b.path), 217 - (issues) => ({ 218 - ...issues[0], 219 - values: Array.from(new Set(issues.flatMap((iss) => iss.values))), 220 - }), 221 - ), 222 - // Pass through other issues 223 - ...issues.filter( 224 - (issue) => 225 - issue.code !== "invalid_type" && issue.code !== "invalid_value", 226 - ), 227 - ]; 228 - } 229 - 230 - function comparePropertyPaths( 231 - a: readonly PropertyKey[], 232 - b: readonly PropertyKey[], 233 - ) { 234 - if (a.length !== b.length) return false; 235 - for (let i = 0; i < a.length; i++) { 236 - if (a[i] !== b[i]) return false; 237 - } 238 - return true; 239 - }
-368
lex/schema/validation/validator.ts
··· 1 - import { 2 - failure, 3 - type ResultFailure, 4 - type ResultSuccess, 5 - success, 6 - } from "../core.ts"; 7 - import type { PropertyKey } from "./property-key.ts"; 8 - import { ValidationError } from "./validation-error.ts"; 9 - import type { 10 - IssueTooBig, 11 - IssueTooSmall, 12 - ValidationIssue, 13 - } from "./validation-issue.ts"; 14 - 15 - export type ValidationSuccess<Value = unknown> = ResultSuccess<Value>; 16 - export type ValidationFailure = ResultFailure<ValidationError>; 17 - export type ValidationResult<Value = unknown> = 18 - | ValidationSuccess<Value> 19 - | ValidationFailure; 20 - 21 - type ValidationOptions = { 22 - path?: PropertyKey[]; 23 - 24 - /** @default true */ 25 - allowTransform?: boolean; 26 - }; 27 - 28 - export type Infer<T extends Validator> = T["_lex"]["output"]; 29 - 30 - export abstract class Validator<Output = unknown> { 31 - /** 32 - * This property is used for type inference purposes and does not actually 33 - * exist at runtime. 34 - * 35 - * @deprecated For internal use only (not actually deprecated) 36 - */ 37 - _lex!: { output: Output }; 38 - 39 - readonly lexiconType?: string; 40 - 41 - /** 42 - * @internal **INTERNAL API, DO NOT USE**. 43 - * 44 - * Use {@link Validator.assert assert}, {@link Validator.check check}, 45 - * {@link Validator.parse parse} or {@link Validator.validate validate} 46 - * instead. 47 - * 48 - * This method is implemented by subclasses to perform transformation and 49 - * validation of the input value. Do not call this method directly; as the 50 - * {@link ValidatorContext.options.allowTransform} option will **not** be 51 - * enforced. See {@link ValidatorContext.validate} for details. When 52 - * delegating validation from one validator sub-class implementation to 53 - * another schema, {@link ValidatorContext.validate} should be used instead 54 - * of calling {@link Validator.validateInContext}. This will allow to stop the 55 - * validation process if the value was transformed (by the other schema) but 56 - * transformations are not allowed. 57 - * 58 - * By convention, the {@link ValidationResult} must return the original input 59 - * value if validation was successful and no transformation was applied (i.e. 60 - * the input already conformed to the schema). If a default value, or any 61 - * other transformation was applied, the returned value c&an be different from 62 - * the input. 63 - * 64 - * This convention allows the {@link Validator.check check} and 65 - * {@link Validator.assert assert} methods to check whether the input value 66 - * exactly matches the schema (without defaults or transformations), by 67 - * checking if the returned value is strictly equal to the input. 68 - * 69 - * @see {@link ValidatorContext.validate} 70 - */ 71 - abstract validateInContext( 72 - input: unknown, 73 - ctx: ValidatorContext, 74 - ): ValidationResult<Output>; 75 - 76 - assert(input: unknown): asserts input is Output { 77 - const result = this.validate(input, { allowTransform: false }); 78 - if (!result.success) throw result.error; 79 - } 80 - 81 - check(input: unknown): input is Output { 82 - const result = this.validate(input, { allowTransform: false }); 83 - return result.success; 84 - } 85 - 86 - maybe<I>(input: I): (I & Output) | undefined { 87 - return this.check(input) ? input : undefined; 88 - } 89 - 90 - parse<I>( 91 - input: I, 92 - options: ValidationOptions & { allowTransform: false }, 93 - ): I & Output; 94 - parse(input: unknown, options?: ValidationOptions): Output; 95 - parse(input: unknown, options?: ValidationOptions): Output { 96 - const result = ValidatorContext.validate(input, this, options); 97 - if (!result.success) throw result.error; 98 - return result.value; 99 - } 100 - 101 - validate<I>( 102 - input: I, 103 - options: ValidationOptions & { allowTransform: false }, 104 - ): ValidationResult<I & Output>; 105 - validate( 106 - input: unknown, 107 - options?: ValidationOptions, 108 - ): ValidationResult<Output>; 109 - validate( 110 - input: unknown, 111 - options?: ValidationOptions, 112 - ): ValidationResult<Output> { 113 - return ValidatorContext.validate(input, this, options); 114 - } 115 - 116 - // @NOTE The built lexicons namespaces will export utility functions that 117 - // allow accessing the schema's methods without the need to specify ".main." 118 - // as part of the namespace. This way, a utility for a particular record type 119 - // can be called like "app.bsky.feed.post.<utility>()" instead of 120 - // "app.bsky.feed.post.main.<utility>()". Because those utilities could 121 - // conflict with other schemas (e.g. if there is a lexicon definition at 122 - // "#<utility>"), those exported utilities will be prefixed with "$". In order 123 - // to be able to consistently call the utilities, when using the "main" and 124 - // non "main" definitions, we also expose the same methods with a "$" prefix. 125 - // Thanks to this, both of the following call will be possible: 126 - // 127 - // - "app.bsky.feed.post.$parse(...)" // calls a utility function created by "lex build" 128 - // - "app.bsky.feed.defs.postView.$parse(...)" // uses the alias defined below on the schema instance 129 - 130 - $assert(input: unknown): asserts input is Output { 131 - return this.assert(input); 132 - } 133 - 134 - $check(input: unknown): input is Output { 135 - return this.check(input); 136 - } 137 - 138 - $maybe<I>(input: I): (I & Output) | undefined { 139 - return this.maybe(input); 140 - } 141 - 142 - $parse(input: unknown, options?: ValidationOptions): Output { 143 - return this.parse(input, options); 144 - } 145 - 146 - $validate( 147 - input: unknown, 148 - options?: ValidationOptions, 149 - ): ValidationResult<Output> { 150 - return this.validate(input, options); 151 - } 152 - } 153 - 154 - export type ContextualIssue = { 155 - [Code in ValidationIssue["code"]]: 156 - & Omit< 157 - Extract<ValidationIssue, { code: Code }>, 158 - "path" 159 - > 160 - & { path?: PropertyKey | readonly PropertyKey[] }; 161 - }[ValidationIssue["code"]]; 162 - 163 - const asIssue = ( 164 - { path, ...issue }: ContextualIssue, 165 - currentPath: readonly PropertyKey[], 166 - ): ValidationIssue & { path: PropertyKey[] } => ({ 167 - ...issue, 168 - path: path != null ? currentPath.concat(path) : [...currentPath], 169 - }); 170 - 171 - export class ValidatorContext { 172 - /** 173 - * Creates a new validation context and validates the input using the 174 - * provided validator. 175 - */ 176 - static validate<V>( 177 - input: unknown, 178 - validator: Validator<V>, 179 - options: ValidationOptions = {}, 180 - ): ValidationResult<V> { 181 - const context = new ValidatorContext(options); 182 - return context.validate(input, validator); 183 - } 184 - 185 - private readonly currentPath: PropertyKey[]; 186 - private readonly issues: ValidationIssue[] = []; 187 - 188 - protected constructor(readonly options: ValidationOptions) { 189 - // Create a copy because we will be mutating the array during validation. 190 - this.currentPath = options?.path != null ? [...options.path] : []; 191 - } 192 - 193 - get path() { 194 - return [...this.currentPath]; 195 - } 196 - 197 - get allowTransform() { 198 - // Default to true 199 - return this.options?.allowTransform !== false; 200 - } 201 - 202 - /** 203 - * This is basically the entry point for validation within a context. Use this 204 - * method instead of {@link Validator.validateInContext} directly, because 205 - * this method enforces the {@link ValidationOptions.allowTransform} option. 206 - */ 207 - validate<V>(input: unknown, validator: Validator<V>): ValidationResult<V> { 208 - const result = validator.validateInContext(input, this); 209 - 210 - if (result.success) { 211 - if (!this.allowTransform && !Object.is(result.value, input)) { 212 - // If the value changed, it means that a default (or some other 213 - // transformation) was applied, meaning that the original value did 214 - // *not* match the (output) schema. When "allowTransform" is false, we 215 - // consider this a failure. 216 - 217 - // This check is the reason why Validator.validateInContext should not 218 - // be used directly, and ValidatorContext.validate should be used 219 - // instead, even when delegating validation from one validator to 220 - // another. 221 - 222 - // This if block comes before the next one because 'this.issues' will 223 - // end-up being appended to the returned ValidationError (see the 224 - // "failure" method below), resulting in a more complete error report. 225 - return this.issueInvalidValue(input, [result.value]); 226 - } 227 - 228 - if (this.issues.length > 0) { 229 - // Validator returned a success but issues were added via the context. 230 - // This means the overall validation failed. 231 - return { success: false, error: new ValidationError(this.issues) }; 232 - } 233 - } 234 - 235 - return result; 236 - } 237 - 238 - validateChild<I extends object, K extends PropertyKey & keyof I, V>( 239 - input: I, 240 - key: K, 241 - validator: Validator<V>, 242 - ): ValidationResult<V> { 243 - // Instead of creating a new context, we just push/pop the path segment. 244 - this.currentPath.push(key); 245 - try { 246 - return this.validate(input[key], validator); 247 - } finally { 248 - this.currentPath.length--; 249 - } 250 - } 251 - 252 - addIssue(issue: ContextualIssue): void { 253 - this.issues.push(asIssue(issue, this.currentPath)); 254 - } 255 - 256 - success<V>(value: V): ValidationResult<V> { 257 - return success(value); 258 - } 259 - 260 - failure(issue: ContextualIssue): ValidationFailure { 261 - return failure( 262 - new ValidationError([...this.issues, asIssue(issue, this.currentPath)]), 263 - ); 264 - } 265 - 266 - issueInvalidValue( 267 - input: unknown, 268 - values: readonly unknown[], 269 - path?: PropertyKey | readonly PropertyKey[], 270 - ) { 271 - return this.failure({ 272 - code: "invalid_value", 273 - input, 274 - values, 275 - path, 276 - }); 277 - } 278 - 279 - issueInvalidType( 280 - input: unknown, 281 - expected: string | readonly string[], 282 - path?: PropertyKey | readonly PropertyKey[], 283 - ) { 284 - return this.failure({ 285 - code: "invalid_type", 286 - input, 287 - expected: Array.isArray(expected) ? expected : [expected], 288 - path, 289 - }); 290 - } 291 - 292 - issueInvalidPropertyValue<I>( 293 - input: I, 294 - property: keyof I & PropertyKey, 295 - values: readonly unknown[], 296 - ) { 297 - return this.issueInvalidValue(input[property], values, property); 298 - } 299 - 300 - issueInvalidPropertyType<I>( 301 - input: I, 302 - property: keyof I & PropertyKey, 303 - expected: string | readonly string[], 304 - ) { 305 - return this.issueInvalidType(input[property], expected, property); 306 - } 307 - 308 - issueRequiredKey(input: object, key: PropertyKey) { 309 - return this.failure({ 310 - code: "required_key", 311 - key, 312 - input, 313 - path: key, 314 - }); 315 - } 316 - 317 - issueInvalidFormat(input: unknown, format: string, message?: string) { 318 - return this.failure({ 319 - code: "invalid_format", 320 - message, 321 - format, 322 - input, 323 - }); 324 - } 325 - 326 - issueTooBig( 327 - input: unknown, 328 - type: IssueTooBig["type"], 329 - maximum: number, 330 - actual: number, 331 - ) { 332 - return this.failure({ 333 - code: "too_big", 334 - type, 335 - maximum, 336 - actual, 337 - input, 338 - }); 339 - } 340 - 341 - issueTooSmall( 342 - input: unknown, 343 - type: IssueTooSmall["type"], 344 - minimum: number, 345 - actual: number, 346 - ) { 347 - return this.failure({ 348 - code: "too_small", 349 - type, 350 - minimum, 351 - actual, 352 - input, 353 - }); 354 - } 355 - 356 - custom( 357 - input: unknown, 358 - message: string, 359 - path?: PropertyKey | readonly PropertyKey[], 360 - ) { 361 - return this.failure({ 362 - code: "custom", 363 - input, 364 - message, 365 - path, 366 - }); 367 - } 368 - }