diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 60ddea5007..a6e3fd1819 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -48,15 +48,12 @@ git checkout -b amazing-feature 6. Do your best to write code that is stylistically consistent with its context. The linter will help with this, but it won't catch everything. Here's a few general guidelines: - - Favor functions over classes - - Favor arrow functions outside of classes - - Favor types over interfaces - Favor mutation over copying objects in perf-sensitive contexts - Favor clarity in naming with the following exceptions: - Ubiquitous variables/types. For example, use `s` over `dynamicParserState` for a variable of type DynamicParserState that is used in the same way across many functions. - Ephemeral variables whose contents can be trivially inferred from context. For example, prefer `rawKeyDefinitions.map(_ => _.trim())` to `rawKeyDefinitions.map(rawKeyDefinition => rawKeyDefinition.trim())`. -We also have some unique casing rules for our TypeScript types to making writing isomorphic code easier: +We also have some unique casing rules for our TypeScript types to facilitate type-level code that can parallel its runtime implementation and be easily understood: - Use `CapitalCase` for... diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index beccdce051..8bac1e014a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -18,23 +18,18 @@ assignees: "ssalbdivad" ### 🧩 Context - ArkType version: -- TypeScript version (4.8, 4.9, or 5.0): -- Other context you think may be relevant (Node version, OS, etc.): +- TypeScript version (5.1+): +- Other context you think may be relevant (JS flavor, OS, etc.): ### πŸ§‘β€πŸ’» Repro -https://stackblitz.com/edit/arktype-bug?devToolsHeight=33&file=demo.ts +Please do your best to write the simplest code you can that reproduces the issue! -```ts -import { type, scope } from "arktype" +If it requires other dependencies besides arktype, it's probably either not a minimal repro or not an arktype bug. +--> +```ts // Paste reproduction code here ``` diff --git a/.github/SECURITY.md b/.github/SECURITY.md index ec2ae1066d..b4e69f4166 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -4,7 +4,7 @@ | Version | Supported | | ------- | ------------------ | -| 1.x | :white_check_mark: | +| 2.x | :white_check_mark: | ## Reporting a Vulnerability diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index cdf847ba7f..3c251acfb9 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -25,3 +25,7 @@ runs: - name: Build shell: bash run: pnpm build + + - name: Post-build install + shell: bash + run: pnpm install diff --git a/.github/semantic.yml b/.github/semantic.yml deleted file mode 100644 index 90cf41d4b0..0000000000 --- a/.github/semantic.yml +++ /dev/null @@ -1,9 +0,0 @@ -titleOnly: true -scopes: - - attest - - dark - - fs - - repo - - schema - - type - - utils diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ad15a2ef48..37e0a97cd9 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -34,9 +34,8 @@ jobs: include: - os: ubuntu-latest node: lts/-1 - # https://github.com/arktypeio/arktype/issues/738 - # - os: ubuntu-latest - # node: latest + - os: ubuntu-latest + node: latest fail-fast: false runs-on: ${{ matrix.os }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 451da58fb5..9e552f3141 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,8 +18,8 @@ // too many overlapping names, easy to import in schema/arktype where we don't want it // should just import as * as ts when we need it in attest "typescript", - "./ark/type/main.ts", - "./ark/schema/main.ts" + "./ark/type/api.ts", + "./ark/schema/api.ts" ], "typescript.tsserver.experimental.enableProjectDiagnostics": true, // IF YOU UPDATE THE MOCHA CONFIG HERE, PLEASE ALSO UPDATE package.json/mocha AND ark/repo/mocha.jsonc diff --git a/ark/attest/README.md b/ark/attest/README.md index 550043fe93..58104c2530 100644 --- a/ark/attest/README.md +++ b/ark/attest/README.md @@ -37,12 +37,10 @@ export default defineConfig({ `setupVitest.ts` ```ts -import * as attest from "@arktype/attest" +import { setup, teardown } from "@arktype/attest" // config options can be passed here -export const setup = () => attest.setup({}) - -export const teardown = attest.teardown +export default () => setup({}) ``` ### Mocha @@ -124,6 +122,21 @@ describe("attest features", () => { attest<(number | bigint)[]>(numericArray.infer) } }) + + it("integrated type performance benchmarking", () => { + const user = type({ + kind: "'admin'", + "powers?": "string[]" + }) + .or({ + kind: "'superadmin'", + "superpowers?": "string[]" + }) + .or({ + kind: "'pleb'" + }) + attest.instantiations([7574, "instantiations"]) + }) }) ``` @@ -155,6 +168,12 @@ bench( .types([337, "instantiations"]) ``` +If you'd like to fail in CI above a threshold, you can add flags like the following (default value is 20%, but it will not throw unless `--benchErrorOnThresholdExceeded` is set): + +``` + tsx ./p99/within-limit/p99-tall-simple.bench.ts --benchErrorOnThresholdExceeded --benchPercentThreshold 10 +``` + ## CLI Attest also includes a builtin `attest` CLI including the following commands: diff --git a/ark/attest/__tests__/instantiations.test.ts b/ark/attest/__tests__/instantiations.test.ts index 3c4df29928..aefe232201 100644 --- a/ark/attest/__tests__/instantiations.test.ts +++ b/ark/attest/__tests__/instantiations.test.ts @@ -4,7 +4,17 @@ import { it } from "mocha" contextualize(() => { it("Inline instantiations", () => { - type("string") - attest.instantiations([1968, "instantiations"]) + const user = type({ + kind: "'admin'", + "powers?": "string[]" + }) + .or({ + kind: "'superadmin'", + "superpowers?": "string[]" + }) + .or({ + kind: "'pleb'" + }) + attest.instantiations([7574, "instantiations"]) }) }) diff --git a/ark/attest/__tests__/utils.ts b/ark/attest/__tests__/utils.ts index 1339c8c6a9..14702d86da 100644 --- a/ark/attest/__tests__/utils.ts +++ b/ark/attest/__tests__/utils.ts @@ -1,7 +1,9 @@ -import { dirName, readFile, shell } from "@arktype/fs" +import { dirName, fromHere, readFile, shell } from "@arktype/fs" import { copyFileSync, rmSync } from "node:fs" export const runThenGetContents = (templatePath: string): string => { + rmSync(fromHere(".attest"), { force: true, recursive: true }) + const tempPath = templatePath + ".temp.ts" copyFileSync(templatePath, tempPath) try { diff --git a/ark/attest/main.ts b/ark/attest/api.ts similarity index 100% rename from ark/attest/main.ts rename to ark/attest/api.ts diff --git a/ark/attest/assert/assertions.ts b/ark/attest/assert/assertions.ts index f6c0813cdf..456752006c 100644 --- a/ark/attest/assert/assertions.ts +++ b/ark/attest/assert/assertions.ts @@ -89,27 +89,30 @@ const unversionedAssertEquals: AssertFn = (expected, actual, ctx) => { } } -export const assertEquals = versionableAssertion(unversionedAssertEquals) +export const assertEquals: AssertFn = versionableAssertion( + unversionedAssertEquals +) -export const typeEqualityMapping = new TypeAssertionMapping(data => { - const expected = data.typeArgs[0] - const actual = data.typeArgs[1] ?? data.args[0] - if (!expected || !actual) - throwInternalError(`Unexpected type data ${printable(data)}`) +export const typeEqualityMapping: TypeAssertionMapping = + new TypeAssertionMapping(data => { + const expected = data.typeArgs[0] + const actual = data.typeArgs[1] ?? data.args[0] + if (!expected || !actual) + throwInternalError(`Unexpected type data ${printable(data)}`) - if (actual.relationships.typeArgs[0] !== "equality") { - return { - expected: expected.type, - actual: - expected.type === actual.type ? - "(serializes to same value)" - : actual.type + if (actual.relationships.typeArgs[0] !== "equality") { + return { + expected: expected.type, + actual: + expected.type === actual.type ? + "(serializes to same value)" + : actual.type + } } - } - return null -}) + return null + }) -export const assertEqualOrMatching = versionableAssertion( +export const assertEqualOrMatching: AssertFn = versionableAssertion( (expected, actual, ctx) => { const assertionArgs = { actual, expected, ctx } if (typeof actual !== "string") { diff --git a/ark/attest/assert/attest.ts b/ark/attest/assert/attest.ts index 249a6b7acd..6996c22415 100644 --- a/ark/attest/assert/attest.ts +++ b/ark/attest/assert/attest.ts @@ -72,17 +72,15 @@ export const attestInternal = ( return new ChainableAssertions(ctx) } -attestInternal.instantiations = ( - args: Measure<"instantiations"> | undefined -) => { - const attestConfig = getConfig() - if (attestConfig.skipInlineInstantiations) return +export const attest: AttestFn = Object.assign(attestInternal, { + instantiations: (args: Measure<"instantiations"> | undefined) => { + const attestConfig = getConfig() + if (attestConfig.skipInlineInstantiations) return - const calledFrom = caller() - const ctx = getBenchCtx([calledFrom.file]) - ctx.benchCallPosition = calledFrom - ctx.lastSnapCallPosition = calledFrom - instantiationDataHandler({ ...ctx, kind: "instantiations" }, args, false) -} - -export const attest: AttestFn = attestInternal as AttestFn + const calledFrom = caller() + const ctx = getBenchCtx([calledFrom.file]) + ctx.benchCallPosition = calledFrom + ctx.lastSnapCallPosition = calledFrom + instantiationDataHandler({ ...ctx, kind: "instantiations" }, args, false) + } +}) diff --git a/ark/attest/assert/chainableAssertions.ts b/ark/attest/assert/chainableAssertions.ts index 1a2e51193d..6886761bd1 100644 --- a/ark/attest/assert/chainableAssertions.ts +++ b/ark/attest/assert/chainableAssertions.ts @@ -1,10 +1,5 @@ import { caller } from "@arktype/fs" -import { - printable, - snapshot, - type Constructor, - type Guardable -} from "@arktype/util" +import { printable, snapshot, type Constructor } from "@arktype/util" import * as assert from "node:assert/strict" import { isDeepStrictEqual } from "node:util" import { @@ -39,7 +34,7 @@ export class ChainableAssertions implements AssertionRecord { return snapshot(value) } - private get actual() { + private get unversionedActual() { if (this.ctx.actual instanceof TypeAssertionMapping) { return this.ctx.actual.fn( this.ctx.typeRelationshipAssertionEntries![0][1], @@ -50,7 +45,7 @@ export class ChainableAssertions implements AssertionRecord { } private get serializedActual() { - return this.serialize(this.actual) + return this.serialize(this.unversionedActual) } private snapRequiresUpdate(expectedSerialized: unknown) { @@ -58,45 +53,31 @@ export class ChainableAssertions implements AssertionRecord { !isDeepStrictEqual(this.serializedActual, expectedSerialized) || // If actual is undefined, we still need to write the "undefined" literal // to the snap even though it will serialize to the same value as the (nonexistent) first arg - this.actual === undefined + this.unversionedActual === undefined ) } - narrow(predicate: Guardable, messageOnError?: string): never { - if (!predicate(this.actual)) { - throwAssertionError({ - ctx: this.ctx, - message: - messageOnError ?? - `${this.serializedActual} failed to satisfy predicate${ - predicate.name ? ` ${predicate.name}` : "" - }` - }) - } - return this.actual as never - } - get unknown(): this { return this } is(expected: unknown): this { - assert.equal(this.actual, expected) + assert.equal(this.unversionedActual, expected) return this } equals(expected: unknown): this { - assertEquals(expected, this.actual, this.ctx) + assertEquals(expected, this.ctx.actual, this.ctx) return this } instanceOf(expected: Constructor): this { - if (!(this.actual instanceof expected)) { + if (!(this.ctx.actual instanceof expected)) { throwAssertionError({ ctx: this.ctx, message: `Expected an instance of ${expected.name} (was ${ - typeof this.actual === "object" && this.actual !== null ? - this.actual.constructor.name + typeof this.ctx.actual === "object" && this.ctx.actual !== null ? + this.ctx.actual.constructor.name : this.serializedActual })` }) @@ -123,7 +104,7 @@ export class ChainableAssertions implements AssertionRecord { // to give a clearer error message. This avoid problems with objects // like subtypes of array that do not pass node's deep equality test // but serialize to the same value. - if (printable(args[0]) !== printable(this.actual)) + if (printable(args[0]) !== printable(this.unversionedActual)) assertEquals(expectedSerialized, this.serializedActual, this.ctx) } return this @@ -163,8 +144,8 @@ export class ChainableAssertions implements AssertionRecord { } } if (this.ctx.allowRegex) - assertEqualOrMatching(expected, this.actual, this.ctx) - else assertEquals(expected, this.actual, this.ctx) + assertEqualOrMatching(expected, this.ctx.actual, this.ctx) + else assertEquals(expected, this.ctx.actual, this.ctx) return this } @@ -174,7 +155,7 @@ export class ChainableAssertions implements AssertionRecord { } get throws(): unknown { - const result = callAssertedFunction(this.actual as Function) + const result = callAssertedFunction(this.unversionedActual as Function) this.ctx.actual = getThrownMessage(result, this.ctx) this.ctx.allowRegex = true this.ctx.defaultExpected = "" @@ -184,7 +165,10 @@ export class ChainableAssertions implements AssertionRecord { throwsAndHasTypeError(matchValue: string | RegExp): void { assertEqualOrMatching( matchValue, - getThrownMessage(callAssertedFunction(this.actual as Function), this.ctx), + getThrownMessage( + callAssertedFunction(this.unversionedActual as Function), + this.ctx + ), this.ctx ) if (!this.ctx.cfg.skipTypes) { @@ -294,10 +278,6 @@ export type comparableValueAssertion = { instanceOf: (constructor: Constructor) => nextAssertions is: (value: expected) => nextAssertions completions: (value?: Completions) => void - narrow( - predicate: (data: unknown) => data is narrowed, - messageOnError?: string - ): narrowed // This can be used to assert values without type constraints unknown: Omit, "unknown"> } diff --git a/ark/attest/bench/baseline.ts b/ark/attest/bench/baseline.ts index f48afaf2e6..cac8d8c2d6 100644 --- a/ark/attest/bench/baseline.ts +++ b/ark/attest/bench/baseline.ts @@ -41,9 +41,9 @@ export const compareToBaseline = ( result: MeasureComparison, ctx: BenchContext ): void => { - console.log(`🏌️ Result: ${stringifyMeasure(result.updated)}`) + console.log(`β›³ Result: ${stringifyMeasure(result.updated)}`) if (result.baseline && !ctx.cfg.updateSnapshots) { - console.log(`β›³ Baseline: ${stringifyMeasure(result.baseline)}`) + console.log(`🎯 Baseline: ${stringifyMeasure(result.baseline)}`) const delta = ((result.updated[0] - result.baseline[0]) / result.baseline[0]) * 100 const formattedDelta = `${delta.toFixed(2)}%` @@ -52,6 +52,8 @@ export const compareToBaseline = ( else if (delta < -ctx.cfg.benchPercentThreshold) handleNegativeDelta(formattedDelta, ctx) else console.log(`πŸ“Š Delta: ${delta > 0 ? "+" : ""}${formattedDelta}`) + // add an extra newline + console.log() } } diff --git a/ark/attest/bench/bench.ts b/ark/attest/bench/bench.ts index a64afdd192..28b9c131f9 100644 --- a/ark/attest/bench/bench.ts +++ b/ark/attest/bench/bench.ts @@ -28,6 +28,7 @@ export const bench = ( options: BenchOptions = {} ): InitialBenchAssertions => { const qualifiedPath = [...currentSuitePath, name] + console.log(`🏌️ ${qualifiedPath.join("/")}`) const ctx = getBenchCtx( qualifiedPath, fn.constructor.name === "AsyncFunction", diff --git a/ark/attest/bench/measure.ts b/ark/attest/bench/measure.ts index 916771cb45..33e671437e 100644 --- a/ark/attest/bench/measure.ts +++ b/ark/attest/bench/measure.ts @@ -15,9 +15,9 @@ export type MeasureComparison = { export type MarkMeasure = Partial> export const stringifyMeasure = ([value, units]: Measure): string => - units in TIME_UNIT_RATIOS ? + units in timeUnitRatios ? stringifyTimeMeasure([value, units as TimeUnit]) - : `${value}${units}` + : `${value} ${units}` export const TYPE_UNITS = ["instantiations"] as const @@ -33,30 +33,29 @@ export const createTypeComparison = ( } } -export const TIME_UNIT_RATIOS = Object.freeze({ +export const timeUnitRatios = { ns: 0.000_001, us: 0.001, ms: 1, s: 1000 -}) +} -export type TimeUnit = keyof typeof TIME_UNIT_RATIOS +export type TimeUnit = keyof typeof timeUnitRatios export const stringifyTimeMeasure = ([ value, unit ]: Measure): string => `${value.toFixed(2)}${unit}` -const convertTimeUnit = (n: number, from: TimeUnit, to: TimeUnit) => { - return round((n * TIME_UNIT_RATIOS[from]) / TIME_UNIT_RATIOS[to], 2) -} +const convertTimeUnit = (n: number, from: TimeUnit, to: TimeUnit) => + round((n * timeUnitRatios[from]) / timeUnitRatios[to], 2) /** * Establish a new baseline using the most appropriate time unit */ export const createTimeMeasure = (ms: number): Measure => { let bestMatch: Measure | undefined - for (const u in TIME_UNIT_RATIOS) { + for (const u in timeUnitRatios) { const candidateMeasure = createTimeMeasureForUnit(ms, u as TimeUnit) if (!bestMatch) bestMatch = candidateMeasure else if (bestMatch[0] >= 1) { @@ -89,4 +88,4 @@ export const createTimeComparison = ( updated: createTimeMeasure(ms), baseline: undefined } -} \ No newline at end of file +} diff --git a/ark/attest/cache/ts.ts b/ark/attest/cache/ts.ts index 1d06474064..e243eaa012 100644 --- a/ark/attest/cache/ts.ts +++ b/ark/attest/cache/ts.ts @@ -128,8 +128,7 @@ export const getTsConfigInfoOrThrow = (): TsconfigInfo => { {}, configFilePath ) - // ensure type.toString is as precise as possible - configParseResult.options.noErrorTruncation = true + if (configParseResult.errors.length > 0) { throw new Error( ts.formatDiagnostics(configParseResult.errors, { diff --git a/ark/attest/cache/utils.ts b/ark/attest/cache/utils.ts index 4e6cc25ea9..450744c571 100644 --- a/ark/attest/cache/utils.ts +++ b/ark/attest/cache/utils.ts @@ -118,14 +118,12 @@ export const getInstantiationsContributedByNode = ( baselineFile + `\nconst $attestIsolatedBench = ${benchBlock.getFullText()}` if (!instantiationsByPath[fakePath]) { - console.log(`⏳ attest: Analyzing type assertions...`) const instantiationsWithoutNode = getInstantiationsWithFile( baselineFile, fakePath ) instantiationsByPath[fakePath] = instantiationsWithoutNode - console.log(`⏳ Cached type assertions \n`) } const instantiationsWithNode = getInstantiationsWithFile( diff --git a/ark/attest/fixtures.ts b/ark/attest/fixtures.ts index 8600b82598..4bedc09c80 100644 --- a/ark/attest/fixtures.ts +++ b/ark/attest/fixtures.ts @@ -6,12 +6,12 @@ import { analyzeProjectAssertions } from "./cache/writeAssertionCache.js" import { ensureCacheDirs, getConfig, type AttestConfig } from "./config.js" import { forTypeScriptVersions } from "./tsVersioning.js" -export const setup = (options: Partial = {}): void => { +export const setup = (options: Partial = {}): typeof teardown => { const config = getConfig() Object.assign(config, options) rmSync(config.cacheDir, { recursive: true, force: true }) ensureCacheDirs() - if (config.skipTypes) return + if (config.skipTypes) return teardown if ( config.tsVersions.length === 1 && @@ -21,13 +21,14 @@ export const setup = (options: Partial = {}): void => { else { forTypeScriptVersions(config.tsVersions, version => shell( - `npm exec -c "attestPrecache ${join( + `npm exec -c "attest precache ${join( config.assertionCacheDir, version.alias + ".json" )}"` ) ) } + return teardown } export const writeAssertionData = (toPath: string): void => { @@ -40,4 +41,4 @@ export const writeAssertionData = (toPath: string): void => { export const cleanup = (): void => writeSnapshotUpdatesOnExit() /** alias for cleanup to align with vitest and others */ -export const teardown = cleanup +export const teardown: () => void = cleanup diff --git a/ark/attest/package.json b/ark/attest/package.json index a4c3a69495..785a66f7f3 100644 --- a/ark/attest/package.json +++ b/ark/attest/package.json @@ -1,27 +1,20 @@ { "name": "@arktype/attest", - "version": "0.7.0", + "version": "0.7.5", "author": { "name": "David Blass", "email": "david@arktype.io", "url": "https://arktype.io" }, "type": "module", - "main": "./out/main.js", - "types": "./out/main.d.ts", + "main": "./out/api.js", + "types": "./out/api.d.ts", "exports": { - ".": { - "types": "./out/main.d.ts", - "default": "./out/main.js" - }, - "./internal/*": { - "default": "./out/*" - } + ".": "./out/api.js", + "./internal/*": "./out/*" }, "files": [ - "out", - "!__tests__", - "**/*.ts" + "out" ], "bin": { "attest": "./out/cli/cli.js" diff --git a/ark/attest/tsconfig.build.json b/ark/attest/tsconfig.build.json index 80a796b963..f74ef64d4c 120000 --- a/ark/attest/tsconfig.build.json +++ b/ark/attest/tsconfig.build.json @@ -1 +1 @@ -../repo/tsconfig.build.json \ No newline at end of file +../repo/tsconfig.esm.json \ No newline at end of file diff --git a/ark/dark/package.json b/ark/dark/package.json index 11a8c5bf91..232bfc6aad 100644 --- a/ark/dark/package.json +++ b/ark/dark/package.json @@ -2,7 +2,7 @@ "name": "arkdark", "displayName": "ArkDark", "description": "ArkType syntax highlighting and themeβ›΅", - "version": "4.0.1", + "version": "5.0.2", "publisher": "arktypeio", "type": "module", "scripts": { @@ -51,19 +51,19 @@ }, "errorLens.replace": [ { - "matcher": "[^]*\n([^\n]*)$", + "matcher": "^(?:Type|Argument of type) '.*' is not assignable to type 'keyError<\"(.*)\">'\\.$", "message": "$1" }, { - "matcher": "Argument of type '.*' is not assignable to parameter of type '\"(.*)\"'.", + "matcher": "^(?:Type|Argument of type) '.*' is not assignable to type '\"(.*\\u200A)\"'\\.$", "message": "$1" }, { - "matcher": "Type '.*' is not assignable to type '\"(.*)\"'.", - "message": "$1" + "matcher": "^(?:Type|Argument of type) '\"(.*)\"' is not assignable to type '(\"\\1.*\")'\\.$", + "message": "$2" }, { - "matcher": "Type '.*' is not assignable to type 'indexParseError<(.*)>'.", + "matcher": "[^]*\n([^\n]*)$", "message": "$1" } ] diff --git a/ark/fs/main.ts b/ark/fs/api.ts similarity index 100% rename from ark/fs/main.ts rename to ark/fs/api.ts diff --git a/ark/fs/fs.ts b/ark/fs/fs.ts index 4249ea90c6..859c098fe0 100644 --- a/ark/fs/fs.ts +++ b/ark/fs/fs.ts @@ -109,7 +109,7 @@ export const fromCwd = (...joinWith: string[]): string => export const fromHome = (...joinWith: string[]): string => join(homedir()!, ...joinWith) -export const fsRoot = parse(process.cwd()).root +export const fsRoot: string = parse(process.cwd()).root export const findPackageRoot = (fromDir?: string): string => { const startDir = fromDir ?? dirOfCaller() @@ -145,7 +145,7 @@ export const getSourceControlPaths = (): string[] => .split("\n") .filter(path => existsSync(path) && statSync(path).isFile()) -export const tsFileMatcher = /^.*\.(c|m)?tsx?$/ +export const tsFileMatcher: RegExp = /^.*\.(c|m)?tsx?$/ const inFileFilter: WalkOptions = { include: path => tsFileMatcher.test(path), diff --git a/ark/fs/package.json b/ark/fs/package.json index fbb1ee3a06..c9ec914519 100644 --- a/ark/fs/package.json +++ b/ark/fs/package.json @@ -1,29 +1,22 @@ { "name": "@arktype/fs", - "version": "0.0.17", + "version": "0.0.19", "author": { "name": "David Blass", "email": "david@arktype.io", "url": "https://arktype.io" }, "type": "module", - "main": "./out/main.js", - "types": "./out/main.d.ts", + "main": "./out/api.js", + "types": "./out/api.d.ts", "exports": { - ".": { - "types": "./out/main.d.ts", - "default": "./out/main.js" - }, - "./internal/*": { - "default": "./out/*" - } + ".": "./out/api.js", + "./internal/*": "./out/*" }, "scripts": { "build": "tsx ../repo/build.ts" }, "files": [ - "out", - "!__tests__", - "**/*.ts" + "out" ] } diff --git a/ark/fs/tsconfig.build.json b/ark/fs/tsconfig.build.json index 80a796b963..f74ef64d4c 120000 --- a/ark/fs/tsconfig.build.json +++ b/ark/fs/tsconfig.build.json @@ -1 +1 @@ -../repo/tsconfig.build.json \ No newline at end of file +../repo/tsconfig.esm.json \ No newline at end of file diff --git a/ark/repo/.eslintrc.cjs b/ark/repo/.eslintrc.cjs index 8142b92e02..cc5161e2fa 100644 --- a/ark/repo/.eslintrc.cjs +++ b/ark/repo/.eslintrc.cjs @@ -7,8 +7,7 @@ module.exports = defineConfig({ "@typescript-eslint", "import", "only-warn", - "prefer-arrow-functions", - "unused-imports" + "prefer-arrow-functions" ], extends: [ "eslint:recommended", @@ -55,12 +54,11 @@ module.exports = defineConfig({ "@typescript-eslint/no-unused-vars": [ "warn", { + args: "after-used", + argsIgnorePattern: "^_", ignoreRestSiblings: true } ], - "unused-imports/no-unused-imports": "warn", - // Can be replaced by --isolatedDeclarations in tsconfig once it's released: - // https://github.com/microsoft/TypeScript/pull/53463 "@typescript-eslint/explicit-module-boundary-types": [ "warn", { allowDirectConstAssertionInArrowFunctions: true } @@ -94,7 +92,7 @@ module.exports = defineConfig({ message: `Use a specifier like '@arktype/util' to import from a package` }, { - group: ["**/main.js"], + group: ["**/api.js"], message: `Use a path like '../original/definition.js' instead of a package entrypoint` } ] diff --git a/ark/repo/bench.ts b/ark/repo/bench.ts new file mode 100644 index 0000000000..a67a57cceb --- /dev/null +++ b/ark/repo/bench.ts @@ -0,0 +1,39 @@ +import { bench } from "@arktype/attest" +import { type } from "arktype" + +export const validData = Object.freeze({ + number: 1, + negNumber: -1, + maxNumber: Number.MAX_VALUE, + string: "string", + longString: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Vivendum intellegat et qui, ei denique consequuntur vix. Semper aeterno percipit ut his, sea ex utinam referrentur repudiandae. No epicuri hendrerit consetetur sit, sit dicta adipiscing ex, in facete detracto deterruisset duo. Quot populo ad qui. Sit fugit nostrum et. Ad per diam dicant interesset, lorem iusto sensibus ut sed. No dicam aperiam vis. Pri posse graeco definitiones cu, id eam populo quaestio adipiscing, usu quod malorum te. Ex nam agam veri, dicunt efficiantur ad qui, ad legere adversarium sit. Commune platonem mel id, brute adipiscing duo an. Vivendum intellegat et qui, ei denique consequuntur vix. Offendit eleifend moderatius ex vix, quem odio mazim et qui, purto expetendis cotidieque quo cu, veri persius vituperata ei nec. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + boolean: true, + deeplyNested: { + foo: "bar", + num: 1, + bool: false + } +}) + +export const t = type({ + number: "number", + negNumber: "number", + maxNumber: "number", + string: "string", + longString: "string", + boolean: "boolean", + deeplyNested: { + foo: "string", + num: "number", + bool: "boolean" + } +}) + +bench("allows", () => { + t.allows(validData) +}).median([5.59, "ns"]) + +bench("apply", () => { + t(validData) +}).median([7.01, "ns"]) diff --git a/ark/repo/build.ts b/ark/repo/build.ts index ccb96a692f..ef25fd305d 100644 --- a/ark/repo/build.ts +++ b/ark/repo/build.ts @@ -1,6 +1,6 @@ import { symlinkSync, unlinkSync } from "fs" // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { fromCwd, shell, writeJson } from "../fs/main.js" +import { fromCwd, shell, writeJson } from "../fs/api.js" const isCjs = process.argv.includes("--cjs") || process.env.ARKTYPE_CJS diff --git a/ark/repo/examples/syntax.ts b/ark/repo/examples/syntax.ts index d78bab9aa8..bdce874c58 100644 --- a/ark/repo/examples/syntax.ts +++ b/ark/repo/examples/syntax.ts @@ -1,6 +1,5 @@ -import { type } from "arktype" -import type { Out } from "../../schema/schemas/morph.js" -import type { Type } from "../../type/type.js" +import type { Out } from "@arktype/schema" +import { type, type Type } from "arktype" // Syntax carried over from 1.0 + TS export const currentTsSyntax = type({ @@ -30,7 +29,7 @@ export const upcomingTsSyntax = type({ export const validationSyntax = type({ keywords: "email|uuid|creditCard|integer", // and many more - builtinParsers: "parse.date", // parses a Date from a string + // builtinParsers: "parse.date", // parses a Date from a string nativeRegexLiteral: /@arktype\.io/, embeddedRegexLiteral: "email&/@arktype\\.io/", divisibility: "number%10", // a multiple of 10 diff --git a/ark/repo/scratch.ts b/ark/repo/scratch.ts index 885431668b..d01f668c4e 100644 --- a/ark/repo/scratch.ts +++ b/ark/repo/scratch.ts @@ -1,40 +1,23 @@ -import { bench } from "@arktype/attest" import { type } from "arktype" -import "./arkConfig.js" -export const validData = Object.freeze({ - number: 1, - negNumber: -1, - maxNumber: Number.MAX_VALUE, - string: "string", - longString: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Vivendum intellegat et qui, ei denique consequuntur vix. Semper aeterno percipit ut his, sea ex utinam referrentur repudiandae. No epicuri hendrerit consetetur sit, sit dicta adipiscing ex, in facete detracto deterruisset duo. Quot populo ad qui. Sit fugit nostrum et. Ad per diam dicant interesset, lorem iusto sensibus ut sed. No dicam aperiam vis. Pri posse graeco definitiones cu, id eam populo quaestio adipiscing, usu quod malorum te. Ex nam agam veri, dicunt efficiantur ad qui, ad legere adversarium sit. Commune platonem mel id, brute adipiscing duo an. Vivendum intellegat et qui, ei denique consequuntur vix. Offendit eleifend moderatius ex vix, quem odio mazim et qui, purto expetendis cotidieque quo cu, veri persius vituperata ei nec. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", - boolean: true, - deeplyNested: { - foo: "bar", - num: 1, - bool: false - } -}) +// type Z = Type<{ age: number.default<5> }> + +const f = (arg?: string) => {} -export const t = type({ - number: "number", - negNumber: "number", - maxNumber: "number", - string: "string", - longString: "string", - boolean: "boolean", - deeplyNested: { - foo: "string", - num: "number", - bool: "boolean" - } +const user = type({ + "+": "delete", + name: "string>10", + email: "email" + // age: ["number", "=", 5] }) -bench("allows", () => { - t.allows(validData) -}).median([5.59, "ns"]) +const out = user({ + name: "test", + email: "" +}) -bench("apply", () => { - t(validData) -}).median([7.01, "ns"]) +if (out instanceof type.errors) { + console.log(out.summary) +} else { + console.log(out) +} diff --git a/ark/repo/scratch/typeClass.ts b/ark/repo/scratch/typeClass.ts index b5fff6aa66..21232a682f 100644 --- a/ark/repo/scratch/typeClass.ts +++ b/ark/repo/scratch/typeClass.ts @@ -1,7 +1,6 @@ import type { Ark } from "@arktype/schema" import { DynamicBase } from "@arktype/util" -import { ambient, type } from "../../type/ark.js" -import type { inferTypeRoot, validateTypeRoot } from "../../type/type.js" +import { type, type inferTypeRoot, type validateTypeRoot } from "arktype" const Class = (def: validateTypeRoot) => { const validator = type(def as never) diff --git a/ark/repo/testTsVersions.ts b/ark/repo/testTsVersions.ts index f23b9ff755..8697125e87 100644 --- a/ark/repo/testTsVersions.ts +++ b/ark/repo/testTsVersions.ts @@ -4,4 +4,4 @@ import { shell } from "@arktype/fs" process.env.ATTEST_CONFIG = JSON.stringify({ tsVersions: "*" } satisfies AttestConfig) -shell("pnpm test") +shell("pnpm testTyped") diff --git a/ark/repo/tsconfig.cjs.json b/ark/repo/tsconfig.cjs.json index acb1d03be2..dd314955eb 100644 --- a/ark/repo/tsconfig.cjs.json +++ b/ark/repo/tsconfig.cjs.json @@ -1,14 +1,10 @@ { + "extends": "../../tsconfig.json", "compilerOptions": { - "target": "ES2022", "module": "CommonJS", "moduleResolution": "Node", - "esModuleInterop": true, - "sourceMap": true, - "composite": true, - "declarationMap": true, - "skipLibCheck": true, - "strict": true, + "verbatimModuleSyntax": false, + "rootDir": ".", "outDir": "out", "noEmit": false, "paths": {} diff --git a/ark/schema/__tests__/bounds.test.ts b/ark/schema/__tests__/bounds.test.ts index 39b9aa1f74..c96ba61df2 100644 --- a/ark/schema/__tests__/bounds.test.ts +++ b/ark/schema/__tests__/bounds.test.ts @@ -1,7 +1,7 @@ import { attest, contextualize } from "@arktype/attest" import { schema } from "@arktype/schema" import { entriesOf, flatMorph } from "@arktype/util" -import { boundKindPairsByLower } from "../constraints/refinements/range.js" +import { boundKindPairsByLower } from "../refinements/range.js" import { Disjoint } from "../shared/disjoint.js" const numericCases = { diff --git a/ark/schema/__tests__/errors.test.ts b/ark/schema/__tests__/errors.test.ts index 959bab19c8..e6a1f1fd3e 100644 --- a/ark/schema/__tests__/errors.test.ts +++ b/ark/schema/__tests__/errors.test.ts @@ -15,7 +15,7 @@ contextualize(() => { it("at path", () => { const o = schema({ domain: "object", - prop: { + required: { key: "foo", value: { domain: "number", diff --git a/ark/schema/__tests__/morphs.test.ts b/ark/schema/__tests__/morphs.test.ts index bc2d65ebb0..8c81558a67 100644 --- a/ark/schema/__tests__/morphs.test.ts +++ b/ark/schema/__tests__/morphs.test.ts @@ -5,7 +5,7 @@ import { wellFormedNumberMatcher } from "@arktype/util" contextualize(() => { it("in/out", () => { const parseNumber = schema({ - from: { + in: { domain: "string", regex: wellFormedNumberMatcher, description: "a well-formed numeric string" @@ -23,7 +23,7 @@ contextualize(() => { it("in/out union", () => { const n = schema([ { - from: "string", + in: "string", morphs: (s: string) => Number.parseFloat(s) }, "number" diff --git a/ark/schema/__tests__/parse.test.ts b/ark/schema/__tests__/parse.test.ts index 1ee85a3e2c..b19cd5979d 100644 --- a/ark/schema/__tests__/parse.test.ts +++ b/ark/schema/__tests__/parse.test.ts @@ -1,10 +1,10 @@ import { attest, contextualize } from "@arktype/attest" -import { type Schema, schema } from "@arktype/schema" +import { type Root, schema } from "@arktype/schema" contextualize(() => { it("single constraint", () => { const t = schema({ domain: "string", regex: ".*" }) - attest>(t) + attest>(t) attest(t.json).snap({ domain: "string", regex: [".*"] }) }) @@ -19,7 +19,7 @@ contextualize(() => { divisor: 5 }) const result = l.and(r) - attest>(result) + attest>(result) attest(result.json).snap({ domain: "number", divisor: 15, diff --git a/ark/schema/__tests__/props.test.ts b/ark/schema/__tests__/props.test.ts index c42c28be4e..aded45c2b1 100644 --- a/ark/schema/__tests__/props.test.ts +++ b/ark/schema/__tests__/props.test.ts @@ -5,36 +5,199 @@ contextualize(() => { it("normalizes prop order", () => { const l = schema({ domain: "object", - prop: [ + required: [ { key: "a", value: "string" }, { key: "b", value: "number" } ] }) const r = schema({ domain: "object", - prop: [ + required: [ { key: "b", value: "number" }, { key: "a", value: "string" } ] }) attest(l.json).equals(r.json) }) - it("strict intersection", () => { + + it("preserves matching literal", () => { + const l = schema({ + domain: "object", + index: [{ signature: "string", value: "string" }], + undeclared: "reject" + }) + + const r = schema({ + domain: "object", + required: [{ key: "a", value: { unit: "foo" } }] + }) + + const result = l.and(r) + + attest(result.json).snap({ + undeclared: "reject", + required: [{ key: "a", value: { unit: "foo" } }], + index: [{ value: "string", signature: "string" }], + domain: "object" + }) + }) + + it("preserves matching index", () => { + const l = schema({ + domain: "object", + index: [{ signature: "string", value: "string" }], + undeclared: "reject" + }) + + const r = schema({ + domain: "object", + index: [{ signature: "string", value: { unit: "foo" } }] + }) + + const result = l.and(r) + + attest(result.json).snap({ + undeclared: "reject", + index: [{ signature: "string", value: { unit: "foo" } }], + domain: "object" + }) + }) + + const startingWithA = schema({ domain: "string", regex: /^a.*/ }) + + const endingWithZ = schema({ domain: "string", regex: /.*z$/ }) + + const startingWithAAndEndingWithZ = schema({ + domain: "string", + regex: [/^a.*/, /.*z$/] + }) + + it("intersects nonsubtype index signatures", () => { + const l = schema({ + domain: "object", + index: [{ signature: startingWithA, value: "string" }], + undeclared: "reject" + }) + + const r = schema({ + domain: "object", + index: [{ signature: endingWithZ, value: { unit: "foo" } }] + }) + + const result = l.and(r) + + const expected = schema({ + domain: "object", + index: [ + { signature: startingWithA, value: "string" }, + { signature: startingWithAAndEndingWithZ, value: { unit: "foo" } } + ], + undeclared: "reject" + }) + + attest(result.json).snap(expected.json) + }) + + it("intersects non-subtype strict index signatures", () => { + const l = schema({ + domain: "object", + index: [{ signature: startingWithA, value: endingWithZ }], + undeclared: "reject" + }) + + const r = schema({ + domain: "object", + index: [{ signature: endingWithZ, value: startingWithA }], + undeclared: "reject" + }) + + const result = l.and(r) + + const expected = schema({ + domain: "object", + index: [ + { + signature: startingWithAAndEndingWithZ, + value: startingWithAAndEndingWithZ + } + ], + undeclared: "reject" + }) + + attest(result.json).equals(expected.json) + }) + + it("prunes undeclared optional", () => { + const l = schema({ + domain: "object", + required: [{ key: "a", value: "string" }], + undeclared: "reject" + }) + + const r = schema({ + domain: "object", + optional: [{ key: "b", value: "number" }] + }) + + const result = l.and(r) + + attest(result.json).snap(l.json) + }) + + it("prunes undeclared index", () => { const l = schema({ domain: "object", - prop: [ + index: [{ signature: "string", value: "string" }] + }) + + const r = schema({ + domain: "object", + required: [{ key: "a", value: { unit: "foo" } }], + undeclared: "reject" + }) + + const result = l.and(r) + + attest(result.json).snap({ + undeclared: "reject", + required: [{ key: "a", value: { unit: "foo" } }], + domain: "object" + }) + }) + + it("undeclared required", () => { + const l = schema({ + domain: "object", + required: [ { key: "a", value: "string" }, { key: "b", value: "number" } ] }) const r = schema({ domain: "object", - prop: [{ key: "a", value: "string" }], - onExtraneousKey: "throw" + required: [{ key: "a", value: "string" }], + undeclared: "reject" }) attest(() => l.and(r)).throws.snap( - "ParseError: Intersection at b of true and false results in an unsatisfiable type" + "ParseError: Intersection at b of number and never results in an unsatisfiable type" ) }) + + it("delete & reject", () => { + const l = schema({ + domain: "object", + required: [{ key: "a", value: "string" }], + undeclared: "delete" + }) + const r = schema({ + domain: "object", + required: [{ key: "a", value: "string" }], + undeclared: "reject" + }) + + const result = l.and(r) + + attest(result.json).equals(r.json) + }) }) diff --git a/ark/schema/__tests__/scope.test.ts b/ark/schema/__tests__/scope.test.ts index a00e29ec76..5536d3a2c2 100644 --- a/ark/schema/__tests__/scope.test.ts +++ b/ark/schema/__tests__/scope.test.ts @@ -6,7 +6,7 @@ contextualize(() => { const types = schemaScope({ a: { domain: "object", - prop: { + required: { key: "b", value: "$b" } @@ -17,7 +17,7 @@ contextualize(() => { }).export() attest(types.a.json).snap({ domain: "object", - prop: [{ key: "b", value: "string" }] + required: [{ key: "b", value: "string" }] }) attest(types.b.json).snap({ domain: "string" }) }) @@ -25,14 +25,14 @@ contextualize(() => { const types = schemaScope({ a: { domain: "object", - prop: { + required: { key: "b", value: "$b" } }, b: { domain: "object", - prop: { + required: { key: "a", value: "$a" } @@ -41,17 +41,17 @@ contextualize(() => { attest(types.a.json).snap({ domain: "object", - prop: [ + required: [ { key: "b", - value: { domain: "object", prop: [{ key: "a", value: "$a" }] } + value: { domain: "object", required: [{ key: "a", value: "$a" }] } } ] }) attest(types.b.json).snap({ domain: "object", - prop: [{ key: "a", value: "$a" }] + required: [{ key: "a", value: "$a" }] }) const a = {} as { b: typeof b } diff --git a/ark/schema/api.ts b/ark/schema/api.ts new file mode 100644 index 0000000000..5c20e0a7d9 --- /dev/null +++ b/ark/schema/api.ts @@ -0,0 +1,43 @@ +export * from "./ast.js" +export * from "./config.js" +export * from "./constraint.js" +export * from "./generic.js" +export * from "./inference.js" +export * from "./keywords/internal.js" +export * from "./keywords/jsObjects.js" +export * from "./keywords/keywords.js" +export * from "./keywords/parsing.js" +export * from "./keywords/tsKeywords.js" +export * from "./keywords/validation.js" +export * from "./kinds.js" +export * from "./module.js" +export * from "./node.js" +export * from "./parse.js" +export * from "./predicate.js" +export * from "./refinements/after.js" +export * from "./refinements/before.js" +export * from "./refinements/divisor.js" +export * from "./refinements/max.js" +export * from "./refinements/maxLength.js" +export * from "./refinements/min.js" +export * from "./refinements/minLength.js" +export * from "./refinements/range.js" +export * from "./refinements/regex.js" +export * from "./roots/discriminate.js" +export * from "./roots/intersection.js" +export * from "./roots/morph.js" +export * from "./roots/root.js" +export * from "./roots/union.js" +export * from "./roots/unit.js" +export * from "./scope.js" +export * from "./shared/declare.js" +export * from "./shared/disjoint.js" +export * from "./shared/errors.js" +export * from "./shared/implement.js" +export * from "./shared/intersections.js" +export * from "./shared/utils.js" +export * from "./structure/index.js" +export * from "./structure/optional.js" +export * from "./structure/prop.js" +export * from "./structure/sequence.js" +export * from "./structure/structure.js" diff --git a/ark/schema/constraints/ast.ts b/ark/schema/ast.ts similarity index 82% rename from ark/schema/constraints/ast.ts rename to ark/schema/ast.ts index d8039a14ac..d7de567c68 100644 --- a/ark/schema/constraints/ast.ts +++ b/ark/schema/ast.ts @@ -1,7 +1,7 @@ import type { conform } from "@arktype/util" -import type { NodeDef } from "../kinds.js" -import type { constraintKindOf } from "../schemas/intersection.js" -import type { PrimitiveConstraintKind } from "./util.js" +import type { PrimitiveConstraintKind } from "./constraint.js" +import type { NodeSchema } from "./kinds.js" +import type { constraintKindOf } from "./roots/intersection.js" export type Comparator = "<" | "<=" | ">" | ">=" | "==" @@ -94,15 +94,15 @@ export namespace number { export type constrain< kind extends PrimitiveConstraintKind, - def extends NodeDef + schema extends NodeSchema > = - normalizePrimitiveConstraintSchema extends infer rule ? + normalizePrimitiveConstraintRoot extends infer rule ? kind extends "min" ? - def extends { exclusive: true } ? + schema extends { exclusive: true } ? moreThan : atLeast : kind extends "max" ? - def extends { exclusive: true } ? + schema extends { exclusive: true } ? lessThan : atMost : kind extends "divisor" ? divisibleBy @@ -152,9 +152,9 @@ export namespace string { export type constrain< kind extends PrimitiveConstraintKind, - schema extends NodeDef + schema extends NodeSchema > = - normalizePrimitiveConstraintSchema extends infer rule ? + normalizePrimitiveConstraintRoot extends infer rule ? kind extends "minLength" ? schema extends { exclusive: true } ? moreThanLength @@ -207,9 +207,9 @@ export namespace Date { export type constrain< kind extends PrimitiveConstraintKind, - schema extends NodeDef + schema extends NodeSchema > = - normalizePrimitiveConstraintSchema extends infer rule ? + normalizePrimitiveConstraintRoot extends infer rule ? kind extends "after" ? schema extends { exclusive: true } ? after> @@ -225,9 +225,9 @@ export namespace Date { export type constrain< t, kind extends PrimitiveConstraintKind, - def extends NodeDef + schema extends NodeSchema > = - schemaToConstraint extends infer constraint ? + schemaToConstraint extends infer constraint ? t extends of ? [number, base] extends [base, number] ? number.is @@ -235,48 +235,48 @@ export type constrain< string.is : [Date, base] extends [base, Date] ? Date.is : of - : [number, t] extends [t, number] ? number.constrain - : [string, t] extends [t, string] ? string.constrain - : [Date, t] extends [t, Date] ? Date.constrain + : [number, t] extends [t, number] ? number.constrain + : [string, t] extends [t, string] ? string.constrain + : [Date, t] extends [t, Date] ? Date.constrain : of> : never -export type normalizePrimitiveConstraintSchema< - def extends NodeDef +export type normalizePrimitiveConstraintRoot< + schema extends NodeSchema > = - "rule" extends keyof def ? conform - : conform + "rule" extends keyof schema ? conform + : conform export type schemaToConstraint< kind extends PrimitiveConstraintKind, - def extends NodeDef + schema extends NodeSchema > = - normalizePrimitiveConstraintSchema extends infer rule ? + normalizePrimitiveConstraintRoot extends infer rule ? kind extends "regex" ? Matching : kind extends "divisor" ? DivisibleBy : kind extends "exactLength" ? Length : kind extends "min" ? - def extends { exclusive: true } ? + schema extends { exclusive: true } ? MoreThan : AtLeast : kind extends "max" ? - def extends { exclusive: true } ? + schema extends { exclusive: true } ? LessThan : AtMost : kind extends "minLength" ? - def extends { exclusive: true } ? + schema extends { exclusive: true } ? MoreThanLength : AtLeastLength : kind extends "maxLength" ? - def extends { exclusive: true } ? + schema extends { exclusive: true } ? LessThanLength : AtMostLength : kind extends "after" ? - def extends { exclusive: true } ? + schema extends { exclusive: true } ? After> : AtOrAfter> : kind extends "before" ? - def extends { exclusive: true } ? + schema extends { exclusive: true } ? Before> : AtOrBefore> : Narrowed diff --git a/ark/schema/constraint.ts b/ark/schema/constraint.ts new file mode 100644 index 0000000000..ba74af180d --- /dev/null +++ b/ark/schema/constraint.ts @@ -0,0 +1,254 @@ +import { + append, + appendUnique, + capitalize, + isArray, + throwInternalError, + throwParseError, + type array, + type describeExpression, + type listable, + type satisfy +} from "@arktype/util" +import type { + Inner, + MutableInner, + Node, + NodeSchema, + Prerequisite, + innerAttachedAs +} from "./kinds.js" +import { BaseNode } from "./node.js" +import type { NodeParseContext } from "./parse.js" +import type { + IntersectionInner, + MutableIntersectionInner +} from "./roots/intersection.js" +import type { BaseRoot, Root, UnknownRoot } from "./roots/root.js" +import type { NodeCompiler } from "./shared/compile.js" +import type { RawNodeDeclaration } from "./shared/declare.js" +import { Disjoint } from "./shared/disjoint.js" +import { + compileErrorContext, + constraintKeys, + type ConstraintKind, + type IntersectionContext, + type NodeKind, + type RootKind, + type StructuralKind, + type kindLeftOf +} from "./shared/implement.js" +import { intersectNodes, intersectNodesRoot } from "./shared/intersections.js" +import type { TraverseAllows, TraverseApply } from "./shared/traversal.js" +import { arkKind } from "./shared/utils.js" + +export interface BaseConstraintDeclaration extends RawNodeDeclaration { + kind: ConstraintKind +} + +export abstract class BaseConstraint< + /** uses -ignore rather than -expect-error because this is not an error in .d.ts + * @ts-ignore allow instantiation assignment to the base type */ + out d extends BaseConstraintDeclaration = BaseConstraintDeclaration +> extends BaseNode { + readonly [arkKind] = "constraint" + abstract readonly impliedBasis: BaseRoot | null + readonly impliedSiblings?: array + + intersect( + r: r + ): intersectConstraintKinds { + return intersectNodesRoot(this, r, this.$) as never + } +} + +export type ConstraintReductionResult = + | BaseRoot + | Disjoint + | MutableIntersectionInner + +export abstract class RawPrimitiveConstraint< + d extends BaseConstraintDeclaration +> extends BaseConstraint { + abstract traverseAllows: TraverseAllows + abstract readonly compiledCondition: string + abstract readonly compiledNegation: string + + traverseApply: TraverseApply = (data, ctx) => { + if (!this.traverseAllows(data, ctx)) ctx.error(this.errorContext as never) + } + + compile(js: NodeCompiler): void { + js.compilePrimitive(this as never) + } + + get errorContext(): d["errorContext"] { + return { code: this.kind, description: this.description, ...this.inner } + } + + get compiledErrorContext(): string { + return compileErrorContext(this.errorContext!) + } +} + +export const constraintKeyParser = + (kind: kind) => + ( + schema: listable>, + ctx: NodeParseContext + ): innerAttachedAs | undefined => { + if (isArray(schema)) { + if (schema.length === 0) { + // Omit empty lists as input + return + } + return schema + .map(schema => ctx.$.node(kind, schema as never)) + .sort((l, r) => (l.innerHash < r.innerHash ? -1 : 1)) as never + } + const child = ctx.$.node(kind, schema) + return child.hasOpenIntersection() ? [child] : (child as any) + } + +type ConstraintGroupKind = satisfy + +interface ConstraintIntersectionState< + kind extends ConstraintGroupKind = ConstraintGroupKind +> { + kind: kind + baseInner: Record + l: BaseConstraint[] + r: BaseConstraint[] + roots: BaseRoot[] + ctx: IntersectionContext +} + +export const intersectConstraints = ( + s: ConstraintIntersectionState +): Node> | Disjoint => { + const head = s.r.shift() + if (!head) { + let result: BaseNode | Disjoint = + s.l.length === 0 && s.kind === "structure" ? + s.ctx.$.keywords.unknown.raw + : s.ctx.$.node( + s.kind, + Object.assign(s.baseInner, unflattenConstraints(s.l)), + { prereduced: true } + ) + + for (const root of s.roots) { + if (result instanceof Disjoint) return result + + result = intersectNodes(root, result, s.ctx)! + } + + return result as never + } + let matched = false + for (let i = 0; i < s.l.length; i++) { + const result = intersectNodes(s.l[i], head, s.ctx) + if (result === null) continue + if (result instanceof Disjoint) return result + + if (!matched) { + if (result.isRoot()) { + s.roots.push(result) + s.l.splice(i) + return intersectConstraints(s) + } + s.l[i] = result as BaseConstraint + matched = true + } else if (!s.l.includes(result as never)) { + return throwInternalError( + `Unexpectedly encountered multiple distinct intersection results for refinement ${result}` + ) + } + } + if (!matched) s.l.push(head) + + if (s.kind === "intersection") + head.impliedSiblings?.forEach(node => appendUnique(s.r, node)) + return intersectConstraints(s) +} + +export const flattenConstraints = (inner: object): BaseConstraint[] => { + const result = Object.entries(inner) + .flatMap(([k, v]) => + k in constraintKeys ? (v as listable) : [] + ) + .sort((l, r) => + l.precedence < r.precedence ? -1 + : l.precedence > r.precedence ? 1 + : l.innerHash < r.innerHash ? -1 + : 1 + ) + + return result +} + +// TODO: Fix type +export const unflattenConstraints = ( + constraints: array +): IntersectionInner & Inner<"structure"> => { + const inner: MutableIntersectionInner & MutableInner<"structure"> = {} + for (const constraint of constraints) { + if (constraint.hasOpenIntersection()) { + inner[constraint.kind] = append( + inner[constraint.kind], + constraint + ) as never + } else { + if (inner[constraint.kind]) { + return throwInternalError( + `Unexpected intersection of closed refinements of kind ${constraint.kind}` + ) + } + inner[constraint.kind] = constraint as never + } + } + return inner +} + +export type constraintKindLeftOf = ConstraintKind & + kindLeftOf + +export type constraintKindOrLeftOf = + | kind + | constraintKindLeftOf + +export type intersectConstraintKinds< + l extends ConstraintKind, + r extends ConstraintKind +> = Node | Disjoint | null + +export const throwInvalidOperandError = ( + ...args: Parameters +): never => throwParseError(writeInvalidOperandMessage(...args)) + +export const writeInvalidOperandMessage = < + kind extends ConstraintKind, + expected extends Root, + actual extends Root +>( + kind: kind, + expected: expected, + actual: actual +): writeInvalidOperandMessage => + `${capitalize(kind)} operand must be ${ + expected.description + } (was ${actual.exclude(expected).description})` as never + +export type writeInvalidOperandMessage< + kind extends ConstraintKind, + actual extends Root +> = `${Capitalize} operand must be ${describeExpression< + Prerequisite +>} (was ${describeExpression>>})` + +export interface ConstraintAttachments { + impliedBasis: UnknownRoot | null + impliedSiblings?: array | null +} + +export type PrimitiveConstraintKind = Exclude diff --git a/ark/schema/constraints/constraint.ts b/ark/schema/constraints/constraint.ts deleted file mode 100644 index 2088092f24..0000000000 --- a/ark/schema/constraints/constraint.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { RawNode } from "../node.js" -import type { RawSchema } from "../schema.js" -import type { NodeCompiler } from "../shared/compile.js" -import type { RawNodeDeclaration } from "../shared/declare.js" -import { - compileErrorContext, - type ConstraintKind, - type PropKind -} from "../shared/implement.js" -import { intersectNodesRoot } from "../shared/intersections.js" -import type { TraverseAllows, TraverseApply } from "../shared/traversal.js" -import { arkKind } from "../shared/utils.js" -import type { intersectConstraintKinds } from "./util.js" - -export interface BaseConstraintDeclaration extends RawNodeDeclaration { - kind: ConstraintKind -} - -export abstract class RawConstraint< - /** uses -ignore rather than -expect-error because this is not an error in .d.ts - * @ts-ignore allow instantiation assignment to the base type */ - out d extends BaseConstraintDeclaration = BaseConstraintDeclaration -> extends RawNode { - readonly [arkKind] = "constraint" - abstract readonly impliedBasis: RawSchema | null - readonly impliedSiblings?: RawConstraint[] | null - - intersect( - r: r - ): intersectConstraintKinds { - return intersectNodesRoot(this, r, this.$) as never - } -} - -export type PrimitiveConstraintKind = Exclude - -export abstract class RawPrimitiveConstraint< - d extends BaseConstraintDeclaration -> extends RawConstraint { - abstract traverseAllows: TraverseAllows - abstract readonly compiledCondition: string - abstract readonly compiledNegation: string - - traverseApply: TraverseApply = (data, ctx) => { - if (!this.traverseAllows(data, ctx)) ctx.error(this.errorContext as never) - } - - compile(js: NodeCompiler): void { - js.compilePrimitive(this as never) - } - - get errorContext(): d["errorContext"] { - return { code: this.kind, description: this.description, ...this.inner } - } - - get compiledErrorContext(): string { - return compileErrorContext(this.errorContext!) - } -} diff --git a/ark/schema/constraints/predicate.ts b/ark/schema/constraints/predicate.ts deleted file mode 100644 index ea0ab26eec..0000000000 --- a/ark/schema/constraints/predicate.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { registeredReference } from "@arktype/util" -import type { errorContext } from "../kinds.js" -import type { NodeCompiler } from "../shared/compile.js" -import type { BaseMeta, declareNode } from "../shared/declare.js" -import { implementNode } from "../shared/implement.js" -import type { TraversalContext, TraverseApply } from "../shared/traversal.js" -import type { constrain, of } from "./ast.js" -import { RawConstraint } from "./constraint.js" - -export interface PredicateInner = Predicate> - extends BaseMeta { - readonly predicate: rule -} - -export type PredicateErrorContext = Partial - -export type NormalizedPredicateDef = PredicateInner - -export type PredicateDef = NormalizedPredicateDef | Predicate - -export type PredicateDeclaration = declareNode<{ - kind: "predicate" - def: PredicateDef - normalizedDef: NormalizedPredicateDef - inner: PredicateInner - intersectionIsOpen: true - errorContext: PredicateErrorContext -}> - -export const predicateImplementation = implementNode({ - kind: "predicate", - hasAssociatedError: true, - collapsibleKey: "predicate", - keys: { - predicate: {} - }, - normalize: def => (typeof def === "function" ? { predicate: def } : def), - defaults: { - description: node => - `valid according to ${node.predicate.name || "an anonymous predicate"}` - }, - intersectionIsOpen: true, - intersections: { - // TODO: allow changed order to be the same type - // as long as the narrows in l and r are individually safe to check - // in the order they're specified, checking them in the order - // resulting from this intersection should also be safe. - predicate: () => null - } -}) - -export class PredicateNode extends RawConstraint { - serializedPredicate = registeredReference(this.predicate) - compiledCondition = `${this.serializedPredicate}(data, ctx)` - compiledNegation = `!${this.compiledCondition}` - - impliedBasis = null - - expression = this.serializedPredicate - traverseAllows = this.predicate - - errorContext: errorContext<"predicate"> = { - code: "predicate", - description: this.description - } - - compiledErrorContext = `{ code: "predicate", description: "${this.description}" }` - - traverseApply: TraverseApply = (data, ctx) => { - if (!this.predicate(data, ctx) && !ctx.hasError()) - ctx.error(this.errorContext) - } - - compile(js: NodeCompiler): void { - if (js.traversalKind === "Allows") { - js.return(this.compiledCondition) - return - } - js.if(`${this.compiledNegation} && !ctx.hasError()`, () => - js.line(`ctx.error(${this.compiledErrorContext})`) - ) - } -} - -export type Predicate = ( - data: data, - ctx: TraversalContext -) => boolean - -export type PredicateCast = ( - input: input, - ctx: TraversalContext -) => input is narrowed - -export type inferNarrow = - predicate extends (data: any, ...args: any[]) => data is infer narrowed ? - In extends of ? - constrain, "predicate", any> - : constrain - : constrain diff --git a/ark/schema/constraints/props/index.ts b/ark/schema/constraints/props/index.ts deleted file mode 100644 index f3dfb10528..0000000000 --- a/ark/schema/constraints/props/index.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { - printable, - stringAndSymbolicEntriesOf, - throwParseError -} from "@arktype/util" -import type { Node, SchemaDef } from "../../node.js" -import type { RawSchema } from "../../schema.js" -import type { UnitNode } from "../../schemas/unit.js" -import type { BaseMeta, declareNode } from "../../shared/declare.js" -import { Disjoint } from "../../shared/disjoint.js" -import { implementNode, type SchemaKind } from "../../shared/implement.js" -import { intersectNodes } from "../../shared/intersections.js" -import type { TraverseAllows, TraverseApply } from "../../shared/traversal.js" -import { RawConstraint } from "../constraint.js" - -export type IndexKeyKind = Exclude - -export type IndexKeyNode = Node - -export interface IndexDef extends BaseMeta { - readonly key: SchemaDef - readonly value: SchemaDef -} - -export interface IndexInner extends BaseMeta { - readonly key: IndexKeyNode - readonly value: RawSchema -} - -export type IndexDeclaration = declareNode<{ - kind: "index" - def: IndexDef - normalizedDef: IndexDef - inner: IndexInner - prerequisite: object - intersectionIsOpen: true - childKind: SchemaKind -}> - -export const indexImplementation = implementNode({ - kind: "index", - hasAssociatedError: false, - intersectionIsOpen: true, - keys: { - key: { - child: true, - parse: (def, ctx) => { - const key = ctx.$.schema(def) - if (!key.extends(ctx.$.keywords.propertyKey)) - return throwParseError(writeInvalidPropertyKeyMessage(key.expression)) - // TODO: explicit manual annotation once we can upgrade to 5.5 - const enumerableBranches = key.branches.filter((b): b is UnitNode => - b.hasKind("unit") - ) - if (enumerableBranches.length) { - return throwParseError( - writeEnumerableIndexBranches( - enumerableBranches.map(b => printable(b.unit)) - ) - ) - } - return key as IndexKeyNode - } - }, - value: { - child: true, - parse: (def, ctx) => ctx.$.schema(def) - } - }, - normalize: def => def, - defaults: { - description: node => `[${node.key.expression}]: ${node.value.description}` - }, - intersections: { - index: (l, r, ctx) => { - if (l.key.equals(r.key)) { - const valueIntersection = intersectNodes(l.value, r.value, ctx) - const value = - valueIntersection instanceof Disjoint ? - ctx.$.keywords.never.raw - : valueIntersection - return ctx.$.node("index", { key: l.key, value }) - } - - // if r constrains all of l's keys to a subtype of l's value, r is a subtype of l - if (l.key.extends(r.key) && l.value.subsumes(r.value)) return r - // if l constrains all of r's keys to a subtype of r's value, l is a subtype of r - if (r.key.extends(l.key) && r.value.subsumes(l.value)) return l - - // other relationships between index signatures can't be generally reduced - return null - } - } -}) - -export class IndexNode extends RawConstraint { - impliedBasis = this.$.keywords.object.raw - expression = `[${this.key.expression}]: ${this.value.expression}` - - traverseAllows: TraverseAllows = (data, ctx) => - stringAndSymbolicEntriesOf(data).every(entry => { - if (this.key.traverseAllows(entry[0], ctx)) { - // ctx will be undefined if this node isn't context-dependent - ctx?.path.push(entry[0]) - const allowed = this.value.traverseAllows(entry[1], ctx) - ctx?.path.pop() - return allowed - } - return true - }) - - traverseApply: TraverseApply = (data, ctx) => - stringAndSymbolicEntriesOf(data).forEach(entry => { - if (this.key.traverseAllows(entry[0], ctx)) { - ctx.path.push(entry[0]) - this.value.traverseApply(entry[1], ctx) - ctx.path.pop() - } - }) - - compile(): void { - // this is currently handled by the props group - } -} - -export const writeEnumerableIndexBranches = (keys: string[]): string => - `Index keys ${keys.join(", ")} should be specified as named props.` - -export const writeInvalidPropertyKeyMessage = ( - indexDef: indexDef -): writeInvalidPropertyKeyMessage => - `Indexed key definition '${indexDef}' must be a string, number or symbol` - -export type writeInvalidPropertyKeyMessage = - `Indexed key definition '${indexDef}' must be a string, number or symbol` diff --git a/ark/schema/constraints/props/prop.ts b/ark/schema/constraints/props/prop.ts deleted file mode 100644 index fca8b6c41d..0000000000 --- a/ark/schema/constraints/props/prop.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { compileSerializedValue, type Key } from "@arktype/util" -import type { SchemaDef } from "../../node.js" -import type { RawSchema } from "../../schema.js" -import type { NodeCompiler } from "../../shared/compile.js" -import type { - BaseErrorContext, - BaseMeta, - declareNode -} from "../../shared/declare.js" -import { Disjoint } from "../../shared/disjoint.js" -import { - compileErrorContext, - implementNode, - type SchemaKind -} from "../../shared/implement.js" -import { intersectNodes } from "../../shared/intersections.js" -import type { TraverseAllows, TraverseApply } from "../../shared/traversal.js" -import { RawConstraint } from "../constraint.js" - -export interface PropDef extends BaseMeta { - readonly key: Key - readonly value: SchemaDef - readonly optional?: boolean -} - -export interface PropInner extends BaseMeta { - readonly key: Key - readonly value: RawSchema - readonly optional?: true -} - -export type PropDeclaration = declareNode<{ - kind: "prop" - def: PropDef - normalizedDef: PropDef - inner: PropInner - errorContext: PropErrorContext - prerequisite: object - intersectionIsOpen: true - childKind: SchemaKind -}> - -export interface PropErrorContext extends BaseErrorContext<"prop"> { - missingValueDescription: string -} - -export const propImplementation = implementNode({ - kind: "prop", - hasAssociatedError: true, - intersectionIsOpen: true, - keys: { - key: {}, - value: { - child: true, - parse: (def, ctx) => ctx.$.schema(def) - }, - optional: { - // normalize { optional: false } to {} - parse: def => def || undefined - } - }, - normalize: def => def, - defaults: { - description: node => - `${node.compiledKey}${node.optional ? "?" : ""}: ${ - node.value.description - }`, - expected: ctx => ctx.missingValueDescription, - actual: () => "missing" - }, - intersections: { - prop: (l, r, ctx) => { - if (l.key !== r.key) return null - - const key = l.key - let value = intersectNodes(l.value, r.value, ctx) - const optional = l.optional === true && r.optional === true - if (value instanceof Disjoint) { - if (optional) value = ctx.$.keywords.never.raw - else return value.withPrefixKey(l.compiledKey) - } - return ctx.$.node("prop", { - key, - value, - optional - }) - } - } -}) - -export class PropNode extends RawConstraint { - required = !this.optional - impliedBasis = this.$.keywords.object.raw - serializedKey = compileSerializedValue(this.key) - compiledKey = typeof this.key === "string" ? this.key : this.serializedKey - expression = `${this.compiledKey}${this.optional ? "?" : ""}: ${ - this.value.expression - }` - - errorContext = Object.freeze({ - code: "prop", - missingValueDescription: this.value.description - } satisfies PropErrorContext) - - compiledErrorContext: string = compileErrorContext(this.errorContext) - - traverseAllows: TraverseAllows = (data, ctx) => { - if (this.key in data) { - // ctx will be undefined if this node isn't context-dependent - ctx?.path.push(this.key) - const allowed = this.value.traverseAllows((data as any)[this.key], ctx) - ctx?.path.pop() - return allowed - } - return !this.required - } - - traverseApply: TraverseApply = (data, ctx) => { - ctx.path.push(this.key) - if (this.key in data) this.value.traverseApply((data as any)[this.key], ctx) - else if (this.required) ctx.error(this.errorContext) - ctx.path.pop() - } - - compile(js: NodeCompiler): void { - const requiresContext = js.requiresContextFor(this.value) - if (requiresContext) js.line(`ctx.path.push(${this.serializedKey})`) - - js.if(`${this.serializedKey} in ${js.data}`, () => - js.check(this.value, { - arg: `${js.data}${js.prop(this.key)}` - }) - ) - if (this.required) { - js.else(() => { - if (js.traversalKind === "Apply") - return js.line(`ctx.error(${this.compiledErrorContext})`) - else { - if (requiresContext) js.line(`ctx.path.pop()`) - - return js.return(false) - } - }) - } - - if (requiresContext) js.line(`ctx.path.pop()`) - if (js.traversalKind === "Allows") js.return(true) - } -} diff --git a/ark/schema/constraints/props/props.ts b/ark/schema/constraints/props/props.ts deleted file mode 100644 index 03eb57b688..0000000000 --- a/ark/schema/constraints/props/props.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { - DynamicBase, - type Key, - conflatenateAll, - flatMorph, - registeredReference -} from "@arktype/util" -import type { Node } from "../../node.js" -import type { RawSchema } from "../../schema.js" -import type { IntersectionNode } from "../../schemas/intersection.js" -import type { RawSchemaScope } from "../../scope.js" -import type { NodeCompiler } from "../../shared/compile.js" -import type { PropKind } from "../../shared/implement.js" -import type { TraverseAllows, TraverseApply } from "../../shared/traversal.js" -import { arrayIndexMatcherReference } from "./shared.js" - -export type ExtraneousKeyBehavior = "ignore" | ExtraneousKeyRestriction - -export type ExtraneousKeyRestriction = "error" | "prune" - -export type PropsGroupInput = Pick< - IntersectionNode, - PropKind | "onExtraneousKey" -> - -export class PropsGroup extends DynamicBase { - readonly requiredLiteralKeys: Key[] = [] - readonly optionalLiteralKeys: Key[] = [] - readonly literalKeys: Key[] - - readonly all = conflatenateAll>( - this.prop, - this.index, - this.sequence - ) - - constructor( - public inner: PropsGroupInput, - public $: RawSchemaScope - ) { - super(inner) - this.all.forEach(node => { - if (node.kind === "index") return - if (node.kind === "prop") { - if (node.required) this.requiredLiteralKeys.push(node.key) - else this.optionalLiteralKeys.push(node.key) - } else { - node.prevariadic.forEach((_, i) => { - if (i < node.minLength) this.requiredLiteralKeys.push(`${i}`) - else this.optionalLiteralKeys.push(`${i}`) - }) - } - }) - this.literalKeys = [ - ...this.requiredLiteralKeys, - ...this.optionalLiteralKeys - ] - } - - readonly nameSet = - this.prop ? flatMorph(this.prop, (i, node) => [node.key, 1] as const) : {} - readonly nameSetReference = registeredReference(this.nameSet) - readonly description = describeProps(this, "description") - readonly expression = describeProps(this, "expression") - - private keyofCache: RawSchema | undefined - keyof(): RawSchema { - if (!this.keyofCache) { - let branches = this.$.units(this.literalKeys).branches - this.index?.forEach(({ key }) => { - branches = branches.concat(key.branches) - }) - this.keyofCache = this.$.node("union", branches) - } - return this.keyofCache - } - - traverseAllows: TraverseAllows = (data, ctx) => - this.all.every(prop => prop.traverseAllows(data as never, ctx)) - - traverseApply: TraverseApply = (data, ctx) => - this.all.forEach(prop => prop.traverseApply(data as never, ctx)) - - readonly exhaustive = - this.onExtraneousKey !== undefined || this.index !== undefined - - compile(js: NodeCompiler): void { - if (this.exhaustive) this.compileExhaustive(js) - else this.compileEnumerable(js) - } - - protected compileEnumerable(js: NodeCompiler): void { - if (js.traversalKind === "Allows") { - this.all.forEach(node => - js.if(`!${js.invoke(node)}`, () => js.return(false)) - ) - } else this.all.forEach(node => js.line(js.invoke(node))) - } - - protected compileExhaustive(js: NodeCompiler): void { - this.prop?.forEach(prop => js.check(prop)) - this.sequence?.compile(js) - if (this.sequence) js.check(this.sequence) - Object.getOwnPropertySymbols - js.const("keys", "Object.keys(data)") - js.const("symbols", "Object.getOwnPropertySymbols(data)") - js.if("symbols.length", () => js.line("keys.push(...symbols)")) - js.for("i < keys.length", () => this.compileExhaustiveEntry(js)) - } - - protected compileExhaustiveEntry(js: NodeCompiler): NodeCompiler { - js.const("k", "keys[i]") - - if (this.onExtraneousKey) js.let("matched", false) - - this.index?.forEach(node => { - js.if(`${js.invoke(node.key, { arg: "k", kind: "Allows" })}`, () => { - js.checkReferenceKey("k", node.value) - if (this.onExtraneousKey) js.set("matched", true) - return js - }) - }) - - if (this.onExtraneousKey) { - if (this.prop?.length !== 0) - js.line(`matched ||= k in ${this.nameSetReference}`) - - if (this.sequence) - js.line(`matched ||= ${arrayIndexMatcherReference}.test(k)`) - - // // TODO: replace error - // js.if("!matched", () => js.line(`throw new Error("strict")`)) - } - - return js - } -} - -const describeProps = ( - inner: PropsGroupInput, - childStringProp: "expression" | "description" -) => { - if (inner.prop || inner.index) { - const parts = inner.index?.map(String) ?? [] - inner.prop?.forEach(node => parts.push(node[childStringProp])) - const objectLiteralDescription = `${ - inner.onExtraneousKey ? "exact " : "" - }{ ${parts.join(", ")} }` - return inner.sequence ? - `${objectLiteralDescription} & ${inner.sequence.description}` - : objectLiteralDescription - } - return inner.sequence?.description ?? "{}" -} diff --git a/ark/schema/constraints/props/sequence.ts b/ark/schema/constraints/props/sequence.ts deleted file mode 100644 index 9e84fb40a8..0000000000 --- a/ark/schema/constraints/props/sequence.ts +++ /dev/null @@ -1,445 +0,0 @@ -import { - append, - type array, - type mutable, - type satisfy, - throwInternalError, - throwParseError -} from "@arktype/util" -import type { MutableInner } from "../../kinds.js" -import type { SchemaDef } from "../../node.js" -import type { RawSchema } from "../../schema.js" -import type { NodeCompiler } from "../../shared/compile.js" -import type { BaseMeta, declareNode } from "../../shared/declare.js" -import { Disjoint } from "../../shared/disjoint.js" -import { - implementNode, - type IntersectionContext, - type NodeKeyImplementation, - type SchemaKind -} from "../../shared/implement.js" -import { intersectNodes } from "../../shared/intersections.js" -import type { TraverseAllows, TraverseApply } from "../../shared/traversal.js" -import { RawConstraint } from "../constraint.js" - -export interface NormalizedSequenceDef extends BaseMeta { - readonly prefix?: array - readonly optional?: array - readonly variadic?: SchemaDef - readonly minVariadicLength?: number - readonly postfix?: array -} - -export type SequenceDef = NormalizedSequenceDef | SchemaDef - -export interface SequenceInner extends BaseMeta { - // a list of fixed position elements starting at index 0 - readonly prefix?: array - // a list of optional elements following prefix - readonly optional?: array - // the variadic element (only checked if all optional elements are present) - readonly variadic?: RawSchema - readonly minVariadicLength?: number - // a list of fixed position elements, the last being the last element of the array - readonly postfix?: array -} - -export type SequenceDeclaration = declareNode<{ - kind: "sequence" - def: SequenceDef - normalizedDef: NormalizedSequenceDef - inner: SequenceInner - prerequisite: array - reducibleTo: "sequence" - childKind: SchemaKind -}> - -const fixedSequenceKeyDefinition: NodeKeyImplementation< - SequenceDeclaration, - "prefix" | "postfix" | "optional" -> = { - child: true, - parse: (def, ctx) => - def.length === 0 ? - // empty affixes are omitted. an empty array should therefore - // be specified as `{ proto: Array, length: 0 }` - undefined - : def.map(element => ctx.$.schema(element)) -} - -export const sequenceImplementation = implementNode({ - kind: "sequence", - hasAssociatedError: false, - collapsibleKey: "variadic", - keys: { - prefix: fixedSequenceKeyDefinition, - optional: fixedSequenceKeyDefinition, - variadic: { - child: true, - parse: (def, ctx) => ctx.$.schema(def, ctx) - }, - minVariadicLength: { - // minVariadicLength is reflected in the id of this node, - // but not its IntersectionNode parent since it is superceded by the minLength - // node it implies - implied: true, - parse: min => (min === 0 ? undefined : min) - }, - postfix: fixedSequenceKeyDefinition - }, - normalize: def => { - if (typeof def === "string") return { variadic: def } - - if ( - "variadic" in def || - "prefix" in def || - "optional" in def || - "postfix" in def || - "minVariadicLength" in def - ) { - if (def.postfix?.length) { - if (!def.variadic) return throwParseError(postfixWithoutVariadicMessage) - - if (def.optional?.length) - return throwParseError(postfixFollowingOptionalMessage) - } - if (def.minVariadicLength && !def.variadic) { - return throwParseError( - "minVariadicLength may not be specified without a variadic element" - ) - } - return def - } - return { variadic: def } - }, - reduce: (raw, $) => { - let minVariadicLength = raw.minVariadicLength ?? 0 - const prefix = raw.prefix?.slice() ?? [] - const optional = raw.optional?.slice() ?? [] - const postfix = raw.postfix?.slice() ?? [] - if (raw.variadic) { - // optional elements equivalent to the variadic parameter are redundant - while (optional.at(-1)?.equals(raw.variadic)) optional.pop() - - if (optional.length === 0) { - // If there are no optional, normalize prefix - // elements adjacent and equivalent to variadic: - // { variadic: number, prefix: [string, number] } - // reduces to: - // { variadic: number, prefix: [string], minVariadicLength: 1 } - while (prefix.at(-1)?.equals(raw.variadic)) { - prefix.pop() - minVariadicLength++ - } - } - // Normalize postfix elements adjacent and equivalent to variadic: - // { variadic: number, postfix: [number, number, 5] } - // reduces to: - // { variadic: number, postfix: [5], minVariadicLength: 2 } - while (postfix[0]?.equals(raw.variadic)) { - postfix.shift() - minVariadicLength++ - } - } else if (optional.length === 0) { - // if there's no variadic or optional parameters, - // postfix can just be appended to prefix - prefix.push(...postfix.splice(0)) - } - if ( - // if any variadic adjacent elements were moved to minVariadicLength - minVariadicLength !== raw.minVariadicLength || - // or any postfix elements were moved to prefix - (raw.prefix && raw.prefix.length !== prefix.length) - ) { - // reparse the reduced def - return $.node( - "sequence", - { - ...raw, - // empty lists will be omitted during parsing - prefix, - postfix, - optional, - minVariadicLength - }, - { prereduced: true } - ) - } - }, - defaults: { - description: node => { - if (node.isVariadicOnly) return `${node.variadic!.nestableExpression}[]` - const innerDescription = node.tuple - .map(element => - element.kind === "optional" ? `${element.node.nestableExpression}?` - : element.kind === "variadic" ? - `...${element.node.nestableExpression}[]` - : element.node.expression - ) - .join(", ") - return `[${innerDescription}]` - } - }, - intersections: { - sequence: (l, r, ctx) => { - const rootState = intersectSequences({ - l: l.tuple, - r: r.tuple, - disjoint: new Disjoint({}), - result: [], - fixedVariants: [], - ctx - }) - - const viableBranches = - rootState.disjoint.isEmpty() ? - [rootState, ...rootState.fixedVariants] - : rootState.fixedVariants - - return ( - viableBranches.length === 0 ? rootState.disjoint! - : viableBranches.length === 1 ? - ctx.$.node("sequence", sequenceTupleToInner(viableBranches[0].result)) - : ctx.$.node( - "union", - viableBranches.map(state => ({ - proto: Array, - sequence: sequenceTupleToInner(state.result) - })) - ) - ) - } - // exactLength, minLength, and maxLength don't need to be defined - // here since impliedSiblings guarantees they will be added - // directly to the IntersectionNode parent of the SequenceNode - // they exist on - } -}) - -export class SequenceNode extends RawConstraint { - impliedBasis = this.$.keywords.Array.raw - prefix = this.inner.prefix ?? [] - optional = this.inner.optional ?? [] - prevariadic = [...this.prefix, ...this.optional] - postfix = this.inner.postfix ?? [] - isVariadicOnly = this.prevariadic.length + this.postfix.length === 0 - minVariadicLength = this.inner.minVariadicLength ?? 0 - minLength = this.prefix.length + this.minVariadicLength + this.postfix.length - minLengthNode = - this.minLength === 0 ? null : this.$.node("minLength", this.minLength) - maxLength = this.variadic ? null : this.minLength + this.optional.length - maxLengthNode = - this.maxLength === null ? null : this.$.node("maxLength", this.maxLength) - impliedSiblings = - this.minLengthNode ? - this.maxLengthNode ? - [this.minLengthNode, this.maxLengthNode] - : [this.minLengthNode] - : this.maxLengthNode ? [this.maxLengthNode] - : null - - protected childAtIndex(data: array, index: number): RawSchema { - if (index < this.prevariadic.length) return this.prevariadic[index] - const postfixStartIndex = data.length - this.postfix.length - if (index >= postfixStartIndex) - return this.postfix[index - postfixStartIndex] - return ( - this.variadic ?? - throwInternalError( - `Unexpected attempt to access index ${index} on ${this}` - ) - ) - } - - // minLength/maxLength should be checked by Intersection before either traversal - traverseAllows: TraverseAllows = (data, ctx) => { - for (let i = 0; i < data.length; i++) - if (!this.childAtIndex(data, i).traverseAllows(data[i], ctx)) return false - - return true - } - - traverseApply: TraverseApply = (data, ctx) => { - for (let i = 0; i < data.length; i++) { - ctx.path.push(i) - this.childAtIndex(data, i).traverseApply(data[i], ctx) - ctx.path.pop() - } - } - - // minLength/maxLength compilation should be handled by Intersection - compile(js: NodeCompiler): void { - this.prefix.forEach((node, i) => js.checkReferenceKey(`${i}`, node)) - this.optional.forEach((node, i) => { - const dataIndex = `${i + this.prefix.length}` - js.if(`${dataIndex} >= ${js.data}.length`, () => - js.traversalKind === "Allows" ? js.return(true) : js.return() - ) - js.checkReferenceKey(dataIndex, node) - }) - - if (this.variadic) { - js.const( - "lastVariadicIndex", - `${js.data}.length${this.postfix ? `- ${this.postfix.length}` : ""}` - ) - js.for( - "i < lastVariadicIndex", - () => js.checkReferenceKey("i", this.variadic!), - this.prevariadic.length - ) - this.postfix.forEach((node, i) => - js.checkReferenceKey(`lastVariadicIndex + ${i + 1}`, node) - ) - } - - if (js.traversalKind === "Allows") js.return(true) - } - - tuple = sequenceInnerToTuple(this.inner) - // this depends on tuple so needs to come after it - expression = this.description -} - -const sequenceInnerToTuple = (inner: SequenceInner): SequenceTuple => { - const tuple: mutable = [] - inner.prefix?.forEach(node => tuple.push({ kind: "prefix", node })) - inner.optional?.forEach(node => tuple.push({ kind: "optional", node })) - if (inner.variadic) tuple.push({ kind: "variadic", node: inner.variadic }) - inner.postfix?.forEach(node => tuple.push({ kind: "postfix", node })) - return tuple -} - -const sequenceTupleToInner = (tuple: SequenceTuple): SequenceInner => - tuple.reduce>((result, node) => { - if (node.kind === "variadic") result.variadic = node.node - else result[node.kind] = append(result[node.kind], node.node) - - return result - }, {}) - -export const postfixFollowingOptionalMessage = - "A postfix required element cannot follow an optional element" - -export type postfixFollowingOptionalMessage = - typeof postfixFollowingOptionalMessage - -export const postfixWithoutVariadicMessage = - "A postfix element requires a variadic element" - -export type postfixWithoutVariadicMessage = typeof postfixWithoutVariadicMessage - -export type SequenceElementKind = satisfy< - keyof SequenceInner, - "prefix" | "optional" | "variadic" | "postfix" -> - -export type SequenceElement = { - kind: SequenceElementKind - node: RawSchema -} -export type SequenceTuple = array - -type SequenceIntersectionState = { - l: SequenceTuple - r: SequenceTuple - disjoint: Disjoint - result: SequenceTuple - fixedVariants: SequenceIntersectionState[] - ctx: IntersectionContext -} - -const intersectSequences = ( - s: SequenceIntersectionState -): SequenceIntersectionState => { - const [lHead, ...lTail] = s.l - const [rHead, ...rTail] = s.r - - if (!lHead || !rHead) return s - - const lHasPostfix = lTail.at(-1)?.kind === "postfix" - const rHasPostfix = rTail.at(-1)?.kind === "postfix" - - const kind: SequenceElementKind = - lHead.kind === "prefix" || rHead.kind === "prefix" ? "prefix" - : lHead.kind === "optional" || rHead.kind === "optional" ? - // if either operand has postfix elements, the full-length - // intersection can't include optional elements (though they may - // exist in some of the fixed length variants) - lHasPostfix || rHasPostfix ? - "prefix" - : "optional" - : lHead.kind === "postfix" || rHead.kind === "postfix" ? "postfix" - : "variadic" - - if (lHead.kind === "prefix" && rHead.kind === "variadic" && rHasPostfix) { - const postfixBranchResult = intersectSequences({ - ...s, - fixedVariants: [], - r: rTail.map(element => ({ ...element, kind: "prefix" })) - }) - if (postfixBranchResult.disjoint.isEmpty()) - s.fixedVariants.push(postfixBranchResult) - } else if ( - rHead.kind === "prefix" && - lHead.kind === "variadic" && - lHasPostfix - ) { - const postfixBranchResult = intersectSequences({ - ...s, - fixedVariants: [], - l: lTail.map(element => ({ ...element, kind: "prefix" })) - }) - if (postfixBranchResult.disjoint.isEmpty()) - s.fixedVariants.push(postfixBranchResult) - } - - const result = intersectNodes(lHead.node, rHead.node, s.ctx) - if (result instanceof Disjoint) { - if (kind === "prefix" || kind === "postfix") { - s.disjoint.add( - result.withPrefixKey( - // TODO: more precise path handling for Disjoints - kind === "prefix" ? `${s.result.length}` : `-${lTail.length + 1}` - ) - ) - s.result = [...s.result, { kind, node: s.ctx.$.keywords.never.raw }] - } else if (kind === "optional") { - // if the element result is optional and unsatisfiable, the - // intersection can still be satisfied as long as the tuple - // ends before the disjoint element would occur - return s - } else { - // if the element is variadic and unsatisfiable, the intersection - // can be satisfied with a fixed length variant including zero - // variadic elements - return intersectSequences({ - ...s, - fixedVariants: [], - // if there were any optional elements, there will be no postfix elements - // so this mapping will never occur (which would be illegal otherwise) - l: lTail.map(element => ({ ...element, kind: "prefix" })), - r: lTail.map(element => ({ ...element, kind: "prefix" })) - }) - } - } else s.result = [...s.result, { kind, node: result }] - - const lRemaining = s.l.length - const rRemaining = s.r.length - - if ( - lHead.kind !== "variadic" || - (lRemaining >= rRemaining && - (rHead.kind === "variadic" || rRemaining === 1)) - ) - s.l = lTail - - if ( - rHead.kind !== "variadic" || - (rRemaining >= lRemaining && - (lHead.kind === "variadic" || lRemaining === 1)) - ) - s.r = rTail - - return intersectSequences(s) -} diff --git a/ark/schema/constraints/props/shared.ts b/ark/schema/constraints/props/shared.ts deleted file mode 100644 index 4daa39c93f..0000000000 --- a/ark/schema/constraints/props/shared.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { registeredReference } from "@arktype/util" - -export const arrayIndexMatcher = /(?:0|(?:[1-9]\\d*))$/ - -export const arrayIndexMatcherReference = registeredReference(arrayIndexMatcher) diff --git a/ark/schema/constraints/refinements/after.ts b/ark/schema/constraints/refinements/after.ts deleted file mode 100644 index 50e58d25fd..0000000000 --- a/ark/schema/constraints/refinements/after.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { declareNode } from "../../shared/declare.js" -import { implementNode } from "../../shared/implement.js" -import type { TraverseAllows } from "../../shared/traversal.js" -import { - type BaseNormalizedRangeSchema, - BaseRange, - type BaseRangeInner, - type LimitSchemaValue, - parseDateLimit, - parseExclusiveKey -} from "./range.js" - -export interface AfterInner extends BaseRangeInner { - rule: Date -} - -export interface NormalizedAfterDef extends BaseNormalizedRangeSchema { - rule: LimitSchemaValue -} - -export type AfterDef = NormalizedAfterDef | LimitSchemaValue - -export type AfterDeclaration = declareNode<{ - kind: "after" - def: AfterDef - normalizedDef: NormalizedAfterDef - inner: AfterInner - prerequisite: Date - errorContext: AfterInner -}> - -export const afterImplementation = implementNode({ - kind: "after", - collapsibleKey: "rule", - hasAssociatedError: true, - keys: { - rule: { - parse: parseDateLimit, - serialize: def => def.toISOString() - }, - exclusive: parseExclusiveKey - }, - normalize: def => - typeof def === "number" || typeof def === "string" || def instanceof Date ? - { rule: def } - : def, - defaults: { - description: node => - node.exclusive ? - `after ${node.stringLimit}` - : `${node.stringLimit} or later`, - actual: data => data.toLocaleString() - }, - intersections: { - after: (l, r) => (l.isStricterThan(r) ? l : r) - } -}) - -export class AfterNode extends BaseRange { - traverseAllows: TraverseAllows = - this.exclusive ? data => data > this.rule : data => data >= this.rule - - impliedBasis = this.$.keywords.Date.raw -} diff --git a/ark/schema/constraints/refinements/before.ts b/ark/schema/constraints/refinements/before.ts deleted file mode 100644 index 63b438d0d1..0000000000 --- a/ark/schema/constraints/refinements/before.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { declareNode } from "../../shared/declare.js" -import { Disjoint } from "../../shared/disjoint.js" -import { implementNode } from "../../shared/implement.js" -import type { TraverseAllows } from "../../shared/traversal.js" -import { - type BaseNormalizedRangeSchema, - BaseRange, - type BaseRangeInner, - type LimitSchemaValue, - parseDateLimit, - parseExclusiveKey -} from "./range.js" - -export interface BeforeInner extends BaseRangeInner { - rule: Date -} - -export interface NormalizedBeforeDef extends BaseNormalizedRangeSchema { - rule: LimitSchemaValue -} - -export type BeforeDef = NormalizedBeforeDef | LimitSchemaValue - -export type BeforeDeclaration = declareNode<{ - kind: "before" - def: BeforeDef - normalizedDef: NormalizedBeforeDef - inner: BeforeInner - prerequisite: Date - errorContext: BeforeInner -}> - -export const beforeImplementation = implementNode({ - kind: "before", - collapsibleKey: "rule", - hasAssociatedError: true, - keys: { - rule: { - parse: parseDateLimit, - serialize: def => def.toISOString() - }, - exclusive: parseExclusiveKey - }, - normalize: def => - typeof def === "number" || typeof def === "string" || def instanceof Date ? - { rule: def } - : def, - defaults: { - description: node => - node.exclusive ? - `before ${node.stringLimit}` - : `${node.stringLimit} or earlier`, - actual: data => data.toLocaleString() - }, - intersections: { - before: (l, r) => (l.isStricterThan(r) ? l : r), - after: (before, after, ctx) => - before.overlapsRange(after) ? - before.overlapIsUnit(after) ? - ctx.$.node("unit", { unit: before.rule }) - : null - : Disjoint.from("range", before, after) - } -}) - -export class BeforeNode extends BaseRange { - traverseAllows: TraverseAllows = - this.exclusive ? data => data < this.rule : data => data <= this.rule - - impliedBasis = this.$.keywords.Date.raw -} diff --git a/ark/schema/constraints/refinements/divisor.ts b/ark/schema/constraints/refinements/divisor.ts deleted file mode 100644 index 673a18d8c0..0000000000 --- a/ark/schema/constraints/refinements/divisor.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { Schema } from "../../schema.js" -import type { BaseMeta, declareNode } from "../../shared/declare.js" -import { implementNode } from "../../shared/implement.js" -import type { TraverseAllows } from "../../shared/traversal.js" -import { RawPrimitiveConstraint } from "../constraint.js" -import { writeInvalidOperandMessage } from "../util.js" - -export interface DivisorInner extends BaseMeta { - readonly rule: number -} - -export type DivisorDef = DivisorInner | number - -export type DivisorDeclaration = declareNode<{ - kind: "divisor" - def: DivisorDef - normalizedDef: DivisorInner - inner: DivisorInner - prerequisite: number - errorContext: DivisorInner -}> - -export const divisorImplementation = implementNode({ - kind: "divisor", - collapsibleKey: "rule", - keys: { - rule: {} - }, - normalize: def => (typeof def === "number" ? { rule: def } : def), - intersections: { - divisor: (l, r, ctx) => - ctx.$.node("divisor", { - rule: Math.abs( - (l.rule * r.rule) / greatestCommonDivisor(l.rule, r.rule) - ) - }) - }, - hasAssociatedError: true, - defaults: { - description: node => - node.rule === 1 ? "an integer" : `a multiple of ${node.rule}` - } -}) - -export class DivisorNode extends RawPrimitiveConstraint { - traverseAllows: TraverseAllows = data => data % this.rule === 0 - - readonly compiledCondition = `data % ${this.rule} === 0` - readonly compiledNegation = `data % ${this.rule} !== 0` - readonly impliedBasis = this.$.keywords.number.raw - readonly expression = `% ${this.rule}` -} - -export const writeIndivisibleMessage = ( - t: node -): writeIndivisibleMessage => - writeInvalidOperandMessage("divisor", t.$.raw.keywords.number, t) - -export type writeIndivisibleMessage = - writeInvalidOperandMessage<"divisor", node> - -// https://en.wikipedia.org/wiki/Euclidean_algorithm -const greatestCommonDivisor = (l: number, r: number) => { - let previous: number - let greatestCommonDivisor = l - let current = r - while (current !== 0) { - previous = current - current = greatestCommonDivisor % current - greatestCommonDivisor = previous - } - return greatestCommonDivisor -} diff --git a/ark/schema/constraints/refinements/exactLength.ts b/ark/schema/constraints/refinements/exactLength.ts deleted file mode 100644 index 8c262a7e1d..0000000000 --- a/ark/schema/constraints/refinements/exactLength.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { BaseMeta, declareNode } from "../../shared/declare.js" -import { Disjoint } from "../../shared/disjoint.js" -import { implementNode } from "../../shared/implement.js" -import type { TraverseAllows } from "../../shared/traversal.js" -import { RawPrimitiveConstraint } from "../constraint.js" -import type { LengthBoundableData } from "./range.js" - -export interface ExactLengthInner extends BaseMeta { - readonly rule: number -} - -export type NormalizedExactLengthDef = ExactLengthInner - -export type ExactLengthDef = NormalizedExactLengthDef | number - -export type ExactLengthDeclaration = declareNode<{ - kind: "exactLength" - def: ExactLengthDef - normalizedDef: NormalizedExactLengthDef - inner: ExactLengthInner - prerequisite: LengthBoundableData - errorContext: ExactLengthInner -}> - -export const exactLengthImplementation = implementNode({ - kind: "exactLength", - collapsibleKey: "rule", - keys: { - rule: {} - }, - normalize: def => (typeof def === "number" ? { rule: def } : def), - intersections: { - exactLength: (l, r, ctx) => - new Disjoint({ - "[length]": { - unit: { - l: ctx.$.node("unit", { unit: l.rule }), - r: ctx.$.node("unit", { unit: r.rule }) - } - } - }), - minLength: (exactLength, minLength) => - ( - minLength.exclusive ? - exactLength.rule > minLength.rule - : exactLength.rule >= minLength.rule - ) ? - exactLength - : Disjoint.from("range", exactLength, minLength), - maxLength: (exactLength, maxLength) => - ( - maxLength.exclusive ? - exactLength.rule < maxLength.rule - : exactLength.rule <= maxLength.rule - ) ? - exactLength - : Disjoint.from("range", exactLength, maxLength) - }, - hasAssociatedError: true, - defaults: { - description: node => `exactly length ${node.rule}` - } -}) - -export class ExactLengthNode extends RawPrimitiveConstraint { - traverseAllows: TraverseAllows = data => - data.length === this.rule - - readonly compiledCondition = `data.length === ${this.rule}` - readonly compiledNegation = `data.length !== ${this.rule}` - readonly impliedBasis = this.$.keywords.lengthBoundable.raw - readonly expression = `{ length: ${this.rule} }` -} diff --git a/ark/schema/constraints/refinements/max.ts b/ark/schema/constraints/refinements/max.ts deleted file mode 100644 index ffcec9b54a..0000000000 --- a/ark/schema/constraints/refinements/max.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { declareNode } from "../../shared/declare.js" -import { Disjoint } from "../../shared/disjoint.js" -import { implementNode } from "../../shared/implement.js" -import type { TraverseAllows } from "../../shared/traversal.js" -import { - type BaseNormalizedRangeSchema, - BaseRange, - type BaseRangeInner, - parseExclusiveKey -} from "./range.js" - -export interface MaxInner extends BaseRangeInner { - rule: number -} - -export interface NormalizedMaxDef extends BaseNormalizedRangeSchema { - rule: number -} - -export type MaxDef = NormalizedMaxDef | number - -export type MaxDeclaration = declareNode<{ - kind: "max" - def: MaxDef - normalizedDef: NormalizedMaxDef - inner: MaxInner - prerequisite: number - errorContext: MaxInner -}> - -export const maxImplementation = implementNode({ - kind: "max", - collapsibleKey: "rule", - hasAssociatedError: true, - keys: { - rule: {}, - exclusive: parseExclusiveKey - }, - normalize: def => (typeof def === "number" ? { rule: def } : def), - defaults: { - description: node => - `${node.exclusive ? "less than" : "at most"} ${node.rule}` - }, - intersections: { - max: (l, r) => (l.isStricterThan(r) ? l : r), - min: (max, min, ctx) => - max.overlapsRange(min) ? - max.overlapIsUnit(min) ? - ctx.$.node("unit", { unit: max.rule }) - : null - : Disjoint.from("range", max, min) - } -}) - -export class MaxNode extends BaseRange { - impliedBasis = this.$.keywords.number.raw - - traverseAllows: TraverseAllows = - this.exclusive ? data => data < this.rule : data => data <= this.rule -} diff --git a/ark/schema/constraints/refinements/maxLength.ts b/ark/schema/constraints/refinements/maxLength.ts deleted file mode 100644 index b4cc6794f5..0000000000 --- a/ark/schema/constraints/refinements/maxLength.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { declareNode } from "../../shared/declare.js" -import { Disjoint } from "../../shared/disjoint.js" -import { implementNode } from "../../shared/implement.js" -import type { TraverseAllows } from "../../shared/traversal.js" -import { - type BaseNormalizedRangeSchema, - BaseRange, - type BaseRangeInner, - type LengthBoundableData, - parseExclusiveKey -} from "./range.js" - -export interface MaxLengthInner extends BaseRangeInner { - rule: number -} - -export interface NormalizedMaxLengthDef extends BaseNormalizedRangeSchema { - rule: number -} - -export type MaxLengthDef = NormalizedMaxLengthDef | number - -export type MaxLengthDeclaration = declareNode<{ - kind: "maxLength" - def: MaxLengthDef - normalizedDef: NormalizedMaxLengthDef - inner: MaxLengthInner - prerequisite: LengthBoundableData - errorContext: MaxLengthInner -}> - -export const maxLengthImplementation = implementNode({ - kind: "maxLength", - collapsibleKey: "rule", - hasAssociatedError: true, - keys: { - rule: {}, - exclusive: parseExclusiveKey - }, - normalize: def => (typeof def === "number" ? { rule: def } : def), - defaults: { - description: node => - node.exclusive ? - `less than length ${node.rule}` - : `at most length ${node.rule}`, - actual: data => `${data.length}` - }, - intersections: { - maxLength: (l, r) => (l.isStricterThan(r) ? l : r), - minLength: (max, min, ctx) => - max.overlapsRange(min) ? - max.overlapIsUnit(min) ? - ctx.$.node("exactLength", { rule: max.rule }) - : null - : Disjoint.from("range", max, min) - } -}) - -export class MaxLengthNode extends BaseRange { - readonly impliedBasis = this.$.keywords.lengthBoundable.raw - - traverseAllows: TraverseAllows = - this.exclusive ? - data => data.length < this.rule - : data => data.length <= this.rule -} diff --git a/ark/schema/constraints/refinements/min.ts b/ark/schema/constraints/refinements/min.ts deleted file mode 100644 index b26b3462ca..0000000000 --- a/ark/schema/constraints/refinements/min.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { declareNode } from "../../shared/declare.js" -import { implementNode } from "../../shared/implement.js" -import type { TraverseAllows } from "../../shared/traversal.js" -import { - type BaseNormalizedRangeSchema, - BaseRange, - type BaseRangeInner, - parseExclusiveKey -} from "./range.js" - -export interface MinInner extends BaseRangeInner { - rule: number -} - -export interface NormalizedMinSchema extends BaseNormalizedRangeSchema { - rule: number -} - -export type MinSchema = NormalizedMinSchema | number - -export type MinDeclaration = declareNode<{ - kind: "min" - def: MinSchema - normalizedDef: NormalizedMinSchema - inner: MinInner - prerequisite: number - errorContext: MinInner -}> - -export const minImplementation = implementNode({ - kind: "min", - collapsibleKey: "rule", - hasAssociatedError: true, - keys: { - rule: {}, - exclusive: parseExclusiveKey - }, - normalize: def => (typeof def === "number" ? { rule: def } : def), - intersections: { - min: (l, r) => (l.isStricterThan(r) ? l : r) - }, - defaults: { - description: node => - `${node.exclusive ? "more than" : "at least"} ${node.rule}` - } -}) - -export class MinNode extends BaseRange { - readonly impliedBasis = this.$.keywords.number.raw - - traverseAllows: TraverseAllows = - this.exclusive ? data => data > this.rule : data => data >= this.rule -} diff --git a/ark/schema/constraints/refinements/minLength.ts b/ark/schema/constraints/refinements/minLength.ts deleted file mode 100644 index ef6c8c487c..0000000000 --- a/ark/schema/constraints/refinements/minLength.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { declareNode } from "../../shared/declare.js" -import { implementNode } from "../../shared/implement.js" -import type { TraverseAllows } from "../../shared/traversal.js" -import { - type BaseNormalizedRangeSchema, - BaseRange, - type BaseRangeInner, - type LengthBoundableData, - parseExclusiveKey -} from "./range.js" - -export interface MinLengthInner extends BaseRangeInner { - rule: number -} - -export interface NormalizedMinLengthDef extends BaseNormalizedRangeSchema { - rule: number -} - -export type MinLengthDef = NormalizedMinLengthDef | number - -export type MinLengthDeclaration = declareNode<{ - kind: "minLength" - def: MinLengthDef - normalizedDef: NormalizedMinLengthDef - inner: MinLengthInner - prerequisite: LengthBoundableData - errorContext: MinLengthInner -}> - -export const minLengthImplementation = implementNode({ - kind: "minLength", - collapsibleKey: "rule", - hasAssociatedError: true, - keys: { - rule: {}, - exclusive: parseExclusiveKey - }, - normalize: def => (typeof def === "number" ? { rule: def } : def), - defaults: { - description: node => - node.exclusive ? - node.rule === 0 ? - "non-empty" - : `more than length ${node.rule}` - : node.rule === 1 ? "non-empty" - : `at least length ${node.rule}`, - actual: data => `${data.length}` - }, - intersections: { - minLength: (l, r) => (l.isStricterThan(r) ? l : r) - } -}) - -export class MinLengthNode extends BaseRange { - traverseAllows: TraverseAllows = - this.exclusive ? - data => data.length > this.rule - : data => data.length >= this.rule - - readonly impliedBasis = this.$.keywords.lengthBoundable.raw -} diff --git a/ark/schema/constraints/refinements/regex.ts b/ark/schema/constraints/refinements/regex.ts deleted file mode 100644 index a324044dee..0000000000 --- a/ark/schema/constraints/refinements/regex.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { BaseMeta, declareNode } from "../../shared/declare.js" -import { implementNode } from "../../shared/implement.js" -import { RawPrimitiveConstraint } from "../constraint.js" - -export interface RegexInner extends BaseMeta { - readonly rule: string - readonly flags?: string -} - -export type NormalizedRegexDef = RegexInner - -export type RegexDef = NormalizedRegexDef | string | RegExp - -export type RegexDeclaration = declareNode<{ - kind: "regex" - def: RegexDef - normalizedDef: NormalizedRegexDef - inner: RegexInner - intersectionIsOpen: true - prerequisite: string - errorContext: RegexInner -}> - -export const regexImplementation = implementNode({ - kind: "regex", - collapsibleKey: "rule", - keys: { - rule: {}, - flags: {} - }, - normalize: def => - typeof def === "string" ? { rule: def } - : def instanceof RegExp ? - def.flags ? - { rule: def.source, flags: def.flags } - : { rule: def.source } - : def, - hasAssociatedError: true, - intersectionIsOpen: true, - intersections: { - // for now, non-equal regex are naively intersected - regex: () => null - }, - defaults: { - description: node => `matched by ${node.rule}` - } -}) - -export class RegexNode extends RawPrimitiveConstraint { - readonly instance = new RegExp(this.rule, this.flags) - readonly expression = `${this.instance}` - traverseAllows = this.instance.test.bind(this.instance) - - readonly compiledCondition = `${this.expression}.test(data)` - readonly compiledNegation = `!${this.compiledCondition}` - readonly impliedBasis = this.$.keywords.string.raw -} diff --git a/ark/schema/constraints/util.ts b/ark/schema/constraints/util.ts deleted file mode 100644 index 51dbc6100d..0000000000 --- a/ark/schema/constraints/util.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - type array, - capitalize, - type describeExpression, - throwParseError -} from "@arktype/util" -import type { Prerequisite } from "../kinds.js" -import type { Node } from "../node.js" -import type { Schema, UnknownSchema } from "../schema.js" -import type { Disjoint } from "../shared/disjoint.js" -import type { - ConstraintKind, - PropKind, - kindLeftOf -} from "../shared/implement.js" -import type { RawConstraint } from "./constraint.js" - -export type constraintKindLeftOf = ConstraintKind & - kindLeftOf - -export type constraintKindOrLeftOf = - | kind - | constraintKindLeftOf - -export type intersectConstraintKinds< - l extends ConstraintKind, - r extends ConstraintKind -> = Node | Disjoint | null - -export const throwInvalidOperandError = ( - ...args: Parameters -): never => throwParseError(writeInvalidOperandMessage(...args)) - -export const writeInvalidOperandMessage = < - kind extends ConstraintKind, - expected extends Schema, - actual extends Schema ->( - kind: kind, - expected: expected, - actual: actual -): writeInvalidOperandMessage => - `${capitalize(kind)} operand must be ${ - expected.description - } (was ${actual.exclude(expected)})` as never - -export type writeInvalidOperandMessage< - kind extends ConstraintKind, - actual extends Schema -> = `${Capitalize} operand must be ${describeExpression< - Prerequisite ->} (was ${describeExpression})` - -export interface ConstraintAttachments { - impliedBasis: UnknownSchema | null - impliedSiblings?: array | null -} - -export type PrimitiveConstraintKind = Exclude diff --git a/ark/schema/generic.ts b/ark/schema/generic.ts index f8a6136f64..457cf43b33 100644 --- a/ark/schema/generic.ts +++ b/ark/schema/generic.ts @@ -1,8 +1,8 @@ import { Callable, type conform, type repeat } from "@arktype/util" -import type { inferSchema } from "./inference.js" -import type { SchemaDef } from "./node.js" -import type { Schema } from "./schema.js" -import type { SchemaScope } from "./scope.js" +import type { inferRoot } from "./inference.js" +import type { RootSchema } from "./kinds.js" +import type { Root } from "./roots/root.js" +import type { RootScope } from "./scope.js" import { arkKind } from "./shared/utils.js" export type GenericNodeInstantiation< @@ -10,20 +10,20 @@ export type GenericNodeInstantiation< def = unknown, $ = any > = ( - ...args: conform> -) => Schema>> + ...args: conform> +) => Root>> // TODO: ???? export type bindGenericNodeInstantiation = { - [i in keyof params & `${number}` as params[i]]: inferSchema< + [i in keyof params & `${number}` as params[i]]: inferRoot< args[i & keyof args], $ > } export const validateUninstantiatedGenericNode = ( - g: GenericSchema -): GenericSchema => { + g: GenericRoot +): GenericRoot => { g.$.schema(g.def as never, { // // TODO: probably don't need raw once this is fixed. // args: flatMorph(g.params, (_, name) => [name, g.$.raw.keywords.unknown]) @@ -40,14 +40,10 @@ export interface GenericProps< [arkKind]: "generic" params: params def: def - $: SchemaScope<$> + $: RootScope<$> } -export class GenericSchema< - params extends string[] = string[], - def = any, - $ = any - > +export class GenericRoot extends Callable> implements GenericProps { @@ -56,11 +52,11 @@ export class GenericSchema< constructor( public params: params, public def: def, - public $: SchemaScope<$> + public $: RootScope<$> ) { - super((...args: SchemaDef[]) => { + super((...args: RootSchema[]) => { args - // const argNodes: Record = flatMorph( + // const argNodes: Record = flatMorph( // params, // (i, param) => [param, $.schema(args[i])] // ) as never diff --git a/ark/schema/inference.ts b/ark/schema/inference.ts index 6f4cd23717..3a3627da05 100644 --- a/ark/schema/inference.ts +++ b/ark/schema/inference.ts @@ -9,20 +9,20 @@ import type { instanceOf, isAny } from "@arktype/util" -import type { NodeDef, Prerequisite } from "./kinds.js" -import type { RawNode } from "./node.js" -import type { DomainDef } from "./schemas/domain.js" -import type { IntersectionDef } from "./schemas/intersection.js" +import type { NodeSchema, Prerequisite } from "./kinds.js" +import type { BaseNode } from "./node.js" +import type { DomainSchema } from "./roots/domain.js" +import type { IntersectionSchema } from "./roots/intersection.js" import type { Morph, - MorphDef, - MorphInputDef, + MorphInputSchema, + MorphSchema, Out, inferMorphOut -} from "./schemas/morph.js" -import type { ProtoDef } from "./schemas/proto.js" -import type { NormalizedUnionDef, UnionDef } from "./schemas/union.js" -import type { UnitDef } from "./schemas/unit.js" +} from "./roots/morph.js" +import type { ProtoSchema } from "./roots/proto.js" +import type { NormalizedUnionSchema, UnionSchema } from "./roots/union.js" +import type { UnitSchema } from "./roots/unit.js" import type { ArkErrors } from "./shared/errors.js" import type { BasisKind, ConstraintKind } from "./shared/implement.js" import type { inferred } from "./shared/utils.js" @@ -35,66 +35,71 @@ export namespace type { export type errors = ArkErrors } -export type validateSchema = - def extends type.cast ? def - : def extends array ? { [i in keyof def]: validateSchemaBranch } - : def extends NormalizedUnionDef ? +export type validateRoot = + schema extends type.cast ? schema + : schema extends array ? + { [i in keyof schema]: validateRootBranch } + : schema extends NormalizedUnionSchema ? conform< - def, - NormalizedUnionDef & { + schema, + NormalizedUnionSchema & { branches: { - [i in keyof branches]: validateSchemaBranch + [i in keyof branches]: validateRootBranch } } > - : validateSchemaBranch + : validateRootBranch -export type inferSchema = - def extends type.cast ? to - : def extends UnionDef ? +export type inferRoot = + schema extends type.cast ? to + : schema extends UnionSchema ? branches["length"] extends 0 ? never - : branches["length"] extends 1 ? inferSchemaBranch - : inferSchemaBranch - : inferSchemaBranch - -type validateSchemaBranch = - def extends RawNode ? def - : "morphs" extends keyof def ? validateMorphSchema - : validateMorphChild - -type inferSchemaBranch = - def extends type.cast ? to - : def extends MorphDef ? + : branches["length"] extends 1 ? inferRootBranch + : inferRootBranch + : inferRootBranch + +type validateRootBranch = + schema extends BaseNode ? schema + : "morphs" extends keyof schema ? validateMorphRoot + : validateMorphChild + +type inferRootBranch = + schema extends type.cast ? to + : schema extends MorphSchema ? ( - In: def["from"] extends {} ? inferMorphChild : unknown - ) => def["to"] extends {} ? Out> - : def["morphs"] extends infer morph extends Morph ? + In: schema["in"] extends {} ? inferMorphChild : unknown + ) => schema["out"] extends {} ? Out> + : schema["morphs"] extends infer morph extends Morph ? Out> - : def["morphs"] extends readonly [...unknown[], infer morph extends Morph] ? + : schema["morphs"] extends ( + readonly [...unknown[], infer morph extends Morph] + ) ? Out> : never - : def extends MorphInputDef ? inferMorphChild + : schema extends MorphInputSchema ? inferMorphChild : unknown -type NonIntersectableBasisSchema = NonEnumerableDomain | Constructor | UnitDef +type NonIntersectableBasisRoot = NonEnumerableDomain | Constructor | UnitSchema -type validateMorphChild = - [def] extends [NonIntersectableBasisSchema] ? def - : validateIntersectionSchema +type validateMorphChild = + [schema] extends [NonIntersectableBasisRoot] ? schema + : validateIntersectionRoot -type inferMorphChild = - def extends NonIntersectableBasisSchema ? inferBasis - : def extends IntersectionDef ? inferBasisOf +type inferMorphChild = + schema extends NonIntersectableBasisRoot ? inferBasis + : schema extends IntersectionSchema ? inferBasisOf : unknown -type validateMorphSchema = { - [k in keyof def]: k extends "from" | "to" ? validateMorphChild - : k extends keyof MorphDef ? MorphDef[k] +type validateMorphRoot = { + [k in keyof schema]: k extends "from" | "to" ? + validateMorphChild + : k extends keyof MorphSchema ? MorphSchema[k] : `'${k & string}' is not a valid morph schema key` } -type exactBasisMessageOnError = { - [k in keyof def]: k extends keyof expected ? conform +type exactBasisMessageOnError = { + [k in keyof schema]: k extends keyof expected ? + conform : ErrorMessage< k extends ConstraintKind ? `${k} has a prerequisite of ${describe>}` @@ -102,29 +107,30 @@ type exactBasisMessageOnError = { > } -export type validateIntersectionSchema = exactBasisMessageOnError< - def, - IntersectionDef> +export type validateIntersectionRoot = exactBasisMessageOnError< + schema, + IntersectionSchema> > -type inferBasisOf = - "proto" extends keyof def ? inferBasis, $> - : "domain" extends keyof def ? - inferBasis, $> +type inferBasisOf = + "proto" extends keyof schema ? + inferBasis, $> + : "domain" extends keyof schema ? + inferBasis, $> : unknown // TODO: remove // eslint-disable-next-line @typescript-eslint/no-unused-vars -export type inferBasis, $> = - isAny extends ( +export type inferBasis, $> = + isAny extends ( true //allow any to be used to access all constraints ) ? any - : def extends NonEnumerableDomain ? inferDomain - : def extends Constructor ? instance - : def extends DomainDef ? inferDomain - : def extends ProtoDef ? instanceOf - : def extends UnitDef ? is + : schema extends NonEnumerableDomain ? inferDomain + : schema extends Constructor ? instance + : schema extends DomainSchema ? inferDomain + : schema extends ProtoSchema ? instanceOf + : schema extends UnitSchema ? is : never // export type inferPropsInput = diff --git a/ark/schema/keywords/keywords.ts b/ark/schema/keywords/keywords.ts index dd172a4118..78839511fa 100644 --- a/ark/schema/keywords/keywords.ts +++ b/ark/schema/keywords/keywords.ts @@ -1,6 +1,6 @@ -import type { GenericSchema } from "../generic.js" -import type { SchemaModule } from "../module.js" -import { RawSchemaScope, schemaScope, type SchemaScope } from "../scope.js" +import type { GenericRoot } from "../generic.js" +import type { RootModule, SchemaModule } from "../module.js" +import { RawRootScope, schemaScope, type RootScope } from "../scope.js" // the import ordering here is important so builtin keywords can be resolved // and used to bootstrap nodes with constraints import { tsKeywords, type tsKeywordExports } from "./tsKeywords.js" @@ -10,7 +10,7 @@ import { parsing, type parsingExports } from "./parsing.js" import { validation, type validationExports } from "./validation.js" type TsGenericsExports<$ = Ark> = { - Record: GenericSchema< + Record: GenericRoot< ["K", "V"], { "[K]": "V" @@ -21,7 +21,7 @@ type TsGenericsExports<$ = Ark> = { > } -export const ambientSchemaScope: SchemaScope = schemaScope({ +export const ambientRootScope: RootScope = schemaScope({ ...tsKeywords, ...jsObjects, ...validation, @@ -29,9 +29,9 @@ export const ambientSchemaScope: SchemaScope = schemaScope({ // TODO: remove cast }) as never -RawSchemaScope.ambient = ambientSchemaScope.raw +RawRootScope.ambient = ambientRootScope.raw -export const keywordNodes: SchemaModule = ambientSchemaScope.export() +export const keywordNodes: SchemaModule = ambientRootScope.export() // this type is redundant with the inferred definition of ark but allow types // derived from the default scope to be calulated more efficiently @@ -40,5 +40,5 @@ export interface Ark jsObjectExports, validationExports, TsGenericsExports { - parse: SchemaModule + parse: RootModule } diff --git a/ark/schema/keywords/parsing.ts b/ark/schema/keywords/parsing.ts index 499d60f603..1c35849416 100644 --- a/ark/schema/keywords/parsing.ts +++ b/ark/schema/keywords/parsing.ts @@ -4,12 +4,12 @@ import { wellFormedNumberMatcher } from "@arktype/util" import type { SchemaModule } from "../module.js" -import type { Out } from "../schemas/morph.js" +import type { Out } from "../roots/morph.js" import { root, schemaScope } from "../scope.js" -import { parsedDate } from "./utils/date.js" +import { tryParseDatePattern } from "./utils/date.js" -const number = root.defineSchema({ - from: { +const number = root.defineRoot({ + in: { domain: "string", regex: wellFormedNumberMatcher, description: "a well-formed numeric string" @@ -17,8 +17,8 @@ const number = root.defineSchema({ morphs: (s: string) => Number.parseFloat(s) }) -const integer = root.defineSchema({ - from: { +const integer = root.defineRoot({ + in: { domain: "string", regex: wellFormedIntegerMatcher }, @@ -35,8 +35,8 @@ const integer = root.defineSchema({ } }) -const url = root.defineSchema({ - from: { +const url = root.defineRoot({ + in: { domain: "string", description: "a valid URL" }, @@ -49,15 +49,21 @@ const url = root.defineSchema({ } }) -const json = root.defineSchema({ - from: { +const json = root.defineRoot({ + in: { domain: "string", description: "a JSON-parsable string" }, morphs: (s: string): unknown => JSON.parse(s) }) -const date = parsedDate +const date = root.defineRoot({ + in: "string", + morphs: (s: string, ctx) => { + const result = tryParseDatePattern(s) + return typeof result === "string" ? ctx.error(result) : result + } +}) export type parsingExports = { url: (In: string) => Out diff --git a/ark/schema/keywords/utils/creditCard.ts b/ark/schema/keywords/utils/creditCard.ts index 85b8723d9d..733bed8796 100644 --- a/ark/schema/keywords/utils/creditCard.ts +++ b/ark/schema/keywords/utils/creditCard.ts @@ -1,5 +1,3 @@ -import { root } from "../../scope.js" - // https://github.com/validatorjs/validator.js/blob/master/src/lib/isLuhnNumber.js export const isLuhnValid = (creditCardInput: string): boolean => { const sanitized = creditCardInput.replace(/[- ]+/g, "") @@ -22,17 +20,5 @@ export const isLuhnValid = (creditCardInput: string): boolean => { } // https://github.com/validatorjs/validator.js/blob/master/src/lib/isCreditCard.js -const creditCardMatcher = +export const creditCardMatcher: RegExp = /^(?:4[0-9]{12}(?:[0-9]{3,6})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12,15}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11}|6[27][0-9]{14}|^(81[0-9]{14,17}))$/ - -export const creditCard = root.defineSchema({ - domain: "string", - regex: { - rule: creditCardMatcher.source, - description: "a valid credit card number" - }, - predicate: { - predicate: isLuhnValid, - description: "a valid credit card number" - } -}) diff --git a/ark/schema/keywords/utils/date.ts b/ark/schema/keywords/utils/date.ts index ef71f15b6a..77acba4e47 100644 --- a/ark/schema/keywords/utils/date.ts +++ b/ark/schema/keywords/utils/date.ts @@ -1,5 +1,3 @@ -import { root } from "../../scope.js" - type DayDelimiter = "." | "/" | "-" const dayDelimiterMatcher = /^[./-]$/ @@ -95,11 +93,3 @@ export const tryParseDatePattern = ( return writeFormattedExpected(opts.format) } - -export const parsedDate = root.defineSchema({ - from: "string", - morphs: (s: string, ctx) => { - const result = tryParseDatePattern(s) - return typeof result === "string" ? ctx.error(result) : result - } -}) diff --git a/ark/schema/keywords/validation.ts b/ark/schema/keywords/validation.ts index 8ef6f2f9cf..5cc65d3943 100644 --- a/ark/schema/keywords/validation.ts +++ b/ark/schema/keywords/validation.ts @@ -1,10 +1,10 @@ import type { SchemaModule } from "../module.js" import { root, schemaScope } from "../scope.js" -import { creditCard } from "./utils/creditCard.js" +import { creditCardMatcher, isLuhnValid } from "./utils/creditCard.js" // Non-trivial expressions should have an explanation or attribution -const url = root.defineSchema({ +const url = root.defineRoot({ domain: "string", predicate: { predicate: (s: string) => { @@ -22,7 +22,7 @@ const url = root.defineSchema({ // https://www.regular-expressions.info/email.html const emailMatcher = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/ -const email = root.defineSchema({ +const email = root.defineRoot({ domain: "string", regex: { rule: emailMatcher.source, @@ -34,7 +34,7 @@ const uuidMatcher = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/ // https://github.com/validatorjs/validator.js/blob/master/src/lib/isUUID.js -const uuid = root.defineSchema({ +const uuid = root.defineRoot({ domain: "string", regex: { rule: uuidMatcher.source, @@ -46,7 +46,7 @@ const semverMatcher = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ // https://semver.org/ -const semver = root.defineSchema({ +const semver = root.defineRoot({ domain: "string", regex: { rule: semverMatcher.source, @@ -54,6 +54,18 @@ const semver = root.defineSchema({ } }) +const creditCard = root.defineRoot({ + domain: "string", + regex: { + rule: creditCardMatcher.source, + description: "a valid credit card number" + }, + predicate: { + predicate: isLuhnValid, + description: "a valid credit card number" + } +}) + export interface validationExports { alpha: string alphanumeric: string diff --git a/ark/schema/kinds.ts b/ark/schema/kinds.ts index 1341763a39..d03172c6a3 100644 --- a/ark/schema/kinds.ts +++ b/ark/schema/kinds.ts @@ -1,77 +1,94 @@ +import type { array, listable } from "@arktype/util" +import type { BaseNode } from "./node.js" import { - type PredicateDeclaration, + PredicateNode, predicateImplementation, - PredicateNode -} from "./constraints/predicate.js" + type PredicateDeclaration +} from "./predicate.js" import { - type IndexDeclaration, - indexImplementation, - IndexNode -} from "./constraints/props/index.js" -import { - type PropDeclaration, - propImplementation, - PropNode -} from "./constraints/props/prop.js" -import { - type SequenceDeclaration, - sequenceImplementation, - SequenceNode -} from "./constraints/props/sequence.js" -import { - type DivisorDeclaration, + DivisorNode, divisorImplementation, - DivisorNode -} from "./constraints/refinements/divisor.js" + type DivisorDeclaration +} from "./refinements/divisor.js" import { boundClassesByKind, + boundImplementationsByKind, type BoundDeclarations, - boundImplementationsByKind -} from "./constraints/refinements/kinds.js" + type BoundNodesByKind +} from "./refinements/kinds.js" import { - type RegexDeclaration, + RegexNode, regexImplementation, - RegexNode -} from "./constraints/refinements/regex.js" -import type { RawNode } from "./node.js" + type RegexDeclaration +} from "./refinements/regex.js" import { - type AliasDeclaration, + AliasNode, aliasImplementation, - AliasNode -} from "./schemas/alias.js" + type AliasDeclaration +} from "./roots/alias.js" import { - type DomainDeclaration, + DomainNode, domainImplementation, - DomainNode -} from "./schemas/domain.js" + type DomainDeclaration +} from "./roots/domain.js" import { - type IntersectionDeclaration, + IntersectionNode, intersectionImplementation, - IntersectionNode -} from "./schemas/intersection.js" + type IntersectionDeclaration +} from "./roots/intersection.js" import { - type MorphDeclaration, + MorphNode, morphImplementation, - type MorphInputKind, - MorphNode -} from "./schemas/morph.js" + type MorphDeclaration +} from "./roots/morph.js" import { - type ProtoDeclaration, + ProtoNode, protoImplementation, - ProtoNode -} from "./schemas/proto.js" + type ProtoDeclaration +} from "./roots/proto.js" import { - type UnionDeclaration, + UnionNode, unionImplementation, - UnionNode -} from "./schemas/union.js" + type UnionDeclaration +} from "./roots/union.js" import { - type UnitDeclaration, + UnitNode, unitImplementation, - UnitNode -} from "./schemas/unit.js" -import type { NodeKind, UnknownNodeImplementation } from "./shared/implement.js" + type UnitDeclaration +} from "./roots/unit.js" +import type { + ConstraintKind, + NodeKind, + OpenNodeKind, + RootKind, + UnknownNodeImplementation +} from "./shared/implement.js" import type { makeRootAndArrayPropertiesMutable } from "./shared/utils.js" +import { + IndexNode, + indexImplementation, + type IndexDeclaration +} from "./structure/index.js" +import { + OptionalNode, + optionalImplementation, + type OptionalDeclaration +} from "./structure/optional.js" +import { + RequiredNode, + requiredImplementation, + type RequiredDeclaration +} from "./structure/required.js" +import { + SequenceNode, + sequenceImplementation, + type SequenceDeclaration +} from "./structure/sequence.js" +import { + StructureNode, + structureImplementation, + type StructureDeclaration +} from "./structure/structure.js" export interface NodeDeclarationsByKind extends BoundDeclarations { alias: AliasDeclaration @@ -83,10 +100,12 @@ export interface NodeDeclarationsByKind extends BoundDeclarations { intersection: IntersectionDeclaration sequence: SequenceDeclaration divisor: DivisorDeclaration - prop: PropDeclaration + required: RequiredDeclaration + optional: OptionalDeclaration index: IndexDeclaration regex: RegexDeclaration predicate: PredicateDeclaration + structure: StructureDeclaration } export const nodeImplementationsByKind: Record< @@ -104,14 +123,16 @@ export const nodeImplementationsByKind: Record< divisor: divisorImplementation, regex: regexImplementation, predicate: predicateImplementation, - prop: propImplementation, + required: requiredImplementation, + optional: optionalImplementation, index: indexImplementation, - sequence: sequenceImplementation + sequence: sequenceImplementation, + structure: structureImplementation } satisfies Record as never export const nodeClassesByKind: Record< NodeKind, - new (attachments: never) => RawNode + new (attachments: never) => BaseNode > = { ...boundClassesByKind, alias: AliasNode, @@ -124,30 +145,43 @@ export const nodeClassesByKind: Record< divisor: DivisorNode, regex: RegexNode, predicate: PredicateNode, - prop: PropNode, + required: RequiredNode, + optional: OptionalNode, index: IndexNode, + sequence: SequenceNode, + structure: StructureNode +} satisfies Record> as never + +interface NodesByKind extends BoundNodesByKind { + alias: AliasNode + union: UnionNode + morph: MorphNode + intersection: IntersectionNode + unit: UnitNode + proto: ProtoNode + domain: DomainNode + divisor: DivisorNode + regex: RegexNode + predicate: PredicateNode + required: RequiredNode + optional: OptionalNode + index: IndexNode sequence: SequenceNode -} satisfies Record> as never + structure: StructureNode +} + +export type Node = NodesByKind[kind] export type Declaration = NodeDeclarationsByKind[kind] -export type NodeDef = Declaration["def"] +export type NodeSchema = Declaration["schema"] -export type NormalizedDef = - Declaration["normalizedDef"] +export type RootSchema = NodeSchema -export type childKindOf = Declaration["childKind"] +export type NormalizedSchema = + Declaration["normalizedSchema"] -type ParentsByKind = { - [k in NodeKind]: { - [pKind in NodeKind]: k extends childKindOf ? pKind : never - }[NodeKind] -} - -export type parentKindOf = ParentsByKind[kind] - -export type ioKindOf = - kind extends "morph" ? MorphInputKind : reducibleKindOf +export type childKindOf = Declaration["childKind"] export type Prerequisite = Declaration["prerequisite"] @@ -159,13 +193,18 @@ export type reducibleKindOf = export type Inner = Declaration["inner"] +export type defAttachedAs = + kind extends OpenNodeKind ? listable> : NodeSchema + +export type innerAttachedAs = + kind extends OpenNodeKind ? array> : Node + /** make nested arrays mutable while keeping nested nodes immutable */ export type MutableInner = makeRootAndArrayPropertiesMutable> -export type MutableNormalizedSchema = - makeRootAndArrayPropertiesMutable> +export type MutableNormalizedRoot = + makeRootAndArrayPropertiesMutable> -export type errorContext = Readonly< +export type errorContext = Declaration["errorContext"] -> diff --git a/ark/schema/main.ts b/ark/schema/main.ts deleted file mode 100644 index a4e1bb18e8..0000000000 --- a/ark/schema/main.ts +++ /dev/null @@ -1,41 +0,0 @@ -export * from "./config.js" -export * from "./constraints/ast.js" -export * from "./constraints/predicate.js" -export * from "./constraints/props/index.js" -export * from "./constraints/props/prop.js" -export * from "./constraints/props/sequence.js" -export * from "./constraints/refinements/after.js" -export * from "./constraints/refinements/before.js" -export * from "./constraints/refinements/divisor.js" -export * from "./constraints/refinements/max.js" -export * from "./constraints/refinements/maxLength.js" -export * from "./constraints/refinements/min.js" -export * from "./constraints/refinements/minLength.js" -export * from "./constraints/refinements/range.js" -export * from "./constraints/refinements/regex.js" -export * from "./constraints/util.js" -export * from "./generic.js" -export * from "./inference.js" -export * from "./keywords/internal.js" -export * from "./keywords/jsObjects.js" -export * from "./keywords/keywords.js" -export * from "./keywords/parsing.js" -export * from "./keywords/tsKeywords.js" -export * from "./keywords/validation.js" -export * from "./kinds.js" -export * from "./module.js" -export * from "./node.js" -export * from "./parse.js" -export * from "./schema.js" -export * from "./schemas/discriminate.js" -export * from "./schemas/intersection.js" -export * from "./schemas/morph.js" -export * from "./schemas/union.js" -export * from "./schemas/unit.js" -export * from "./scope.js" -export * from "./shared/declare.js" -export * from "./shared/disjoint.js" -export * from "./shared/errors.js" -export * from "./shared/implement.js" -export * from "./shared/intersections.js" -export * from "./shared/utils.js" diff --git a/ark/schema/module.ts b/ark/schema/module.ts index 107308b0ec..aca69bd25f 100644 --- a/ark/schema/module.ts +++ b/ark/schema/module.ts @@ -1,25 +1,31 @@ -import { DynamicBase, type isAnyOrNever } from "@arktype/util" -import type { Schema } from "./schema.js" -import { addArkKind, arkKind } from "./shared/utils.js" +import { DynamicBase, type anyOrNever } from "@arktype/util" +import type { Root } from "./roots/root.js" +import { arkKind } from "./shared/utils.js" export type PreparsedNodeResolution = { [arkKind]: "generic" | "module" } +export class RootModule< + exports extends object = {} +> extends DynamicBase { + // ensure `[arkKind]` is non-enumerable so it doesn't get spread on import/export + get [arkKind](): "module" { + return "module" + } +} + type exportSchemaScope<$> = { [k in keyof $]: $[k] extends PreparsedNodeResolution ? - isAnyOrNever<$[k]> extends true ? - Schema<$[k], $> + [$[k]] extends [anyOrNever] ? + Root<$[k], $> : $[k] - : Schema<$[k], $> + : Root<$[k], $> } -export class SchemaModule<$ = any> extends DynamicBase> { - declare readonly [arkKind]: "module" +export const SchemaModule: new <$ = {}>( + types: exportSchemaScope<$> +) => SchemaModule<$> = RootModule - constructor(types: exportSchemaScope<$>) { - super(types) - // ensure `[arkKind]` is non-enumerable so it doesn't get spread on import/export - addArkKind(this as never, "module") - } -} +export interface SchemaModule<$ = {}> + extends RootModule> {} diff --git a/ark/schema/node.ts b/ark/schema/node.ts index e263d38e0a..335d222ac4 100644 --- a/ark/schema/node.ts +++ b/ark/schema/node.ts @@ -11,24 +11,11 @@ import { type conform, type listable } from "@arktype/util" -import type { RawConstraint } from "./constraints/constraint.js" -import type { PredicateNode } from "./constraints/predicate.js" -import type { IndexNode } from "./constraints/props/index.js" -import type { PropNode } from "./constraints/props/prop.js" -import type { SequenceNode } from "./constraints/props/sequence.js" -import type { DivisorNode } from "./constraints/refinements/divisor.js" -import type { BoundNodesByKind } from "./constraints/refinements/kinds.js" -import type { RegexNode } from "./constraints/refinements/regex.js" -import type { Inner, NodeDef, reducibleKindOf } from "./kinds.js" -import type { RawSchema, Schema } from "./schema.js" -import type { AliasNode } from "./schemas/alias.js" -import type { DomainNode } from "./schemas/domain.js" -import type { IntersectionNode } from "./schemas/intersection.js" -import type { MorphNode } from "./schemas/morph.js" -import type { ProtoNode } from "./schemas/proto.js" -import type { UnionNode } from "./schemas/union.js" -import type { UnitNode } from "./schemas/unit.js" -import type { RawSchemaScope } from "./scope.js" +import type { BaseConstraint } from "./constraint.js" +import type { Inner, Node, reducibleKindOf } from "./kinds.js" +import type { BaseRoot, Root } from "./roots/root.js" +import type { UnitNode } from "./roots/unit.js" +import type { RawRootScope } from "./scope.js" import type { NodeCompiler } from "./shared/compile.js" import type { BaseMeta, @@ -39,15 +26,12 @@ import { basisKinds, constraintKinds, precedenceOfKind, - propKinds, refinementKinds, - schemaKinds, + rootKinds, type BasisKind, type NodeKind, type OpenNodeKind, - type PropKind, type RefinementKind, - type SchemaKind, type UnknownAttachments } from "./shared/implement.js" import { @@ -56,9 +40,9 @@ import { type TraverseApply } from "./shared/traversal.js" -export type UnknownNode = RawNode | Schema +export type UnknownNode = BaseNode | Root -export abstract class RawNode< +export abstract class BaseNode< /** uses -ignore rather than -expect-error because this is not an error in .d.ts * @ts-ignore allow instantiation assignment to the base type */ out d extends RawNodeDeclaration = RawNodeDeclaration @@ -91,21 +75,27 @@ export abstract class RawNode< abstract expression: string abstract compile(js: NodeCompiler): void + readonly qualifiedId = `${this.$.id}${this.id}` readonly includesMorph: boolean = - this.kind === "morph" || this.children.some(child => child.includesMorph) + this.kind === "morph" || + (this.hasKind("optional") && this.hasDefault()) || + (this.hasKind("structure") && this.undeclared === "delete") || + this.children.some(child => child.includesMorph) readonly allowsRequiresContext: boolean = // if a predicate accepts exactly one arg, we can safely skip passing context (this.hasKind("predicate") && this.inner.predicate.length !== 1) || this.kind === "alias" || this.children.some(child => child.allowsRequiresContext) - readonly referencesByName: Record = this.children.reduce( + readonly referencesByName: Record = this.children.reduce( (result, child) => Object.assign(result, child.contributesReferencesById), {} ) - readonly references: readonly RawNode[] = Object.values(this.referencesByName) - readonly contributesReferencesById: Record - readonly contributesReferences: readonly RawNode[] - readonly precedence = precedenceOfKind(this.kind) + readonly references: readonly BaseNode[] = Object.values( + this.referencesByName + ) + readonly contributesReferencesById: Record + readonly contributesReferences: readonly BaseNode[] + readonly precedence: number = precedenceOfKind(this.kind) jit = false allows = (data: d["prerequisite"]): boolean => { @@ -122,28 +112,39 @@ export abstract class RawNode< return this(data) } - private inCache?: RawNode; - get in(): RawNode { - this.inCache ??= this.getIo("in") - return this.inCache as never + // unfortunately we can't use the @cached + // decorator from @arktype/util on these for now + // as they cause a deopt in V8 + private _in?: BaseNode; + get in(): BaseNode { + this._in ??= this.getIo("in") + return this._in as never + } + + private _out?: BaseNode + get out(): BaseNode { + this._out ??= this.getIo("out") + return this._out as never } - private outCache?: RawNode - get out(): RawNode { - this.outCache ??= this.getIo("out") - return this.outCache as never + private _description?: string + get description(): string { + this._description ??= + this.inner.description ?? + this.$.resolvedConfig[this.kind].description?.(this as never) + return this._description } - getIo(kind: "in" | "out"): RawNode { + getIo(kind: "in" | "out"): BaseNode { if (!this.includesMorph) return this as never const ioInner: Record = {} for (const [k, v] of this.entries) { - const keyDefinition = this.impl.keys[k] - if (keyDefinition.meta) continue + const keySchemainition = this.impl.keys[k] + if (keySchemainition.meta) continue - if (keyDefinition.child) { - const childValue = v as listable + if (keySchemainition.child) { + const childValue = v as listable ioInner[k] = isArray(childValue) ? childValue.map(child => child[kind]) @@ -153,14 +154,6 @@ export abstract class RawNode< return this.$.node(this.kind, ioInner) } - private descriptionCache?: string - get description(): string { - this.descriptionCache ??= - this.inner.description ?? - this.$.resolvedConfig[this.kind].description?.(this as never) - return this.descriptionCache - } - toJSON(): Json { return this.json } @@ -170,7 +163,7 @@ export abstract class RawNode< } equals(other: UnknownNode): boolean - equals(other: RawNode): boolean { + equals(other: BaseNode): boolean { return this.typeHash === other.typeHash } @@ -182,7 +175,7 @@ export abstract class RawNode< return includes(basisKinds, this.kind) } - isConstraint(): this is Constraint { + isConstraint(): this is BaseConstraint { return includes(constraintKinds, this.kind) } @@ -190,12 +183,8 @@ export abstract class RawNode< return includes(refinementKinds, this.kind) } - isProp(): this is Node { - return includes(propKinds, this.kind) - } - - isSchema(): this is RawSchema { - return includes(schemaKinds, this.kind) + isRoot(): this is BaseRoot { + return includes(rootKinds, this.kind) } hasUnit(value: unknown): this is UnitNode & { unit: value } { @@ -207,15 +196,10 @@ export abstract class RawNode< } get nestableExpression(): string { - return ( - this.children.length > 1 && - this.children.some(child => !child.isBasis && !child.isProp()) - ) ? - `(${this.expression})` - : this.expression + return this.expression } - bindScope($: RawSchemaScope): this { + bindScope($: RawRootScope): this { if (this.$ === $) return this as never return new (this.constructor as any)( Object.assign(shallowClone(this.attachments), { $ }) @@ -223,13 +207,13 @@ export abstract class RawNode< } firstReference( - filter: Guardable> + filter: Guardable> ): narrowed | undefined { return this.references.find(filter as never) as never } - firstReferenceOrThrow( - filter: Guardable + firstReferenceOrThrow( + filter: Guardable ): narrowed { return ( this.firstReference(filter) ?? @@ -252,9 +236,24 @@ export abstract class RawNode< transform( mapper: DeepNodeTransformation, - shouldTransform: (node: RawNode) => boolean + shouldTransform: ShouldTransformFn ): Node> { - if (!shouldTransform(this as never)) return this as never + return this._transform(mapper, shouldTransform, { seen: {} }) as never + } + + private _transform( + mapper: DeepNodeTransformation, + shouldTransform: ShouldTransformFn, + ctx: DeepNodeTransformationContext + ): BaseNode { + if (ctx.seen[this.id]) + // TODO: remove cast by making lazilyResolve more flexible + // TODO: if each transform has a unique base id, could ensure + // these don't create duplicates + return this.$.lazilyResolve(ctx.seen[this.id]! as never) + if (!shouldTransform(this as never, ctx)) return this + + ctx.seen[this.id] = () => node const innerWithTransformedChildren = flatMorph( this.inner as Dict, @@ -262,15 +261,22 @@ export abstract class RawNode< k, this.impl.keys[k].child ? isArray(v) ? - v.map(node => (node as RawNode).transform(mapper, shouldTransform)) - : (v as RawNode).transform(mapper, shouldTransform) + v.map(node => + (node as BaseNode)._transform(mapper, shouldTransform, ctx) + ) + : (v as BaseNode)._transform(mapper, shouldTransform, ctx) : v ] ) - return this.$.node( + + delete ctx.seen[this.id] + + const node = this.$.node( this.kind, - mapper(this.kind, innerWithTransformedChildren as never) as never - ) as never + mapper(this.kind, innerWithTransformedChildren as never, ctx) as never + ) + + return node } configureShallowDescendants(configOrDescription: BaseMeta | string): this { @@ -280,34 +286,22 @@ export abstract class RawNode< : (configOrDescription as never) return this.transform( (kind, inner) => ({ ...inner, ...config }), - node => !node.isProp() + node => node.kind !== "structure" ) as never } } -export type DeepNodeTransformation = ( - kind: kind, - inner: Inner -) => Inner +export type ShouldTransformFn = ( + node: BaseNode, + ctx: DeepNodeTransformationContext +) => boolean -interface NodesByKind extends BoundNodesByKind { - alias: AliasNode - union: UnionNode - morph: MorphNode - intersection: IntersectionNode - unit: UnitNode - proto: ProtoNode - domain: DomainNode - divisor: DivisorNode - regex: RegexNode - predicate: PredicateNode - prop: PropNode - index: IndexNode - sequence: SequenceNode +export type DeepNodeTransformationContext = { + seen: { [originalId: string]: (() => BaseNode) | undefined } } -export type Node = NodesByKind[kind] - -export type SchemaDef = NodeDef - -export type Constraint = RawConstraint +export type DeepNodeTransformation = ( + kind: kind, + inner: Inner, + ctx: DeepNodeTransformationContext +) => Inner diff --git a/ark/schema/package.json b/ark/schema/package.json index a4661c51fc..52ed0f8c66 100644 --- a/ark/schema/package.json +++ b/ark/schema/package.json @@ -1,6 +1,6 @@ { "name": "@arktype/schema", - "version": "0.1.1", + "version": "0.1.4", "license": "MIT", "author": { "name": "David Blass", @@ -12,25 +12,15 @@ "url": "https://github.com/arktypeio/arktype.git" }, "type": "module", - "main": "./out/main.js", - "types": "./out/main.d.ts", + "main": "./out/api.js", + "types": "./out/api.d.ts", "exports": { - ".": { - "types": "./out/main.d.ts", - "default": "./out/main.js" - }, - "./config": { - "types": "./out/config.d.ts", - "default": "./out/config.js" - }, - "./internal/*": { - "default": "./out/*" - } + ".": "./out/api.js", + "./config": "./out/config.js", + "./internal/*": "./out/*" }, "files": [ - "out", - "!__tests__", - "**/*.ts" + "out" ], "scripts": { "build": "tsx ../repo/build.ts", diff --git a/ark/schema/parse.ts b/ark/schema/parse.ts index ac1f25bab3..6344a527ab 100644 --- a/ark/schema/parse.ts +++ b/ark/schema/parse.ts @@ -1,35 +1,36 @@ import { + entriesOf, + hasDomain, + isArray, + printable, + throwParseError, + unset, type Dict, type Json, type JsonData, type PartialRecord, - entriesOf, - hasDomain, - isArray, type listable, - printable, - type propValueOf, - throwParseError + type propValueOf } from "@arktype/util" import { - type NormalizedDef, nodeClassesByKind, - nodeImplementationsByKind + nodeImplementationsByKind, + type NormalizedSchema } from "./kinds.js" -import type { RawNode } from "./node.js" -import type { UnknownSchema } from "./schema.js" -import type { RawSchemaScope } from "./scope.js" +import type { BaseNode } from "./node.js" +import type { UnknownRoot } from "./roots/root.js" +import type { RawRootScope } from "./scope.js" import type { RawNodeDeclaration } from "./shared/declare.js" import { Disjoint } from "./shared/disjoint.js" import { - type KeyDefinitions, - type NodeKind, - type SchemaKind, - type UnknownAttachments, + constraintKeys, defaultValueSerializer, - discriminatingIntersectionKeys, isNodeKind, - precedenceOfKind + precedenceOfKind, + type KeySchemainitions, + type NodeKind, + type RootKind, + type UnknownAttachments } from "./shared/implement.js" import { hasArkKind } from "./shared/utils.js" @@ -41,74 +42,76 @@ export type NodeParseOptions = { * * Useful for defining reductions like number|string|bigint|symbol|object|true|false|null|undefined => unknown **/ - reduceTo?: RawNode + reduceTo?: BaseNode } export interface NodeParseContext extends NodeParseOptions { - $: RawSchemaScope + $: RawRootScope id: string - args?: Record - def: NormalizedDef + args?: Record + schema: NormalizedSchema } -const baseKeys: PartialRecord>> = { +const baseKeys: PartialRecord>> = { description: { meta: true } -} satisfies KeyDefinitions as never +} satisfies KeySchemainitions as never -export const schemaKindOf = ( - def: unknown, +export const schemaKindOf = ( + schema: unknown, allowedKinds?: readonly kind[] ): kind => { - const kind = discriminateSchemaKind(def) + const kind = discriminateRootKind(schema) if (allowedKinds && !allowedKinds.includes(kind as never)) { return throwParseError( - `Schema of kind ${kind} should be one of ${allowedKinds}` + `Root of kind ${kind} should be one of ${allowedKinds}` ) } return kind as never } -const discriminateSchemaKind = (def: unknown): SchemaKind => { - switch (typeof def) { +const discriminateRootKind = (schema: unknown): RootKind => { + switch (typeof schema) { case "string": - return def[0] === "$" ? "alias" : "domain" + return schema[0] === "$" ? "alias" : "domain" case "function": - return hasArkKind(def, "schema") ? def.kind : "proto" + return hasArkKind(schema, "root") ? schema.kind : "proto" case "object": { // throw at end of function - if (def === null) break + if (schema === null) break - if ("morphs" in def) return "morph" + if ("morphs" in schema) return "morph" - if ("branches" in def || isArray(def)) return "union" + if ("branches" in schema || isArray(schema)) return "union" - if ("unit" in def) return "unit" + if ("unit" in schema) return "unit" - if ("alias" in def) return "alias" + if ("alias" in schema) return "alias" - const schemaKeys = Object.keys(def) + const schemaKeys = Object.keys(schema) - if ( - schemaKeys.length === 0 || - schemaKeys.some(k => k in discriminatingIntersectionKeys) - ) + if (schemaKeys.length === 0 || schemaKeys.some(k => k in constraintKeys)) return "intersection" - if ("proto" in def) return "proto" - if ("domain" in def) return "domain" + if ("proto" in schema) return "proto" + if ("domain" in schema) return "domain" } } - return throwParseError(`${printable(def)} is not a valid type schema`) + return throwParseError(`${printable(schema)} is not a valid type schema`) } -const nodeCache: { [innerHash: string]: RawNode } = {} +const nodeCache: { [innerHash: string]: BaseNode } = {} -export const parseNode = (kind: NodeKind, ctx: NodeParseContext): RawNode => { +const serializeListableChild = (listableNode: listable) => + isArray(listableNode) ? + listableNode.map(node => node.collapsibleJson) + : listableNode.collapsibleJson + +export const parseNode = (kind: NodeKind, ctx: NodeParseContext): BaseNode => { const impl = nodeImplementationsByKind[kind] const inner: Record = {} // ensure node entries are parsed in order of precedence, with non-children // parsed first - const schemaEntries = entriesOf(ctx.def as Dict).sort(([lKey], [rKey]) => + const schemaEntries = entriesOf(ctx.schema as Dict).sort(([lKey], [rKey]) => isNodeKind(lKey) ? isNodeKind(rKey) ? precedenceOfKind(lKey) - precedenceOfKind(rKey) : 1 @@ -116,7 +119,7 @@ export const parseNode = (kind: NodeKind, ctx: NodeParseContext): RawNode => { : lKey < rKey ? -1 : 1 ) - const children: RawNode[] = [] + const children: BaseNode[] = [] for (const entry of schemaEntries) { const k = entry[0] const keyImpl = impl.keys[k] ?? baseKeys[k] @@ -124,36 +127,36 @@ export const parseNode = (kind: NodeKind, ctx: NodeParseContext): RawNode => { return throwParseError(`Key ${k} is not valid on ${kind} schema`) const v = keyImpl.parse ? keyImpl.parse(entry[1], ctx) : entry[1] - if (v !== undefined || keyImpl.preserveUndefined) inner[k] = v + if (v !== unset && (v !== undefined || keyImpl.preserveUndefined)) + inner[k] = v } const entries = entriesOf(inner) let json: Record = {} let typeJson: Record = {} - let collapsibleJson: Record = {} entries.forEach(([k, v]) => { + const listableNode = v as listable const keyImpl = impl.keys[k] ?? baseKeys[k] + const serialize = + keyImpl.serialize ?? + (keyImpl.child ? serializeListableChild : defaultValueSerializer) + + json[k] = serialize(listableNode) + if (keyImpl.child) { - const listableNode = v as listable - if (isArray(listableNode)) { - json[k] = listableNode.map(node => node.collapsibleJson) - children.push(...listableNode) - } else { - json[k] = listableNode.collapsibleJson - children.push(listableNode) - } - } else { - json[k] = - keyImpl.serialize ? keyImpl.serialize(v) : defaultValueSerializer(v) + if (isArray(listableNode)) children.push(...listableNode) + else children.push(listableNode) } - if (!keyImpl.meta) typeJson[k] = json[k] - - if (!keyImpl.implied) collapsibleJson[k] = json[k] }) - // check keys on collapsibleJson instead of schema in case one or more keys is - // implied, e.g. minVariadicLength on a SequenceNode + if (impl.finalizeJson) { + json = impl.finalizeJson(json) as never + typeJson = impl.finalizeJson(typeJson) as never + } + + let collapsibleJson = json + const collapsibleKeys = Object.keys(collapsibleJson) if ( collapsibleKeys.length === 1 && @@ -216,9 +219,12 @@ export const parseNode = (kind: NodeKind, ctx: NodeParseContext): RawNode => { } satisfies UnknownAttachments as Record if (ctx.alias) attachments.alias = ctx.alias - for (const k in inner) if (k !== "description") attachments[k] = inner[k] + for (const k in inner) { + if (k !== "description" && k !== "in" && k !== "out") + attachments[k] = inner[k] + } - const node: RawNode = new nodeClassesByKind[kind](attachments as never) + const node: BaseNode = new nodeClassesByKind[kind](attachments as never) nodeCache[innerHash] = node return node diff --git a/ark/schema/predicate.ts b/ark/schema/predicate.ts new file mode 100644 index 0000000000..94fc37b7a5 --- /dev/null +++ b/ark/schema/predicate.ts @@ -0,0 +1,105 @@ +import { registeredReference, type RegisteredReference } from "@arktype/util" +import type { constrain, of } from "./ast.js" +import { BaseConstraint } from "./constraint.js" +import type { errorContext } from "./kinds.js" +import type { NodeCompiler } from "./shared/compile.js" +import type { BaseMeta, declareNode } from "./shared/declare.js" +import { implementNode, type nodeImplementationOf } from "./shared/implement.js" +import type { + TraversalContext, + TraverseAllows, + TraverseApply +} from "./shared/traversal.js" + +export interface PredicateInner = Predicate> + extends BaseMeta { + readonly predicate: rule +} + +export type PredicateErrorContext = Partial + +export type PredicateSchema = PredicateInner | Predicate + +export interface PredicateDeclaration + extends declareNode<{ + kind: "predicate" + schema: PredicateSchema + normalizedSchema: PredicateInner + inner: PredicateInner + intersectionIsOpen: true + errorContext: PredicateErrorContext + }> {} + +export const predicateImplementation: nodeImplementationOf = + implementNode({ + kind: "predicate", + hasAssociatedError: true, + collapsibleKey: "predicate", + keys: { + predicate: {} + }, + normalize: schema => + typeof schema === "function" ? { predicate: schema } : schema, + defaults: { + description: node => + `valid according to ${node.predicate.name || "an anonymous predicate"}` + }, + intersectionIsOpen: true, + intersections: { + // TODO: allow changed order to be the same type + // as long as the narrows in l and r are individually safe to check + // in the order they're specified, checking them in the order + // resulting from this intersection should also be safe. + predicate: () => null + } + }) + +export class PredicateNode extends BaseConstraint { + serializedPredicate: RegisteredReference = registeredReference(this.predicate) + compiledCondition = `${this.serializedPredicate}(data, ctx)` + compiledNegation = `!${this.compiledCondition}` + + impliedBasis = null + + expression: string = this.serializedPredicate + traverseAllows: TraverseAllows = this.predicate + + errorContext: errorContext<"predicate"> = { + code: "predicate", + description: this.description + } + + compiledErrorContext = `{ code: "predicate", description: "${this.description}" }` + + traverseApply: TraverseApply = (data, ctx) => { + if (!this.predicate(data, ctx) && !ctx.hasError()) + ctx.error(this.errorContext) + } + + compile(js: NodeCompiler): void { + if (js.traversalKind === "Allows") { + js.return(this.compiledCondition) + return + } + js.if(`${this.compiledNegation} && !ctx.hasError()`, () => + js.line(`ctx.error(${this.compiledErrorContext})`) + ) + } +} + +export type Predicate = ( + data: data, + ctx: TraversalContext +) => boolean + +export type PredicateCast = ( + input: input, + ctx: TraversalContext +) => input is narrowed + +export type inferNarrow = + predicate extends (data: any, ...args: any[]) => data is infer narrowed ? + In extends of ? + constrain, "predicate", any> + : constrain + : constrain diff --git a/ark/schema/refinements/after.ts b/ark/schema/refinements/after.ts new file mode 100644 index 0000000000..e7215feac8 --- /dev/null +++ b/ark/schema/refinements/after.ts @@ -0,0 +1,74 @@ +import type { BaseRoot } from "../roots/root.js" +import type { declareNode } from "../shared/declare.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import type { TraverseAllows } from "../shared/traversal.js" +import { + BaseRange, + parseDateLimit, + parseExclusiveKey, + type BaseNormalizedRangeRoot, + type BaseRangeInner, + type LimitRootValue +} from "./range.js" + +export interface AfterInner extends BaseRangeInner { + rule: Date +} + +export interface NormalizedAfterSchema extends BaseNormalizedRangeRoot { + rule: LimitRootValue +} + +export type AfterSchema = NormalizedAfterSchema | LimitRootValue + +export interface AfterDeclaration + extends declareNode<{ + kind: "after" + schema: AfterSchema + normalizedSchema: NormalizedAfterSchema + inner: AfterInner + prerequisite: Date + errorContext: AfterInner + }> {} + +export const afterImplementation: nodeImplementationOf = + implementNode({ + kind: "after", + collapsibleKey: "rule", + hasAssociatedError: true, + keys: { + rule: { + parse: parseDateLimit, + serialize: schema => schema.toISOString() + }, + exclusive: parseExclusiveKey + }, + normalize: schema => + ( + typeof schema === "number" || + typeof schema === "string" || + schema instanceof Date + ) ? + { rule: schema } + : schema, + defaults: { + description: node => + node.exclusive ? + `after ${node.stringLimit}` + : `${node.stringLimit} or later`, + actual: data => data.toLocaleString() + }, + intersections: { + after: (l, r) => (l.isStricterThan(r) ? l : r) + } + }) + +export class AfterNode extends BaseRange { + impliedBasis: BaseRoot = this.$.keywords.Date.raw + + traverseAllows: TraverseAllows = + this.exclusive ? data => data > this.rule : data => data >= this.rule +} diff --git a/ark/schema/refinements/before.ts b/ark/schema/refinements/before.ts new file mode 100644 index 0000000000..1e400a53a1 --- /dev/null +++ b/ark/schema/refinements/before.ts @@ -0,0 +1,81 @@ +import type { BaseRoot } from "../roots/root.js" +import type { declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import type { TraverseAllows } from "../shared/traversal.js" +import { + BaseRange, + parseDateLimit, + parseExclusiveKey, + type BaseNormalizedRangeRoot, + type BaseRangeInner, + type LimitRootValue +} from "./range.js" + +export interface BeforeInner extends BaseRangeInner { + rule: Date +} + +export interface NormalizedBeforeSchema extends BaseNormalizedRangeRoot { + rule: LimitRootValue +} + +export type BeforeSchema = NormalizedBeforeSchema | LimitRootValue + +export interface BeforeDeclaration + extends declareNode<{ + kind: "before" + schema: BeforeSchema + normalizedSchema: NormalizedBeforeSchema + inner: BeforeInner + prerequisite: Date + errorContext: BeforeInner + }> {} + +export const beforeImplementation: nodeImplementationOf = + implementNode({ + kind: "before", + collapsibleKey: "rule", + hasAssociatedError: true, + keys: { + rule: { + parse: parseDateLimit, + serialize: schema => schema.toISOString() + }, + exclusive: parseExclusiveKey + }, + normalize: schema => + ( + typeof schema === "number" || + typeof schema === "string" || + schema instanceof Date + ) ? + { rule: schema } + : schema, + defaults: { + description: node => + node.exclusive ? + `before ${node.stringLimit}` + : `${node.stringLimit} or earlier`, + actual: data => data.toLocaleString() + }, + intersections: { + before: (l, r) => (l.isStricterThan(r) ? l : r), + after: (before, after, ctx) => + before.overlapsRange(after) ? + before.overlapIsUnit(after) ? + ctx.$.node("unit", { unit: before.rule }) + : null + : Disjoint.from("range", before, after) + } + }) + +export class BeforeNode extends BaseRange { + traverseAllows: TraverseAllows = + this.exclusive ? data => data < this.rule : data => data <= this.rule + + impliedBasis: BaseRoot = this.$.keywords.Date.raw +} diff --git a/ark/schema/refinements/divisor.ts b/ark/schema/refinements/divisor.ts new file mode 100644 index 0000000000..37612af56d --- /dev/null +++ b/ark/schema/refinements/divisor.ts @@ -0,0 +1,82 @@ +import { + RawPrimitiveConstraint, + writeInvalidOperandMessage +} from "../constraint.js" +import type { BaseRoot, RawRootDeclaration, Root } from "../roots/root.js" +import type { BaseMeta, declareNode } from "../shared/declare.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import type { TraverseAllows } from "../shared/traversal.js" + +export interface DivisorInner extends BaseMeta { + readonly rule: number +} + +export type DivisorSchema = DivisorInner | number + +export interface DivisorDeclaration + extends declareNode<{ + kind: "divisor" + schema: DivisorSchema + normalizedSchema: DivisorInner + inner: DivisorInner + prerequisite: number + errorContext: DivisorInner + }> {} + +export const divisorImplementation: nodeImplementationOf = + implementNode({ + kind: "divisor", + collapsibleKey: "rule", + keys: { + rule: {} + }, + normalize: schema => + typeof schema === "number" ? { rule: schema } : schema, + hasAssociatedError: true, + defaults: { + description: node => + node.rule === 1 ? "an integer" : `a multiple of ${node.rule}` + }, + intersections: { + divisor: (l, r, ctx) => + ctx.$.node("divisor", { + rule: Math.abs( + (l.rule * r.rule) / greatestCommonDivisor(l.rule, r.rule) + ) + }) + } + }) + +export class DivisorNode extends RawPrimitiveConstraint { + traverseAllows: TraverseAllows = data => data % this.rule === 0 + + readonly compiledCondition: string = `data % ${this.rule} === 0` + readonly compiledNegation: string = `data % ${this.rule} !== 0` + readonly impliedBasis: BaseRoot = + this.$.keywords.number.raw + readonly expression: string = `% ${this.rule}` +} + +export const writeIndivisibleMessage = ( + t: node +): writeIndivisibleMessage => + writeInvalidOperandMessage("divisor", t.$.raw.keywords.number, t) + +export type writeIndivisibleMessage = + writeInvalidOperandMessage<"divisor", node> + +// https://en.wikipedia.org/wiki/Euclidean_algorithm +const greatestCommonDivisor = (l: number, r: number) => { + let previous: number + let greatestCommonDivisor = l + let current = r + while (current !== 0) { + previous = current + current = greatestCommonDivisor % current + greatestCommonDivisor = previous + } + return greatestCommonDivisor +} diff --git a/ark/schema/refinements/exactLength.ts b/ark/schema/refinements/exactLength.ts new file mode 100644 index 0000000000..1ecfb53af9 --- /dev/null +++ b/ark/schema/refinements/exactLength.ts @@ -0,0 +1,79 @@ +import { RawPrimitiveConstraint } from "../constraint.js" +import type { BaseRoot } from "../roots/root.js" +import type { BaseMeta, declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import type { TraverseAllows } from "../shared/traversal.js" +import type { LengthBoundableData } from "./range.js" + +export interface ExactLengthInner extends BaseMeta { + readonly rule: number +} + +export type NormalizedExactLengthSchema = ExactLengthInner + +export type ExactLengthSchema = NormalizedExactLengthSchema | number + +export type ExactLengthDeclaration = declareNode<{ + kind: "exactLength" + schema: ExactLengthSchema + normalizedSchema: NormalizedExactLengthSchema + inner: ExactLengthInner + prerequisite: LengthBoundableData + errorContext: ExactLengthInner +}> + +export const exactLengthImplementation: nodeImplementationOf = + implementNode({ + kind: "exactLength", + collapsibleKey: "rule", + keys: { + rule: {} + }, + normalize: schema => + typeof schema === "number" ? { rule: schema } : schema, + hasAssociatedError: true, + defaults: { + description: node => `exactly length ${node.rule}` + }, + intersections: { + exactLength: (l, r, ctx) => + new Disjoint({ + "[length]": { + unit: { + l: ctx.$.node("unit", { unit: l.rule }), + r: ctx.$.node("unit", { unit: r.rule }) + } + } + }), + minLength: (exactLength, minLength) => + ( + minLength.exclusive ? + exactLength.rule > minLength.rule + : exactLength.rule >= minLength.rule + ) ? + exactLength + : Disjoint.from("range", exactLength, minLength), + maxLength: (exactLength, maxLength) => + ( + maxLength.exclusive ? + exactLength.rule < maxLength.rule + : exactLength.rule <= maxLength.rule + ) ? + exactLength + : Disjoint.from("range", exactLength, maxLength) + } + }) + +export class ExactLengthNode extends RawPrimitiveConstraint { + traverseAllows: TraverseAllows = data => + data.length === this.rule + + readonly compiledCondition: string = `data.length === ${this.rule}` + readonly compiledNegation: string = `data.length !== ${this.rule}` + readonly impliedBasis: BaseRoot = this.$.keywords.lengthBoundable.raw + readonly expression: string = `{ length: ${this.rule} }` +} diff --git a/ark/schema/constraints/refinements/kinds.ts b/ark/schema/refinements/kinds.ts similarity index 78% rename from ark/schema/constraints/refinements/kinds.ts rename to ark/schema/refinements/kinds.ts index 3afe13f06e..3b798d6412 100644 --- a/ark/schema/constraints/refinements/kinds.ts +++ b/ark/schema/refinements/kinds.ts @@ -1,5 +1,5 @@ -import type { BoundKind } from "../../shared/implement.js" -import type { RawConstraint } from "../constraint.js" +import type { BaseConstraint } from "../constraint.js" +import type { BoundKind, nodeImplementationOf } from "../shared/implement.js" import { type AfterDeclaration, AfterNode, @@ -48,7 +48,11 @@ export interface BoundNodesByKind { before: BeforeNode } -export const boundImplementationsByKind = { +export type boundImplementationsByKind = { + [k in BoundKind]: nodeImplementationOf +} + +export const boundImplementationsByKind: boundImplementationsByKind = { min: minImplementation, max: maxImplementation, minLength: minLengthImplementation, @@ -58,7 +62,7 @@ export const boundImplementationsByKind = { before: beforeImplementation } -export const boundClassesByKind: Record> = +export const boundClassesByKind: Record> = { min: MinNode, max: MaxNode, diff --git a/ark/schema/refinements/max.ts b/ark/schema/refinements/max.ts new file mode 100644 index 0000000000..2a46749c27 --- /dev/null +++ b/ark/schema/refinements/max.ts @@ -0,0 +1,67 @@ +import type { BaseRoot } from "../roots/root.js" +import type { declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import type { TraverseAllows } from "../shared/traversal.js" +import { + type BaseNormalizedRangeRoot, + BaseRange, + type BaseRangeInner, + parseExclusiveKey +} from "./range.js" + +export interface MaxInner extends BaseRangeInner { + rule: number +} + +export interface NormalizedMaxSchema extends BaseNormalizedRangeRoot { + rule: number +} + +export type MaxSchema = NormalizedMaxSchema | number + +export interface MaxDeclaration + extends declareNode<{ + kind: "max" + schema: MaxSchema + normalizedSchema: NormalizedMaxSchema + inner: MaxInner + prerequisite: number + errorContext: MaxInner + }> {} + +export const maxImplementation: nodeImplementationOf = + implementNode({ + kind: "max", + collapsibleKey: "rule", + hasAssociatedError: true, + keys: { + rule: {}, + exclusive: parseExclusiveKey + }, + normalize: schema => + typeof schema === "number" ? { rule: schema } : schema, + defaults: { + description: node => + `${node.exclusive ? "less than" : "at most"} ${node.rule}` + }, + intersections: { + max: (l, r) => (l.isStricterThan(r) ? l : r), + min: (max, min, ctx) => + max.overlapsRange(min) ? + max.overlapIsUnit(min) ? + ctx.$.node("unit", { unit: max.rule }) + : null + : Disjoint.from("range", max, min) + } + }) + +export class MaxNode extends BaseRange { + impliedBasis: BaseRoot = this.$.keywords.number.raw + + traverseAllows: TraverseAllows = + this.exclusive ? data => data < this.rule : data => data <= this.rule +} diff --git a/ark/schema/refinements/maxLength.ts b/ark/schema/refinements/maxLength.ts new file mode 100644 index 0000000000..0dbfffcf5b --- /dev/null +++ b/ark/schema/refinements/maxLength.ts @@ -0,0 +1,73 @@ +import type { BaseRoot } from "../roots/root.js" +import type { declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import type { TraverseAllows } from "../shared/traversal.js" +import { + type BaseNormalizedRangeRoot, + BaseRange, + type BaseRangeInner, + type LengthBoundableData, + parseExclusiveKey +} from "./range.js" + +export interface MaxLengthInner extends BaseRangeInner { + rule: number +} + +export interface NormalizedMaxLengthSchema extends BaseNormalizedRangeRoot { + rule: number +} + +export type MaxLengthSchema = NormalizedMaxLengthSchema | number + +export interface MaxLengthDeclaration + extends declareNode<{ + kind: "maxLength" + schema: MaxLengthSchema + normalizedSchema: NormalizedMaxLengthSchema + inner: MaxLengthInner + prerequisite: LengthBoundableData + errorContext: MaxLengthInner + }> {} + +export const maxLengthImplementation: nodeImplementationOf = + implementNode({ + kind: "maxLength", + collapsibleKey: "rule", + hasAssociatedError: true, + keys: { + rule: {}, + exclusive: parseExclusiveKey + }, + normalize: schema => + typeof schema === "number" ? { rule: schema } : schema, + defaults: { + description: node => + node.exclusive ? + `less than length ${node.rule}` + : `at most length ${node.rule}`, + actual: data => `${data.length}` + }, + intersections: { + maxLength: (l, r) => (l.isStricterThan(r) ? l : r), + minLength: (max, min, ctx) => + max.overlapsRange(min) ? + max.overlapIsUnit(min) ? + ctx.$.node("exactLength", { rule: max.rule }) + : null + : Disjoint.from("range", max, min) + } + }) + +export class MaxLengthNode extends BaseRange { + readonly impliedBasis: BaseRoot = this.$.keywords.lengthBoundable.raw + + traverseAllows: TraverseAllows = + this.exclusive ? + data => data.length < this.rule + : data => data.length <= this.rule +} diff --git a/ark/schema/refinements/min.ts b/ark/schema/refinements/min.ts new file mode 100644 index 0000000000..2d8b899547 --- /dev/null +++ b/ark/schema/refinements/min.ts @@ -0,0 +1,60 @@ +import type { BaseRoot } from "../roots/root.js" +import type { declareNode } from "../shared/declare.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import type { TraverseAllows } from "../shared/traversal.js" +import { + type BaseNormalizedRangeRoot, + BaseRange, + type BaseRangeInner, + parseExclusiveKey +} from "./range.js" + +export interface MinInner extends BaseRangeInner { + rule: number +} + +export interface NormalizedMinRoot extends BaseNormalizedRangeRoot { + rule: number +} + +export type MinRoot = NormalizedMinRoot | number + +export interface MinDeclaration + extends declareNode<{ + kind: "min" + schema: MinRoot + normalizedSchema: NormalizedMinRoot + inner: MinInner + prerequisite: number + errorContext: MinInner + }> {} + +export const minImplementation: nodeImplementationOf = + implementNode({ + kind: "min", + collapsibleKey: "rule", + hasAssociatedError: true, + keys: { + rule: {}, + exclusive: parseExclusiveKey + }, + normalize: schema => + typeof schema === "number" ? { rule: schema } : schema, + defaults: { + description: node => + `${node.exclusive ? "more than" : "at least"} ${node.rule}` + }, + intersections: { + min: (l, r) => (l.isStricterThan(r) ? l : r) + } + }) + +export class MinNode extends BaseRange { + readonly impliedBasis: BaseRoot = this.$.keywords.number.raw + + traverseAllows: TraverseAllows = + this.exclusive ? data => data > this.rule : data => data >= this.rule +} diff --git a/ark/schema/refinements/minLength.ts b/ark/schema/refinements/minLength.ts new file mode 100644 index 0000000000..c311c5728a --- /dev/null +++ b/ark/schema/refinements/minLength.ts @@ -0,0 +1,69 @@ +import type { BaseRoot } from "../roots/root.js" +import type { declareNode } from "../shared/declare.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import type { TraverseAllows } from "../shared/traversal.js" +import { + type BaseNormalizedRangeRoot, + BaseRange, + type BaseRangeInner, + type LengthBoundableData, + parseExclusiveKey +} from "./range.js" + +export interface MinLengthInner extends BaseRangeInner { + rule: number +} + +export interface NormalizedMinLengthSchema extends BaseNormalizedRangeRoot { + rule: number +} + +export type MinLengthSchema = NormalizedMinLengthSchema | number + +export interface MinLengthDeclaration + extends declareNode<{ + kind: "minLength" + schema: MinLengthSchema + normalizedSchema: NormalizedMinLengthSchema + inner: MinLengthInner + prerequisite: LengthBoundableData + errorContext: MinLengthInner + }> {} + +export const minLengthImplementation: nodeImplementationOf = + implementNode({ + kind: "minLength", + collapsibleKey: "rule", + hasAssociatedError: true, + keys: { + rule: {}, + exclusive: parseExclusiveKey + }, + normalize: schema => + typeof schema === "number" ? { rule: schema } : schema, + defaults: { + description: node => + node.exclusive ? + node.rule === 0 ? + "non-empty" + : `more than length ${node.rule}` + : node.rule === 1 ? "non-empty" + : `at least length ${node.rule}`, + actual: data => `${data.length}` + }, + intersections: { + minLength: (l, r) => (l.isStricterThan(r) ? l : r) + } + }) + +export class MinLengthNode extends BaseRange { + readonly impliedBasis: BaseRoot = this.$.keywords.lengthBoundable.raw + + traverseAllows: TraverseAllows = + this.exclusive ? + data => data.length > this.rule + : data => data.length >= this.rule +} diff --git a/ark/schema/constraints/refinements/range.ts b/ark/schema/refinements/range.ts similarity index 68% rename from ark/schema/constraints/refinements/range.ts rename to ark/schema/refinements/range.ts index 5e146dbf74..9b9abbaba8 100644 --- a/ark/schema/constraints/refinements/range.ts +++ b/ark/schema/refinements/range.ts @@ -1,40 +1,42 @@ import { - type PartialRecord, type array, - invert, isKeyOf, - type propValueOf + type propValueOf, + type satisfy } from "@arktype/util" -import type { Node } from "../../node.js" -import type { BaseMeta, RawNodeDeclaration } from "../../shared/declare.js" -import type { KeyDefinitions, RangeKind } from "../../shared/implement.js" import { RawPrimitiveConstraint } from "../constraint.js" - +import type { Node } from "../kinds.js" +import type { BaseMeta, RawNodeDeclaration } from "../shared/declare.js" +import type { KeySchemainitions, RangeKind } from "../shared/implement.js" export interface BaseRangeDeclaration extends RawNodeDeclaration { kind: RangeKind inner: BaseRangeInner - normalizedDef: BaseNormalizedRangeSchema + normalizedSchema: BaseNormalizedRangeRoot } export abstract class BaseRange< d extends BaseRangeDeclaration > extends RawPrimitiveConstraint { - readonly boundOperandKind = operandKindsByBoundKind[this.kind] - readonly compiledActual = + readonly boundOperandKind: OperandKindsByBoundKind[d["kind"]] = + operandKindsByBoundKind[this.kind] + readonly compiledActual: string = this.boundOperandKind === "value" ? `data` : this.boundOperandKind === "length" ? `data.length` : `data.valueOf()` - readonly comparator = compileComparator(this.kind, this.exclusive) - readonly numericLimit = this.rule.valueOf() - readonly expression = `${this.comparator}${this.rule}` - readonly compiledCondition = `${this.compiledActual} ${this.comparator} ${this.numericLimit}` - readonly compiledNegation = `${this.compiledActual} ${ + readonly comparator: RelativeComparator = compileComparator( + this.kind, + this.exclusive + ) + readonly numericLimit: number = this.rule.valueOf() + readonly expression: string = `${this.comparator}${this.rule}` + readonly compiledCondition: string = `${this.compiledActual} ${this.comparator} ${this.numericLimit}` + readonly compiledNegation: string = `${this.compiledActual} ${ negatedComparators[this.comparator] } ${this.numericLimit}` // we need to compute stringLimit before errorContext, which references it // transitively through description for date bounds - readonly stringLimit = + readonly stringLimit: string = this.boundOperandKind === "date" ? dateLimitToString(this.numericLimit) : `${this.numericLimit}` @@ -73,13 +75,13 @@ export interface BaseRangeInner extends BaseMeta { readonly exclusive?: true } -export type LimitSchemaValue = +export type LimitRootValue = kind extends "before" | "after" ? Date | number | string : number export type LimitInnerValue = kind extends "before" | "after" ? Date : number -export interface BaseNormalizedRangeSchema extends BaseMeta { +export interface BaseNormalizedRangeRoot extends BaseMeta { readonly exclusive?: boolean } @@ -90,24 +92,30 @@ export type RelativeComparator = { upper: "<" | "<=" }[kind] -export const negatedComparators = { +const negatedComparators = { "<": ">=", "<=": ">", ">": "<=", ">=": "<" } as const satisfies Record -export const boundKindPairsByLower = { +export const boundKindPairsByLower: BoundKindPairsByLower = { min: "max", minLength: "maxLength", after: "before" -} as const satisfies PartialRecord - -type BoundKindPairsByLower = typeof boundKindPairsByLower +} -export const boundKindPairsByUpper = invert(boundKindPairsByLower) +type BoundKindPairsByLower = { + min: "max" + minLength: "maxLength" + after: "before" +} -type BoundKindPairsByUpper = typeof boundKindPairsByUpper +type BoundKindPairsByUpper = { + max: "min" + maxLength: "minLength" + before: "after" +} export type pairedRangeKind = kind extends LowerBoundKind ? BoundKindPairsByLower[kind] @@ -125,30 +133,42 @@ export type NumericallyBoundable = string | number | array export type Boundable = NumericallyBoundable | Date -export const parseExclusiveKey: KeyDefinitions["exclusive"] = +export const parseExclusiveKey: KeySchemainitions["exclusive"] = { // omit key with value false since it is the default parse: (flag: boolean) => flag || undefined } -export const parseDateLimit = (limit: LimitSchemaValue): Date => +export const parseDateLimit = (limit: LimitRootValue): Date => typeof limit === "string" || typeof limit === "number" ? new Date(limit) : limit -export const operandKindsByBoundKind = { +type OperandKindsByBoundKind = satisfy< + Record, + { + min: "value" + max: "value" + minLength: "length" + maxLength: "length" + after: "date" + before: "date" + } +> + +const operandKindsByBoundKind: OperandKindsByBoundKind = { min: "value", max: "value", minLength: "length", maxLength: "length", after: "date", before: "date" -} as const satisfies Record +} as const export const compileComparator = ( kind: RangeKind, exclusive: boolean | undefined -) => +): RelativeComparator => `${isKeyOf(kind, boundKindPairsByLower) ? ">" : "<"}${ exclusive ? "" : "=" }` as const @@ -159,7 +179,7 @@ export type LengthBoundableData = string | array export type DateRangeKind = "before" | "after" -export const dateLimitToString = (limit: LimitSchemaValue): string => +export const dateLimitToString = (limit: LimitRootValue): string => typeof limit === "string" ? limit : new Date(limit).toLocaleString() export const writeUnboundableMessage = ( diff --git a/ark/schema/refinements/regex.ts b/ark/schema/refinements/regex.ts new file mode 100644 index 0000000000..7d8ff8a031 --- /dev/null +++ b/ark/schema/refinements/regex.ts @@ -0,0 +1,66 @@ +import { RawPrimitiveConstraint } from "../constraint.js" +import type { BaseRoot } from "../roots/root.js" +import type { BaseMeta, declareNode } from "../shared/declare.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" + +export interface RegexInner extends BaseMeta { + readonly rule: string + readonly flags?: string +} + +export type NormalizedRegexSchema = RegexInner + +export type RegexSchema = NormalizedRegexSchema | string | RegExp + +export interface RegexDeclaration + extends declareNode<{ + kind: "regex" + schema: RegexSchema + normalizedSchema: NormalizedRegexSchema + inner: RegexInner + intersectionIsOpen: true + prerequisite: string + errorContext: RegexInner + }> {} + +export const regexImplementation: nodeImplementationOf = + implementNode({ + kind: "regex", + collapsibleKey: "rule", + keys: { + rule: {}, + flags: {} + }, + normalize: schema => + typeof schema === "string" ? { rule: schema } + : schema instanceof RegExp ? + schema.flags ? + { rule: schema.source, flags: schema.flags } + : { rule: schema.source } + : schema, + hasAssociatedError: true, + intersectionIsOpen: true, + defaults: { + description: node => `matched by ${node.rule}` + }, + intersections: { + // for now, non-equal regex are naively intersected: + // https://github.com/arktypeio/arktype/issues/853 + regex: () => null + } + }) + +export class RegexNode extends RawPrimitiveConstraint { + readonly instance: RegExp = new RegExp(this.rule, this.flags) + readonly expression: string = `${this.instance}` + traverseAllows: (string: string) => boolean = this.instance.test.bind( + this.instance + ) + + readonly compiledCondition: string = `${this.expression}.test(data)` + readonly compiledNegation: string = `!${this.compiledCondition}` + readonly impliedBasis: BaseRoot = this.$.keywords.string.raw +} diff --git a/ark/schema/roots/alias.ts b/ark/schema/roots/alias.ts new file mode 100644 index 0000000000..7d07b079cb --- /dev/null +++ b/ark/schema/roots/alias.ts @@ -0,0 +1,105 @@ +import { append, cached } from "@arktype/util" +import type { RawRootScope } from "../scope.js" +import type { NodeCompiler } from "../shared/compile.js" +import type { BaseMeta, declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import { intersectNodes } from "../shared/intersections.js" +import type { TraverseAllows, TraverseApply } from "../shared/traversal.js" +import { BaseRoot, type RawRootDeclaration } from "./root.js" +import { defineRightwardIntersections } from "./utils.js" + +export interface AliasInner extends BaseMeta { + readonly alias: alias + readonly resolve?: () => BaseRoot +} + +export type AliasSchema = + | `$${alias}` + | AliasInner + +export interface AliasDeclaration + extends declareNode<{ + kind: "alias" + schema: AliasSchema + normalizedSchema: AliasInner + inner: AliasInner + }> {} + +export class AliasNode extends BaseRoot { + readonly expression: string = this.alias + + @cached + get resolution(): BaseRoot { + return this.resolve?.() ?? this.$.resolveRoot(this.alias) + } + + rawKeyOf(): BaseRoot { + return this.resolution.keyof() + } + + traverseAllows: TraverseAllows = (data, ctx) => { + const seen = ctx.seen[this.id] + if (seen?.includes(data as object)) return true + ctx.seen[this.id] = append(seen, data) + return this.resolution.traverseAllows(data, ctx) + } + + traverseApply: TraverseApply = (data, ctx) => { + const seen = ctx.seen[this.id] + if (seen?.includes(data as object)) return + ctx.seen[this.id] = append(seen, data) + this.resolution.traverseApply(data, ctx) + } + + compile(js: NodeCompiler): void { + js.if(`ctx.seen.${this.id}?.includes(data)`, () => js.return(true)) + js.line(`ctx.seen.${this.id} ??= []`).line(`ctx.seen.${this.id}.push(data)`) + js.return(js.invoke(this.resolution)) + } +} + +export const normalizeAliasSchema = (schema: AliasSchema): AliasInner => + typeof schema === "string" ? { alias: schema.slice(1) } : schema + +export const aliasImplementation: nodeImplementationOf = + implementNode({ + kind: "alias", + hasAssociatedError: false, + collapsibleKey: "alias", + keys: { + alias: { + serialize: schema => `$${schema}` + }, + resolve: {} + }, + normalize: normalizeAliasSchema, + defaults: { + description: node => node.alias + }, + intersections: { + alias: (l, r, ctx) => + ctx.$.lazilyResolve( + () => + neverIfDisjoint( + intersectNodes(l.resolution, r.resolution, ctx), + ctx.$ + ), + `${l.alias}${ctx.pipe ? "|>" : "&"}${r.alias}` + ), + ...defineRightwardIntersections("alias", (l, r, ctx) => + ctx.$.lazilyResolve( + () => neverIfDisjoint(intersectNodes(l.resolution, r, ctx), ctx.$), + `${l.alias}${ctx.pipe ? "|>" : "&"}${r.alias}` + ) + ) + } + }) + +const neverIfDisjoint = ( + result: BaseRoot | Disjoint, + $: RawRootScope +): BaseRoot => (result instanceof Disjoint ? $.keywords.never.raw : result) diff --git a/ark/schema/schemas/basis.ts b/ark/schema/roots/basis.ts similarity index 75% rename from ark/schema/schemas/basis.ts rename to ark/schema/roots/basis.ts index 981344b205..6c811d9db2 100644 --- a/ark/schema/schemas/basis.ts +++ b/ark/schema/roots/basis.ts @@ -1,18 +1,18 @@ -import type { Key } from "@arktype/util" +import type { array, Key } from "@arktype/util" -import { RawSchema, type RawSchemaDeclaration } from "../schema.js" import type { NodeCompiler } from "../shared/compile.js" import { compileErrorContext } from "../shared/implement.js" import type { TraverseApply } from "../shared/traversal.js" +import { BaseRoot, type RawRootDeclaration } from "./root.js" export abstract class RawBasis< - d extends RawSchemaDeclaration = RawSchemaDeclaration -> extends RawSchema { + d extends RawRootDeclaration = RawRootDeclaration +> extends BaseRoot { abstract compiledCondition: string abstract compiledNegation: string - abstract literalKeys: Key[] + abstract literalKeys: array - rawKeyOf(): RawSchema { + rawKeyOf(): BaseRoot { return this.$.units(this.literalKeys) } diff --git a/ark/schema/schemas/discriminate.ts b/ark/schema/roots/discriminate.ts similarity index 94% rename from ark/schema/schemas/discriminate.ts rename to ark/schema/roots/discriminate.ts index b7e7c28013..95af5416c0 100644 --- a/ark/schema/schemas/discriminate.ts +++ b/ark/schema/roots/discriminate.ts @@ -151,10 +151,10 @@ export const discriminate = ( return discriminant } -// TODO: if deeply includes morphs? -export const writeUndiscriminableMorphUnionMessage = ( - path: path -) => - `${ - path === "/" ? "A" : `At ${path}, a` - } union including one or more morphs must be discriminable` as const +// // TODO: if deeply includes morphs? +// const writeUndiscriminableMorphUnionMessage = ( +// path: path +// ) => +// `${ +// path === "/" ? "A" : `At ${path}, a` +// } union including one or more morphs must be discriminable` as const diff --git a/ark/schema/roots/domain.ts b/ark/schema/roots/domain.ts new file mode 100644 index 0000000000..4a204f71ae --- /dev/null +++ b/ark/schema/roots/domain.ts @@ -0,0 +1,72 @@ +import { + type Key, + type NonEnumerableDomain, + type array, + domainDescriptions, + domainOf, + getBaseDomainKeys +} from "@arktype/util" +import type { BaseMeta, declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import type { TraverseAllows } from "../shared/traversal.js" +import { RawBasis } from "./basis.js" + +export interface DomainInner< + domain extends NonEnumerableDomain = NonEnumerableDomain +> extends BaseMeta { + readonly domain: domain +} + +export type DomainSchema< + // only domains with an infinite number of values are allowed as bases + domain extends NonEnumerableDomain = NonEnumerableDomain +> = domain | DomainInner + +export interface DomainDeclaration + extends declareNode<{ + kind: "domain" + schema: DomainSchema + normalizedSchema: DomainInner + inner: DomainInner + errorContext: DomainInner + }> {} + +export class DomainNode extends RawBasis { + traverseAllows: TraverseAllows = data => domainOf(data) === this.domain + + readonly compiledCondition: string = + this.domain === "object" ? + `((typeof data === "object" && data !== null) || typeof data === "function")` + : `typeof data === "${this.domain}"` + + readonly compiledNegation: string = + this.domain === "object" ? + `((typeof data !== "object" || data === null) && typeof data !== "function")` + : `typeof data !== "${this.domain}"` + + readonly expression: string = this.domain + readonly literalKeys: array = getBaseDomainKeys(this.domain) +} + +export const domainImplementation: nodeImplementationOf = + implementNode({ + kind: "domain", + hasAssociatedError: true, + collapsibleKey: "domain", + keys: { + domain: {} + }, + normalize: schema => + typeof schema === "string" ? { domain: schema } : schema, + defaults: { + description: node => domainDescriptions[node.domain], + actual: data => (typeof data === "boolean" ? `${data}` : domainOf(data)) + }, + intersections: { + domain: (l, r) => Disjoint.from("domain", l, r) + } + }) diff --git a/ark/schema/roots/intersection.ts b/ark/schema/roots/intersection.ts new file mode 100644 index 0000000000..b1ffa10df5 --- /dev/null +++ b/ark/schema/roots/intersection.ts @@ -0,0 +1,397 @@ +import { + flatMorph, + hasDomain, + isEmptyObject, + isKeyOf, + omit, + pick, + throwParseError, + type array, + type listable, + type mutable, + type show +} from "@arktype/util" +import { + constraintKeyParser, + flattenConstraints, + intersectConstraints +} from "../constraint.js" +import type { + Inner, + MutableInner, + Node, + NodeSchema, + Prerequisite +} from "../kinds.js" +import type { PredicateNode } from "../predicate.js" +import type { NodeCompiler } from "../shared/compile.js" +import { metaKeys, type BaseMeta, type declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import type { ArkError } from "../shared/errors.js" +import { + implementNode, + structureKeys, + type ConstraintKind, + type IntersectionContext, + type OpenNodeKind, + type RefinementKind, + type StructuralKind, + type nodeImplementationOf +} from "../shared/implement.js" +import { intersectNodes } from "../shared/intersections.js" +import type { TraverseAllows, TraverseApply } from "../shared/traversal.js" +import { hasArkKind, isNode } from "../shared/utils.js" +import type { NormalizedSequenceSchema } from "../structure/sequence.js" +import type { + StructureNode, + StructureSchema, + UndeclaredKeyBehavior +} from "../structure/structure.js" +import type { DomainNode, DomainSchema } from "./domain.js" +import type { ProtoNode, ProtoSchema } from "./proto.js" +import { BaseRoot } from "./root.js" +import { defineRightwardIntersections } from "./utils.js" + +export type IntersectionBasisKind = "domain" | "proto" + +export type IntersectionChildKind = IntersectionBasisKind | ConstraintKind + +export type RefinementsInner = { + [k in RefinementKind]?: intersectionChildInnerValueOf +} + +export interface IntersectionInner extends BaseMeta, RefinementsInner { + domain?: DomainNode + proto?: ProtoNode + structure?: StructureNode + predicate?: array +} + +export type MutableIntersectionInner = MutableInner<"intersection"> + +export type NormalizedIntersectionSchema = Omit< + IntersectionSchema, + StructuralKind | "undeclared" +> + +export type IntersectionSchema = show< + BaseMeta & { + domain?: DomainSchema + proto?: ProtoSchema + } & conditionalRootOf +> + +export type IntersectionDeclaration = declareNode<{ + kind: "intersection" + schema: IntersectionSchema + normalizedSchema: NormalizedIntersectionSchema + inner: IntersectionInner + reducibleTo: "intersection" | IntersectionBasisKind + errorContext: { + errors: readonly ArkError[] + } + childKind: IntersectionChildKind +}> + +export class IntersectionNode extends BaseRoot { + basis: Node | null = this.domain ?? this.proto ?? null + + refinements: array> = this.children.filter( + (node): node is Node => node.isRefinement() + ) + + expression: string = + this.structure?.expression || + this.children.map(node => node.nestableExpression).join(" & ") || + "unknown" + + traverseAllows: TraverseAllows = (data, ctx) => + this.children.every(child => child.traverseAllows(data as never, ctx)) + + traverseApply: TraverseApply = (data, ctx) => { + const errorCount = ctx.currentErrorCount + if (this.basis) { + this.basis.traverseApply(data, ctx) + if (ctx.currentErrorCount > errorCount) return + } + if (this.refinements.length) { + for (let i = 0; i < this.refinements.length - 1; i++) { + this.refinements[i].traverseApply(data as never, ctx) + if (ctx.failFast && ctx.currentErrorCount > errorCount) return + } + this.refinements.at(-1)!.traverseApply(data as never, ctx) + if (ctx.currentErrorCount > errorCount) return + } + if (this.structure) { + this.structure.traverseApply(data as never, ctx) + if (ctx.currentErrorCount > errorCount) return + } + if (this.predicate) { + for (let i = 0; i < this.predicate.length - 1; i++) { + this.predicate[i].traverseApply(data as never, ctx) + if (ctx.failFast && ctx.currentErrorCount > errorCount) return + } + this.predicate.at(-1)!.traverseApply(data as never, ctx) + } + } + + compile(js: NodeCompiler): void { + if (js.traversalKind === "Allows") { + this.children.forEach(child => js.check(child)) + js.return(true) + return + } + + js.initializeErrorCount() + + if (this.basis) { + js.check(this.basis) + // we only have to return conditionally if this is not the last check + if (this.children.length > 1) js.returnIfFail() + } + if (this.refinements.length) { + for (let i = 0; i < this.refinements.length - 1; i++) { + js.check(this.refinements[i]) + js.returnIfFailFast() + } + js.check(this.refinements.at(-1)!) + if (this.structure || this.predicate) js.returnIfFail() + } + if (this.structure) { + js.check(this.structure) + if (this.predicate) js.returnIfFail() + } + if (this.predicate) { + for (let i = 0; i < this.predicate.length - 1; i++) { + js.check(this.predicate[i]) + // since predicates can be chained, we have to fail immediately + // if one fails + js.returnIfFail() + } + js.check(this.predicate.at(-1)!) + } + } + + rawKeyOf(): BaseRoot { + return ( + this.basis ? + this.structure ? + this.basis.rawKeyOf().or(this.structure.keyof()) + : this.basis.rawKeyOf() + : this.structure?.keyof() ?? this.$.keywords.never.raw + ) + } +} + +const intersectIntersections = ( + l: IntersectionInner, + r: IntersectionInner, + ctx: IntersectionContext +): BaseRoot | Disjoint => { + // avoid treating adding instance keys as keys of lRoot, rRoot + if (hasArkKind(l, "root") && l.hasKind("intersection")) + return intersectIntersections(l.inner, r, ctx) + if (hasArkKind(r, "root") && r.hasKind("intersection")) + return intersectIntersections(l, r.inner, ctx) + + const baseInner: MutableIntersectionInner = + isEmptyObject(l) ? pick(r, metaKeys) : {} + + const lBasis = l.proto ?? l.domain + const rBasis = r.proto ?? r.domain + const basisResult = + lBasis ? + rBasis ? + (intersectNodes(lBasis, rBasis, ctx) as Node) + : lBasis + : rBasis + if (basisResult instanceof Disjoint) return basisResult + + if (basisResult) baseInner[basisResult.kind] = basisResult as never + + return intersectConstraints({ + kind: "intersection", + baseInner, + l: flattenConstraints(l), + r: flattenConstraints(r), + roots: [], + ctx + }) +} + +export const intersectionImplementation: nodeImplementationOf = + implementNode({ + kind: "intersection", + hasAssociatedError: true, + normalize: rawSchema => { + if (isNode(rawSchema)) return rawSchema + const { structure, ...schema } = rawSchema + const hasRootStructureKey = !!structure + const normalizedStructure = (structure as mutable) ?? {} + const normalized = flatMorph(schema, (k, v) => { + if (isKeyOf(k, structureKeys)) { + if (hasRootStructureKey) { + throwParseError( + `Flattened structure key ${k} cannot be specified alongside a root 'structure' key.` + ) + } + normalizedStructure[k] = v as never + return [] + } + return [k, v] + }) as mutable + if (!isEmptyObject(normalizedStructure)) + normalized.structure = normalizedStructure + return normalized + }, + finalizeJson: ({ structure, ...rest }) => + hasDomain(structure, "object") ? { ...structure, ...rest } : rest, + keys: { + domain: { + child: true, + parse: (schema, ctx) => ctx.$.node("domain", schema) + }, + proto: { + child: true, + parse: (schema, ctx) => ctx.$.node("proto", schema) + }, + structure: { + child: true, + parse: (schema, ctx) => ctx.$.node("structure", schema), + serialize: node => { + if (!node.sequence?.minLength) return node.collapsibleJson + const { sequence, ...structureJson } = node.collapsibleJson as any + const { minVariadicLength, ...sequenceJson } = + sequence as NormalizedSequenceSchema + const collapsibleSequenceJson = + sequenceJson.variadic && Object.keys(sequenceJson).length === 1 ? + sequenceJson.variadic + : sequenceJson + return { ...structureJson, sequence: collapsibleSequenceJson } + } + }, + divisor: { + child: true, + parse: constraintKeyParser("divisor") + }, + max: { + child: true, + parse: constraintKeyParser("max") + }, + min: { + child: true, + parse: constraintKeyParser("min") + }, + maxLength: { + child: true, + parse: constraintKeyParser("maxLength") + }, + minLength: { + child: true, + parse: constraintKeyParser("minLength") + }, + exactLength: { + child: true, + parse: constraintKeyParser("exactLength") + }, + before: { + child: true, + parse: constraintKeyParser("before") + }, + after: { + child: true, + parse: constraintKeyParser("after") + }, + regex: { + child: true, + parse: constraintKeyParser("regex") + }, + predicate: { + child: true, + parse: constraintKeyParser("predicate") + } + }, + // leverage reduction logic from intersection and identity to ensure initial + // parse result is reduced + reduce: (inner, $) => + // we cast union out of the result here since that only occurs when intersecting two sequences + // that cannot occur when reducing a single intersection schema using unknown + intersectIntersections({}, inner, { + $, + invert: false, + pipe: false + }) as Node<"intersection" | IntersectionBasisKind>, + defaults: { + description: node => + node.children.length === 0 ? + "unknown" + : node.structure?.description ?? + node.children.map(child => child.description).join(" and "), + expected: source => + ` β€’ ${source.errors.map(e => e.expected).join("\n β€’ ")}`, + problem: ctx => `must be...\n${ctx.expected}` + }, + intersections: { + intersection: (l, r, ctx) => { + return intersectIntersections(l, r, ctx) + }, + ...defineRightwardIntersections("intersection", (l, r, ctx) => { + // if l is unknown, return r + if (l.children.length === 0) return r + + const basis = l.basis ? intersectNodes(l.basis, r, ctx) : r + + return ( + basis instanceof Disjoint ? basis + : l?.basis?.equals(basis) ? + // if the basis doesn't change, return the original intesection + l + // given we've already precluded l being unknown, the result must + // be an intersection with the new basis result integrated + : l.$.node( + "intersection", + Object.assign(omit(l.inner, metaKeys), { + [basis.kind]: basis + }), + { prereduced: true } + ) + ) + }) + } + }) + +export type ConditionalTerminalIntersectionRoot = { + undeclared?: UndeclaredKeyBehavior +} + +type ConditionalTerminalIntersectionKey = + keyof ConditionalTerminalIntersectionRoot + +type ConditionalIntersectionKey = + | ConstraintKind + | ConditionalTerminalIntersectionKey + +export type constraintKindOf = { + [k in ConstraintKind]: t extends Prerequisite ? k : never +}[ConstraintKind] + +type conditionalIntersectionKeyOf = + | constraintKindOf + | (t extends object ? "undeclared" : never) + +// not sure why explicitly allowing Inner is necessary in these cases, +// but remove if it can be removed without creating type errors +type intersectionChildRootValueOf = + k extends OpenNodeKind ? listable | Inner> + : NodeSchema | Inner + +type conditionalRootValueOfKey = + k extends IntersectionChildKind ? intersectionChildRootValueOf + : ConditionalTerminalIntersectionRoot[k & ConditionalTerminalIntersectionKey] + +type intersectionChildInnerValueOf = + k extends OpenNodeKind ? readonly Node[] : Node + +export type conditionalRootOf = { + [k in conditionalIntersectionKeyOf]?: conditionalRootValueOfKey +} diff --git a/ark/schema/roots/morph.ts b/ark/schema/roots/morph.ts new file mode 100644 index 0000000000..bfdde87d9d --- /dev/null +++ b/ark/schema/roots/morph.ts @@ -0,0 +1,305 @@ +import { + arrayFrom, + registeredReference, + throwParseError, + type BuiltinObjectKind, + type BuiltinObjects, + type Primitive, + type anyOrNever, + type array, + type listable, + type show +} from "@arktype/util" +import type { of } from "../ast.js" +import type { type } from "../inference.js" +import type { Node, NodeSchema, RootSchema } from "../kinds.js" +import type { StaticArkOption } from "../scope.js" +import type { NodeCompiler } from "../shared/compile.js" +import type { BaseMeta, declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import type { ArkError, ArkErrors } from "../shared/errors.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import { intersectNodes, type inferPipe } from "../shared/intersections.js" +import type { + TraversalContext, + TraverseAllows, + TraverseApply +} from "../shared/traversal.js" +import type { DefaultableAst } from "../structure/optional.js" +import { BaseRoot, type schemaKindRightOf } from "./root.js" +import { defineRightwardIntersections } from "./utils.js" + +export type MorphInputKind = schemaKindRightOf<"morph"> + +const morphInputKinds: array = [ + "intersection", + "unit", + "domain", + "proto" +] + +export type MorphInputNode = Node + +export type MorphInputSchema = NodeSchema + +export type Morph = (In: i, ctx: TraversalContext) => o + +export type Out = ["=>", o] + +export type MorphAst = (In: i) => Out + +export interface MorphInner extends BaseMeta { + readonly in: MorphInputNode + readonly out?: BaseRoot + readonly morphs: readonly Morph[] +} + +export interface MorphSchema extends BaseMeta { + readonly in: MorphInputSchema + readonly out?: RootSchema | undefined + readonly morphs: listable +} + +export interface MorphDeclaration + extends declareNode<{ + kind: "morph" + schema: MorphSchema + normalizedSchema: MorphSchema + inner: MorphInner + childKind: MorphInputKind + }> {} + +export const morphImplementation: nodeImplementationOf = + implementNode({ + kind: "morph", + hasAssociatedError: false, + keys: { + in: { + child: true, + parse: (schema, ctx) => ctx.$.node(morphInputKinds, schema) + }, + out: { + child: true, + parse: (schema, ctx) => { + if (schema === undefined) return + const out = ctx.$.schema(schema) + return out.kind === "intersection" && out.children.length === 0 ? + // ignore unknown as an output validator + undefined + : out + } + }, + morphs: { + parse: arrayFrom, + serialize: morphs => morphs.map(registeredReference) + } + }, + normalize: schema => schema, + defaults: { + description: node => + `a morph from ${node.in.description} to ${node.out?.description ?? "unknown"}` + }, + intersections: { + morph: (l, r, ctx) => { + if (l.morphs.some((morph, i) => morph !== r.morphs[i])) + // TODO: check in for union reduction + return throwParseError("Invalid intersection of morphs") + const inTersection = intersectNodes(l.in, r.in, ctx) + if (inTersection instanceof Disjoint) return inTersection + const out = + l.out ? + r.out ? + intersectNodes(l.out, r.out, ctx) + : l.out + : r.out + if (out instanceof Disjoint) return out + // in case from is a union, we need to distribute the branches + // to can be a union as any schema is allowed + return ctx.$.schema( + inTersection.branches.map(inBranch => + ctx.$.node("morph", { + morphs: l.morphs, + in: inBranch, + out + }) + ) + ) + }, + ...defineRightwardIntersections("morph", (l, r, ctx) => { + const inTersection = intersectNodes(l.in, r, ctx) + return ( + inTersection instanceof Disjoint ? inTersection + : inTersection.kind === "union" ? + ctx.$.node( + "union", + inTersection.branches.map(branch => ({ + ...l.inner, + in: branch + })) + ) + : ctx.$.node("morph", { + ...l.inner, + in: inTersection + }) + ) + }) + } + }) + +export class MorphNode extends BaseRoot { + serializedMorphs: string[] = (this.json as any).morphs + compiledMorphs = `[${this.serializedMorphs}]` + outValidator: TraverseApply | null = this.inner.out?.traverseApply ?? null + + private queueArgs: Parameters = [ + this.morphs, + this.outValidator ? { outValidator: this.outValidator } : {} + ] + + private queueArgsReference = registeredReference(this.queueArgs) + + traverseAllows: TraverseAllows = (data, ctx) => + this.in.traverseAllows(data, ctx) + + traverseApply: TraverseApply = (data, ctx) => { + ctx.queueMorphs(...this.queueArgs) + this.in.traverseApply(data, ctx) + } + + expression = `(In: ${this.in.expression}) => Out<${this.out?.expression ?? "unknown"}>` + + compile(js: NodeCompiler): void { + if (js.traversalKind === "Allows") { + js.return(js.invoke(this.in)) + return + } + js.line(`ctx.queueMorphs(...${this.queueArgsReference})`) + js.line(js.invoke(this.in)) + } + + override get in(): BaseRoot { + return this.inner.in + } + + override get out(): BaseRoot { + return (this.inner.out?.out as BaseRoot) ?? this.$.keywords.unknown.raw + } + + rawKeyOf(): BaseRoot { + return this.in.rawKeyOf() + } +} + +export type inferPipes = + pipes extends [infer head extends Morph, ...infer tail extends Morph[]] ? + inferPipes< + head extends type.cast ? inferPipe + : (In: distillConstrainableIn) => Out>, + tail + > + : t + +export type inferMorphOut = Exclude< + ReturnType, + ArkError | ArkErrors +> + +export type distillIn = + includesMorphs extends true ? _distill : t + +export type distillOut = + includesMorphs extends true ? _distill : t + +export type distillConstrainableIn = + includesMorphs extends true ? _distill : t + +export type distillConstrainableOut = + includesMorphs extends true ? _distill : t + +export type includesMorphs = + [t, _distill, t, _distill] extends ( + [_distill, t, _distill, t] + ) ? + false + : true + +type _distill< + t, + io extends "in" | "out", + distilledKind extends "base" | "constrainable" +> = + t extends TerminallyInferredObjectKind | Primitive ? t + : unknown extends t ? unknown + : t extends MorphAst ? + io extends "in" ? + _distill + : _distill + : t extends DefaultableAst ? _distill + : t extends of ? + distilledKind extends "base" ? + _distill + : t + : t extends array ? distillArray + : // we excluded this from TerminallyInferredObjectKind so that those types could be + // inferred before checking morphs/defaults, which extend Function + t extends Function ? t + : // avoid recursing into classes with private props etc. + { [k in keyof t]: t[k] } extends t ? + io extends "in" ? + show< + { + [k in keyof t as k extends defaultableKeyOf ? never : k]: _distill< + t[k], + io, + distilledKind + > + } & { [k in defaultableKeyOf]?: _distill } + > + : { + [k in keyof t]: _distill + } + : t + +type defaultableKeyOf = { + [k in keyof t]: [t[k]] extends [anyOrNever] ? never + : t[k] extends DefaultableAst ? k + : never +}[keyof t] + +type distillArray< + t extends array, + io extends "in" | "out", + constraints extends "base" | "constrainable", + prefix extends array +> = + t extends readonly [infer head, ...infer tail] ? + distillArray< + tail, + io, + constraints, + [...prefix, _distill] + > + : [...prefix, ...distillPostfix] + +type distillPostfix< + t extends array, + io extends "in" | "out", + constraints extends "base" | "constrainable", + postfix extends array = [] +> = + t extends readonly [...infer init, infer last] ? + distillPostfix< + init, + io, + constraints, + [_distill, ...postfix] + > + : [...{ [i in keyof t]: _distill }, ...postfix] + +/** Objects we don't want to expand during inference like Date or Promise */ +type TerminallyInferredObjectKind = + | StaticArkOption<"preserve"> + | BuiltinObjects[Exclude] diff --git a/ark/schema/roots/proto.ts b/ark/schema/roots/proto.ts new file mode 100644 index 0000000000..4da828d269 --- /dev/null +++ b/ark/schema/roots/proto.ts @@ -0,0 +1,100 @@ +import { + type BuiltinObjectKind, + type Constructor, + type Key, + type array, + builtinConstructors, + constructorExtends, + getExactBuiltinConstructorName, + objectKindDescriptions, + objectKindOrDomainOf, + prototypeKeysOf +} from "@arktype/util" +import type { BaseMeta, declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import { + defaultValueSerializer, + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import type { TraverseAllows } from "../shared/traversal.js" +import { RawBasis } from "./basis.js" + +export interface ProtoInner + extends BaseMeta { + readonly proto: proto +} + +export type NormalizedProtoSchema = + ProtoInner + +export type ProtoReference = Constructor | BuiltinObjectKind + +export interface ExpandedProtoSchema< + proto extends ProtoReference = ProtoReference +> extends BaseMeta { + readonly proto: proto +} + +export type ProtoSchema = + | proto + | ExpandedProtoSchema + +export interface ProtoDeclaration + extends declareNode<{ + kind: "proto" + schema: ProtoSchema + normalizedSchema: NormalizedProtoSchema + inner: ProtoInner + errorContext: ProtoInner + }> {} + +export const protoImplementation: nodeImplementationOf = + implementNode({ + kind: "proto", + hasAssociatedError: true, + collapsibleKey: "proto", + keys: { + proto: { + serialize: ctor => + getExactBuiltinConstructorName(ctor) ?? defaultValueSerializer(ctor) + } + }, + normalize: schema => + typeof schema === "string" ? { proto: builtinConstructors[schema] } + : typeof schema === "function" ? { proto: schema } + : typeof schema.proto === "string" ? + { ...schema, proto: builtinConstructors[schema.proto] } + : (schema as ExpandedProtoSchema), + defaults: { + description: node => + node.builtinName ? + objectKindDescriptions[node.builtinName] + : `an instance of ${node.proto.name}`, + actual: data => objectKindOrDomainOf(data) + }, + intersections: { + proto: (l, r) => + constructorExtends(l.proto, r.proto) ? l + : constructorExtends(r.proto, l.proto) ? r + : Disjoint.from("proto", l, r), + domain: (proto, domain, ctx) => + domain.domain === "object" ? + proto + : Disjoint.from("domain", ctx.$.keywords.object as never, domain) + } + }) + +export class ProtoNode extends RawBasis { + builtinName: BuiltinObjectKind | null = getExactBuiltinConstructorName( + this.proto + ) + serializedConstructor: string = (this.json as { proto: string }).proto + compiledCondition = `data instanceof ${this.serializedConstructor}` + compiledNegation = `!(${this.compiledCondition})` + literalKeys: array = prototypeKeysOf(this.proto.prototype) + + traverseAllows: TraverseAllows = data => data instanceof this.proto + expression: string = this.proto.name + readonly domain = "object" +} diff --git a/ark/schema/schema.ts b/ark/schema/roots/root.ts similarity index 63% rename from ark/schema/schema.ts rename to ark/schema/roots/root.ts index 254e35bff3..17370d8d30 100644 --- a/ark/schema/schema.ts +++ b/ark/schema/roots/root.ts @@ -1,80 +1,92 @@ import { + includes, + omit, throwParseError, type Callable, type Json, type conform } from "@arktype/util" -import type { constrain } from "./constraints/ast.js" -import type { Predicate } from "./constraints/predicate.js" +import type { constrain } from "../ast.js" import { throwInvalidOperandError, type PrimitiveConstraintKind -} from "./constraints/util.js" -import type { NodeDef, reducibleKindOf } from "./kinds.js" -import { RawNode, type Node } from "./node.js" -import type { constraintKindOf } from "./schemas/intersection.js" -import type { - Morph, - distillConstrainableIn, - distillConstrainableOut, - distillIn, - distillOut, - inferMorphOut, - inferPipes -} from "./schemas/morph.js" -import type { UnionChildKind } from "./schemas/union.js" -import type { SchemaScope } from "./scope.js" -import type { BaseMeta, RawNodeDeclaration } from "./shared/declare.js" -import { Disjoint } from "./shared/disjoint.js" -import { ArkErrors } from "./shared/errors.js" -import type { NodeKind, SchemaKind, kindRightOf } from "./shared/implement.js" +} from "../constraint.js" +import type { Node, NodeSchema, reducibleKindOf } from "../kinds.js" +import { BaseNode } from "../node.js" +import type { Predicate } from "../predicate.js" +import type { RootScope } from "../scope.js" +import type { BaseMeta, RawNodeDeclaration } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import { ArkErrors } from "../shared/errors.js" +import { + structuralKinds, + type NodeKind, + type RootKind, + type kindRightOf +} from "../shared/implement.js" import { intersectNodesRoot, pipeNodesRoot, type inferIntersection -} from "./shared/intersections.js" +} from "../shared/intersections.js" import { arkKind, hasArkKind, type inferred, type internalImplementationOf -} from "./shared/utils.js" +} from "../shared/utils.js" +import type { + StructureInner, + UndeclaredKeyBehavior +} from "../structure/structure.js" +import type { constraintKindOf } from "./intersection.js" +import type { + Morph, + distillConstrainableIn, + distillConstrainableOut, + distillIn, + distillOut, + inferMorphOut, + inferPipes +} from "./morph.js" +import type { UnionChildKind } from "./union.js" -export interface RawSchemaDeclaration extends RawNodeDeclaration { - kind: SchemaKind +export interface RawRootDeclaration extends RawNodeDeclaration { + kind: RootKind } -export type UnknownSchema = Schema | RawSchema +export type UnknownRoot = Root | BaseRoot -export type TypeOnlySchemaKey = - | (keyof Schema & symbol) +export type TypeOnlyRootKey = + | (keyof Root & symbol) | "infer" | "inferIn" | "t" | "tIn" | "tOut" -export abstract class RawSchema< +export abstract class BaseRoot< /** uses -ignore rather than -expect-error because this is not an error in .d.ts * @ts-ignore allow instantiation assignment to the base type */ - out d extends RawSchemaDeclaration = RawSchemaDeclaration + out d extends RawRootDeclaration = RawRootDeclaration > - extends RawNode - implements internalImplementationOf + extends BaseNode + // don't require intersect so we can make it protected to ensure it is not called internally + implements internalImplementationOf { readonly branches: readonly Node[] = this.hasKind("union") ? this.inner.branches : [this as never]; - readonly [arkKind] = "schema" + readonly [arkKind] = "root" get raw(): this { return this } - abstract rawKeyOf(): RawSchema + abstract rawKeyOf(): BaseRoot - private _keyof: RawSchema | undefined - keyof(): RawSchema { + private _keyof: BaseRoot | undefined + keyof(): BaseRoot { if (!this._keyof) { this._keyof = this.rawKeyOf() if (this._keyof.branches.length === 0) { @@ -86,18 +98,17 @@ export abstract class RawSchema< return this._keyof as never } - // TODO: can it be enforced that this is not called internally and instead intersectNodes is used? - intersect(r: unknown): RawSchema | Disjoint { + protected intersect(r: unknown): BaseRoot | Disjoint { const rNode = this.$.parseRoot(r) return intersectNodesRoot(this, rNode, this.$) as never } - and(r: unknown): RawSchema { + and(r: unknown): BaseRoot { const result = this.intersect(r) return result instanceof Disjoint ? result.throw() : (result as never) } - or(r: unknown): RawSchema { + or(r: unknown): BaseRoot { const rNode = this.$.parseRoot(r) const branches = [...this.branches, ...(rNode.branches as any)] return this.$.schema(branches) as never @@ -109,26 +120,26 @@ export abstract class RawSchema< } // get( - // ...path: readonly (key | Schema)[] + // ...path: readonly (key | Root)[] // ): this { // return this // } - extract(r: unknown): RawSchema { + extract(r: unknown): BaseRoot { const rNode = this.$.parseRoot(r) return this.$.schema( this.branches.filter(branch => branch.extends(rNode)) ) as never } - exclude(r: UnknownSchema): RawSchema { + exclude(r: UnknownRoot): BaseRoot { const rNode = this.$.parseRoot(r) return this.$.schema( this.branches.filter(branch => !branch.extends(rNode)) ) as never } - array(): RawSchema { + array(): BaseRoot { return this.$.schema( { proto: Array, @@ -138,14 +149,14 @@ export abstract class RawSchema< ) as never } - extends(r: UnknownSchema): boolean { + extends(r: UnknownRoot): boolean { const intersection = this.intersect(r as never) return ( !(intersection instanceof Disjoint) && this.equals(intersection as never) ) } - subsumes(r: UnknownSchema): boolean { + subsumes(r: UnknownRoot): boolean { return r.extends(this as never) } @@ -163,12 +174,12 @@ export abstract class RawSchema< return this.assert(input) } - pipe(...morphs: Morph[]): RawSchema { - return morphs.reduce((acc, morph) => acc.pipeOnce(morph), this) + pipe(...morphs: Morph[]): BaseRoot { + return morphs.reduce((acc, morph) => acc.pipeOnce(morph), this) } - private pipeOnce(morph: Morph): RawSchema { - if (hasArkKind(morph, "schema")) + private pipeOnce(morph: Morph): BaseRoot { + if (hasArkKind(morph, "root")) return pipeNodesRoot(this, morph, this.$) as never if (this.hasKind("union")) { const branches = this.branches.map(node => node.pipe(morph)) @@ -181,20 +192,20 @@ export abstract class RawSchema< }) } return this.$.node("morph", { - from: this, + in: this, morphs: [morph] }) } - narrow(predicate: Predicate): RawSchema { + narrow(predicate: Predicate): BaseRoot { return this.constrain("predicate", predicate) } constrain( kind: kind, - def: NodeDef - ): RawSchema { - const constraint = this.$.node(kind, def) + schema: NodeSchema + ): BaseRoot { + const constraint = this.$.node(kind, schema) if (constraint.impliedBasis && !this.extends(constraint.impliedBasis)) { return throwInvalidOperandError( kind, @@ -211,62 +222,74 @@ export abstract class RawSchema< ) } + onUndeclaredKey(undeclared: UndeclaredKeyBehavior): BaseRoot { + return this.transform( + (kind, inner) => + kind === "structure" ? + undeclared === "ignore" ? + omit(inner as StructureInner, { undeclared: 1 }) + : { ...inner, undeclared } + : inner, + node => !includes(structuralKinds, node.kind) + ) + } + // divisibleBy< // const schema extends validateConstraintArg<"divisor", this["infer"]> - // >(schema: schema): Type, $> { + // >(schema: schema): Type, $> { // return this.rawConstrain("divisor", schema as never) as never // } // atLeast>( // schema: schema - // ): Type, $> { + // ): Type, $> { // return this.rawConstrain("min", schema as never) as never // } // atMost>( // schema: schema - // ): Type, $> { + // ): Type, $> { // return this.rawConstrain("max", schema as never) as never // } // moreThan>( // schema: schema - // ): Type, $> { + // ): Type, $> { // return this.rawConstrain("min", schema as never) as never // } // lessThan>( // schema: schema - // ): Type, $> { + // ): Type, $> { // return this.rawConstrain("max", schema as never) as never // } // atLeastLength< // const schema extends validateConstraintArg<"minLength", this["infer"]> - // >(schema: schema): Type, $> { + // >(schema: schema): Type, $> { // return this.rawConstrain("minLength", schema as never) as never // } // atMostLength< // const schema extends validateConstraintArg<"maxLength", this["infer"]> - // >(schema: schema): Type, $> { + // >(schema: schema): Type, $> { // return this.rawConstrain("maxLength", schema as never) as never // } // earlierThan< // const schema extends validateConstraintArg<"before", this["infer"]> - // >(schema: schema): Type, $> { + // >(schema: schema): Type, $> { // return this.rawConstrain("before", schema as never) as never // } // laterThan>( // schema: schema - // ): Type, $> { + // ): Type, $> { // return this.rawConstrain("after", schema as never) as never // } } -export declare abstract class BaseRoot extends Callable< +export declare abstract class InnerRoot extends Callable< (data: unknown) => distillOut | ArkErrors > { t: t @@ -279,16 +302,16 @@ export declare abstract class BaseRoot extends Callable< json: Json description: string expression: string - raw: RawSchema + raw: BaseRoot - abstract $: SchemaScope<$>; + abstract $: RootScope<$>; abstract get in(): unknown abstract get out(): unknown abstract keyof(): unknown abstract intersect(r: never): unknown | Disjoint abstract and(r: never): unknown abstract or(r: never): unknown - abstract constrain(kind: never, def: never): unknown + abstract constrain(kind: never, schema: never): unknown abstract equals(r: never): this is unknown abstract extract(r: never): unknown abstract exclude(r: never): unknown @@ -306,72 +329,72 @@ export declare abstract class BaseRoot extends Callable< describe(description: string): this + onUndeclaredKey(behavior: UndeclaredKeyBehavior): this + create(literal: this["inferIn"]): this["infer"] } // this is declared as a class internally so we can ensure all "abstract" // methods of BaseRoot are overridden, but we end up exporting it as an interface // to ensure it is not accessed as a runtime value -declare class _Schema extends BaseRoot { - $: SchemaScope<$>; +declare class _Root extends InnerRoot { + $: RootScope<$>; - get in(): Schema + get in(): Root - get out(): Schema + get out(): Root - keyof(): Schema + keyof(): Root - intersect( - r: r - ): Schema> | Disjoint + intersect(r: r): Root> | Disjoint - and(r: r): Schema> + and(r: r): Root> - or(r: r): Schema + or(r: r): Root - array(): Schema + array(): Root constrain< kind extends PrimitiveConstraintKind, - const def extends NodeDef + const schema extends NodeSchema >( kind: conform>, - def: def - ): Schema, $> + schema: schema + ): Root, $> - equals(r: Schema): this is Schema + equals(r: Root): this is Root // TODO: i/o - extract(r: Schema): Schema - exclude(r: Schema): Schema + extract(r: Root): Root + exclude(r: Root): Root // add the extra inferred intersection so that a variable of Type // can be narrowed without other branches becoming never - extends(other: Schema): this is Schema & { [inferred]?: r } + extends(other: Root): this is Root & { [inferred]?: r } - pipe>(a: a): Schema, $> + pipe>(a: a): Root, $> pipe, b extends Morph>>( a: a, b: b - ): Schema, $> + ): Root, $> pipe< a extends Morph, b extends Morph>, c extends Morph> - >(a: a, b: b, c: c): Schema, $> + >(a: a, b: b, c: c): Root, $> pipe< a extends Morph, b extends Morph>, c extends Morph>, d extends Morph> - >(a: a, b: b, c: c, d: d): Schema, $> + >(a: a, b: b, c: c, d: d): Root, $> pipe< a extends Morph, b extends Morph>, c extends Morph>, d extends Morph>, e extends Morph> - >(a: a, b: b, c: c, d: d, e: e): Schema, $> + >(a: a, b: b, c: c, d: d, e: e): Root, $> pipe< a extends Morph, b extends Morph>, @@ -386,7 +409,7 @@ declare class _Schema extends BaseRoot { d: d, e: e, f: f - ): Schema, $> + ): Root, $> pipe< a extends Morph, b extends Morph>, @@ -403,16 +426,16 @@ declare class _Schema extends BaseRoot { e: e, f: f, g: g - ): Schema, $> + ): Root, $> } -export interface Schema< +export interface Root< /** @ts-expect-error allow instantiation assignment to the base type */ out t = unknown, $ = any -> extends _Schema {} +> extends _Root {} -export type intersectSchema = +export type intersectRoot = [l, r] extends [r, l] ? l : asymmetricIntersectionOf | asymmetricIntersectionOf @@ -423,11 +446,11 @@ type asymmetricIntersectionOf = : never : never -export type schemaKindRightOf = Extract< +export type schemaKindRightOf = Extract< kindRightOf, - SchemaKind + RootKind > -export type schemaKindOrRightOf = +export type schemaKindOrRightOf = | kind | schemaKindRightOf diff --git a/ark/schema/schemas/union.ts b/ark/schema/roots/union.ts similarity index 66% rename from ark/schema/schemas/union.ts rename to ark/schema/roots/union.ts index 6c3166bdb9..d749204139 100644 --- a/ark/schema/schemas/union.ts +++ b/ark/schema/roots/union.ts @@ -1,38 +1,38 @@ -import { appendUnique, groupBy, isArray } from "@arktype/util" -import type { NodeDef } from "../kinds.js" -import type { Node } from "../node.js" -import { RawSchema } from "../schema.js" +import { appendUnique, groupBy, isArray, type array } from "@arktype/util" +import type { Node, NodeSchema } from "../kinds.js" import type { NodeCompiler } from "../shared/compile.js" import type { BaseMeta, declareNode } from "../shared/declare.js" import { Disjoint } from "../shared/disjoint.js" -import type { ArkTypeError } from "../shared/errors.js" +import type { ArkError } from "../shared/errors.js" import { implementNode, + schemaKindsRightOf, type IntersectionContext, - type SchemaKind, - schemaKindsRightOf + type RootKind, + type nodeImplementationOf } from "../shared/implement.js" import { intersectNodes, intersectNodesRoot } from "../shared/intersections.js" import type { TraverseAllows, TraverseApply } from "../shared/traversal.js" +import { BaseRoot, type schemaKindRightOf } from "./root.js" import { defineRightwardIntersections } from "./utils.js" -export type UnionChildKind = (typeof unionChildKinds)[number] +export type UnionChildKind = schemaKindRightOf<"union"> | "alias" -export const unionChildKinds = [ +const unionChildKinds: array = [ ...schemaKindsRightOf("union"), "alias" -] as const +] -export type UnionChildDef = NodeDef +export type UnionChildSchema = NodeSchema export type UnionChildNode = Node -export type UnionDef< - branches extends readonly UnionChildDef[] = readonly UnionChildDef[] -> = NormalizedUnionDef | branches +export type UnionSchema< + branches extends readonly UnionChildSchema[] = readonly UnionChildSchema[] +> = NormalizedUnionSchema | branches -export interface NormalizedUnionDef< - branches extends readonly UnionChildDef[] = readonly UnionChildDef[] +export interface NormalizedUnionSchema< + branches extends readonly UnionChildSchema[] = readonly UnionChildSchema[] > extends BaseMeta { readonly branches: branches readonly ordered?: true @@ -43,146 +43,147 @@ export interface UnionInner extends BaseMeta { readonly ordered?: true } -export type UnionDeclaration = declareNode<{ - kind: "union" - def: UnionDef - normalizedDef: NormalizedUnionDef - inner: UnionInner - errorContext: { - errors: readonly ArkTypeError[] - } - reducibleTo: SchemaKind - childKind: UnionChildKind -}> - -export const unionImplementation = implementNode({ - kind: "union", - hasAssociatedError: true, - collapsibleKey: "branches", - keys: { - ordered: {}, - branches: { - child: true, - parse: (def, ctx) => { - const branches = def.map(branch => ctx.$.node(unionChildKinds, branch)) - - if (!ctx.def.ordered) - branches.sort((l, r) => (l.innerHash < r.innerHash ? -1 : 1)) - - return branches - } +export interface UnionDeclaration + extends declareNode<{ + kind: "union" + schema: UnionSchema + normalizedSchema: NormalizedUnionSchema + inner: UnionInner + errorContext: { + errors: readonly ArkError[] } - }, - normalize: def => (isArray(def) ? { branches: def } : def), - reduce: (inner, $) => { - const reducedBranches = reduceBranches(inner) - if (reducedBranches.length === 1) return reducedBranches[0] - - if (reducedBranches.length === inner.branches.length) return - - return $.node( - "union", - { - ...inner, - branches: reducedBranches - }, - { prereduced: true } - ) - }, - defaults: { - description: node => { - return describeBranches(node.branches.map(branch => branch.description)) - }, - expected: ctx => { - const byPath = groupBy(ctx.errors, "propString") as Record< - string, - ArkTypeError[] - > - const pathDescriptions = Object.entries(byPath).map(([path, errors]) => { - const branchesAtPath: string[] = [] - errors.forEach(errorAtPath => - // avoid duplicate messages when multiple branches - // are invalid due to the same error - appendUnique(branchesAtPath, errorAtPath.expected) - ) - const expected = describeBranches(branchesAtPath) - const actual = errors.reduce( - (acc, e) => - e.actual && !acc.includes(e.actual) ? - `${acc && `${acc}, `}${e.actual}` - : acc, - "" - ) - return `${path && `${path} `}must be ${expected}${ - actual && ` (was ${actual})` - }` - }) - return describeBranches(pathDescriptions) - }, - problem: ctx => ctx.expected, - message: ctx => ctx.problem - }, - intersections: { - union: (l, r, ctx) => { - if ( - (l.branches.length === 0 || r.branches.length === 0) && - l.branches.length !== r.branches.length - ) { - // if exactly one operand is never, we can use it to discriminate based on presence - return Disjoint.from( - "presence", - l.branches.length !== 0, - r.branches.length !== 0 - ) - } - let resultBranches: readonly UnionChildNode[] | Disjoint - if (l.ordered) { - if (r.ordered) return Disjoint.from("indiscriminableMorphs", l, r) - - resultBranches = intersectBranches(r.branches, l.branches, ctx) - if (resultBranches instanceof Disjoint) resultBranches.invert() - } else resultBranches = intersectBranches(l.branches, r.branches, ctx) + reducibleTo: RootKind + childKind: UnionChildKind + }> {} + +export const unionImplementation: nodeImplementationOf = + implementNode({ + kind: "union", + hasAssociatedError: true, + collapsibleKey: "branches", + keys: { + ordered: {}, + branches: { + child: true, + parse: (schema, ctx) => { + const branches = schema.map(branch => + ctx.$.node(unionChildKinds, branch) + ) - if (resultBranches instanceof Disjoint) return resultBranches + if (!ctx.schema.ordered) + branches.sort((l, r) => (l.innerHash < r.innerHash ? -1 : 1)) - return ctx.$.schema( - l.ordered || r.ordered ? - { - branches: resultBranches, - ordered: true as const - } - : { branches: resultBranches } + return branches + } + } + }, + normalize: schema => (isArray(schema) ? { branches: schema } : schema), + reduce: (inner, $) => { + const reducedBranches = reduceBranches(inner) + if (reducedBranches.length === 1) return reducedBranches[0] + + if (reducedBranches.length === inner.branches.length) return + + return $.node( + "union", + { + ...inner, + branches: reducedBranches + }, + { prereduced: true } ) }, - ...defineRightwardIntersections("union", (l, r, ctx) => { - const branches = intersectBranches(l.branches, [r], ctx) - if (branches instanceof Disjoint) return branches + defaults: { + description: node => { + return describeBranches(node.branches.map(branch => branch.description)) + }, + expected: ctx => { + const byPath = groupBy(ctx.errors, "propString") as Record< + string, + ArkError[] + > + const pathDescriptions = Object.entries(byPath).map( + ([path, errors]) => { + const branchesAtPath: string[] = [] + errors.forEach(errorAtPath => + // avoid duplicate messages when multiple branches + // are invalid due to the same error + appendUnique(branchesAtPath, errorAtPath.expected) + ) + const expected = describeBranches(branchesAtPath) + const actual = errors.reduce( + (acc, e) => + e.actual && !acc.includes(e.actual) ? + `${acc && `${acc}, `}${e.actual}` + : acc, + "" + ) + return `${path && `${path} `}must be ${expected}${ + actual && ` (was ${actual})` + }` + } + ) + return describeBranches(pathDescriptions) + }, + problem: ctx => ctx.expected, + message: ctx => ctx.problem + }, + intersections: { + union: (l, r, ctx) => { + if (l.isNever !== r.isNever) { + // if exactly one operand is never, we can use it to discriminate based on presence + return Disjoint.from("presence", l, r) + } + let resultBranches: readonly UnionChildNode[] | Disjoint + if (l.ordered) { + if (r.ordered) return Disjoint.from("indiscriminableMorphs", l, r) + + resultBranches = intersectBranches(r.branches, l.branches, ctx) + if (resultBranches instanceof Disjoint) resultBranches.invert() + } else resultBranches = intersectBranches(l.branches, r.branches, ctx) + + if (resultBranches instanceof Disjoint) return resultBranches + + return ctx.$.schema( + l.ordered || r.ordered ? + { + branches: resultBranches, + ordered: true as const + } + : { branches: resultBranches } + ) + }, + ...defineRightwardIntersections("union", (l, r, ctx) => { + const branches = intersectBranches(l.branches, [r], ctx) + if (branches instanceof Disjoint) return branches - if (branches.length === 1) return branches[0] + if (branches.length === 1) return branches[0] - return ctx.$.schema( - l.ordered ? { branches, ordered: true } : { branches } - ) - }) - } -}) + return ctx.$.schema( + l.ordered ? { branches, ordered: true } : { branches } + ) + }) + } + }) -export class UnionNode extends RawSchema { - isBoolean = +export class UnionNode extends BaseRoot { + isNever: boolean = this.branches.length === 0 + isBoolean: boolean = this.branches.length === 2 && this.branches[0].hasUnit(false) && this.branches[1].hasUnit(true) discriminant = null - expression = - this.isBoolean ? "boolean" : ( - this.branches.map(branch => branch.nestableExpression).join(" | ") - ) + expression: string = + this.isNever ? "never" + : this.isBoolean ? "boolean" + : this.branches.map(branch => branch.nestableExpression).join(" | ") + traverseAllows: TraverseAllows = (data, ctx) => this.branches.some(b => b.traverseAllows(data, ctx)) traverseApply: TraverseApply = (data, ctx) => { - const errors: ArkTypeError[] = [] + const errors: ArkError[] = [] for (let i = 0; i < this.branches.length; i++) { ctx.pushBranch() this.branches[i].traverseApply(data, ctx) @@ -214,7 +215,7 @@ export class UnionNode extends RawSchema { } } - rawKeyOf(): RawSchema { + rawKeyOf(): BaseRoot { return this.branches.reduce( (result, branch) => result.and(branch.rawKeyOf()), this.$.keywords.unknown.raw @@ -334,9 +335,9 @@ export const intersectBranches = ( // If the corresponding r branch is identified as a subtype of an l branch, the // value at rIndex is set to null so we can avoid including previous/future // inersections in the reduced result. - const batchesByR: (RawSchema[] | null)[] = r.map(() => []) + const batchesByR: (BaseRoot[] | null)[] = r.map(() => []) for (let lIndex = 0; lIndex < l.length; lIndex++) { - let candidatesByR: { [rIndex: number]: RawSchema } = {} + let candidatesByR: { [rIndex: number]: BaseRoot } = {} for (let rIndex = 0; rIndex < r.length; rIndex++) { if (batchesByR[rIndex] === null) { // rBranch is a subtype of an lBranch and diff --git a/ark/schema/roots/unit.ts b/ark/schema/roots/unit.ts new file mode 100644 index 0000000000..76c0675966 --- /dev/null +++ b/ark/schema/roots/unit.ts @@ -0,0 +1,97 @@ +import { + domainOf, + printable, + prototypeKeysOf, + type Domain, + type JsonPrimitive, + type Key, + type array +} from "@arktype/util" +import type { BaseMeta, declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import { + defaultValueSerializer, + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import type { TraverseAllows } from "../shared/traversal.js" +import { RawBasis } from "./basis.js" +import { defineRightwardIntersections } from "./utils.js" + +export type UnitSchema = UnitInner + +export interface UnitInner extends BaseMeta { + readonly unit: value +} + +export interface UnitDeclaration + extends declareNode<{ + kind: "unit" + schema: UnitSchema + normalizedSchema: UnitSchema + inner: UnitInner + errorContext: UnitInner + }> {} + +export const unitImplementation: nodeImplementationOf = + implementNode({ + kind: "unit", + hasAssociatedError: true, + keys: { + unit: { + preserveUndefined: true, + serialize: schema => + schema instanceof Date ? + schema.toISOString() + : defaultValueSerializer(schema) + } + }, + normalize: schema => schema, + defaults: { + description: node => printable(node.unit) + }, + intersections: { + unit: (l, r) => Disjoint.from("unit", l, r), + ...defineRightwardIntersections("unit", (l, r) => + r.allows(l.unit) ? l : Disjoint.from("assignability", l.unit, r) + ) + } + }) + +export class UnitNode extends RawBasis { + compiledValue: JsonPrimitive = (this.json as any).unit + serializedValue: JsonPrimitive = + typeof this.unit === "string" || this.unit instanceof Date ? + JSON.stringify(this.compiledValue) + : this.compiledValue + literalKeys: array = prototypeKeysOf(this.unit) + + compiledCondition: string = compileEqualityCheck( + this.unit, + this.serializedValue + ) + compiledNegation: string = compileEqualityCheck( + this.unit, + this.serializedValue, + "negated" + ) + expression: string = printable(this.unit) + domain: Domain = domainOf(this.unit) + + traverseAllows: TraverseAllows = + this.unit instanceof Date ? + data => data instanceof Date && data.toISOString() === this.compiledValue + : data => data === this.unit +} + +const compileEqualityCheck = ( + unit: unknown, + serializedValue: JsonPrimitive, + negated?: "negated" +) => { + if (unit instanceof Date) { + const condition = `data instanceof Date && data.toISOString() === ${serializedValue}` + return negated ? `!(${condition})` : condition + } + return `data ${negated ? "!" : "="}== ${serializedValue}` +} diff --git a/ark/schema/roots/utils.ts b/ark/schema/roots/utils.ts new file mode 100644 index 0000000000..e449bdc054 --- /dev/null +++ b/ark/schema/roots/utils.ts @@ -0,0 +1,16 @@ +import { flatMorph } from "@arktype/util" +import { + schemaKindsRightOf, + type RootIntersection, + type RootKind +} from "../shared/implement.js" +import type { schemaKindRightOf } from "./root.js" + +export const defineRightwardIntersections = ( + kind: kind, + implementation: RootIntersection> +): { [k in schemaKindRightOf]: RootIntersection } => + flatMorph(schemaKindsRightOf(kind), (i, kind) => [ + kind, + implementation + ]) as never diff --git a/ark/schema/schemas/alias.ts b/ark/schema/schemas/alias.ts deleted file mode 100644 index 6332d943ea..0000000000 --- a/ark/schema/schemas/alias.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { append } from "@arktype/util" -import { RawSchema, type RawSchemaDeclaration } from "../schema.js" -import type { RawSchemaScope } from "../scope.js" -import type { NodeCompiler } from "../shared/compile.js" -import type { BaseMeta, declareNode } from "../shared/declare.js" -import { Disjoint } from "../shared/disjoint.js" -import { implementNode } from "../shared/implement.js" -import { intersectNodes } from "../shared/intersections.js" -import type { TraverseAllows, TraverseApply } from "../shared/traversal.js" -import { defineRightwardIntersections } from "./utils.js" - -export interface AliasInner extends BaseMeta { - readonly alias: alias - readonly resolve?: () => RawSchema -} - -export type AliasDef = - | `$${alias}` - | AliasInner - -export type AliasDeclaration = declareNode<{ - kind: "alias" - def: AliasDef - normalizedDef: AliasInner - inner: AliasInner -}> - -export class AliasNode extends RawSchema { - readonly expression = this.alias - - private _resolution: RawSchema | undefined - get resolution(): RawSchema { - this._resolution ??= this.resolve?.() ?? this.$.resolveSchema(this.alias) - return this._resolution - } - - rawKeyOf(): RawSchema { - return this.resolution.keyof() - } - - traverseAllows: TraverseAllows = (data, ctx) => { - const seen = ctx.seen[this.id] - if (seen?.includes(data as object)) return true - ctx.seen[this.id] = append(seen, data) - return this.resolution.traverseAllows(data, ctx) - } - - traverseApply: TraverseApply = (data, ctx) => { - const seen = ctx.seen[this.id] - if (seen?.includes(data as object)) return - ctx.seen[this.id] = append(seen, data) - this.resolution.traverseApply(data, ctx) - } - - compile(js: NodeCompiler): void { - js.if(`ctx.seen.${this.id}?.includes(data)`, () => js.return(true)) - js.line(`ctx.seen.${this.id} ??= []`).line(`ctx.seen.${this.id}.push(data)`) - js.return(js.invoke(this.resolution)) - } -} - -export const normalizeAliasDef = (def: AliasDef): AliasInner => - typeof def === "string" ? { alias: def.slice(1) } : def - -export const aliasImplementation = implementNode({ - kind: "alias", - hasAssociatedError: false, - collapsibleKey: "alias", - keys: { - alias: { - serialize: def => `$${def}` - }, - resolve: {} - }, - normalize: normalizeAliasDef, - defaults: { - description: node => node.alias - }, - intersections: { - alias: (l, r, ctx) => - ctx.$.lazilyResolve(`${l.alias}${ctx.pipe ? "|>" : "&"}${r.alias}`, () => - neverIfDisjoint(intersectNodes(l.resolution, r.resolution, ctx), ctx.$) - ), - ...defineRightwardIntersections("alias", (l, r, ctx) => - ctx.$.lazilyResolve(`${l.alias}${ctx.pipe ? "|>" : "&"}${r.alias}`, () => - neverIfDisjoint(intersectNodes(l.resolution, r, ctx), ctx.$) - ) - ) - } -}) - -const neverIfDisjoint = ( - result: RawSchema | Disjoint, - $: RawSchemaScope -): RawSchema => (result instanceof Disjoint ? $.keywords.never.raw : result) diff --git a/ark/schema/schemas/domain.ts b/ark/schema/schemas/domain.ts deleted file mode 100644 index 8ad3b51d5c..0000000000 --- a/ark/schema/schemas/domain.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - type NonEnumerableDomain, - domainDescriptions, - domainOf, - getBaseDomainKeys -} from "@arktype/util" -import type { BaseMeta, declareNode } from "../shared/declare.js" -import { Disjoint } from "../shared/disjoint.js" -import { implementNode } from "../shared/implement.js" -import type { TraverseAllows } from "../shared/traversal.js" -import { RawBasis } from "./basis.js" - -export interface DomainInner< - domain extends NonEnumerableDomain = NonEnumerableDomain -> extends BaseMeta { - readonly domain: domain -} - -export type DomainDef< - // only domains with an infinite number of values are allowed as bases - domain extends NonEnumerableDomain = NonEnumerableDomain -> = domain | NormalizedDomainDef - -export type NormalizedDomainDef< - domain extends NonEnumerableDomain = NonEnumerableDomain -> = DomainInner - -export type DomainDeclaration = declareNode<{ - kind: "domain" - def: DomainDef - normalizedDef: NormalizedDomainDef - inner: DomainInner - errorContext: DomainInner -}> - -export class DomainNode extends RawBasis { - traverseAllows: TraverseAllows = data => domainOf(data) === this.domain - - readonly compiledCondition = - this.domain === "object" ? - `((typeof data === "object" && data !== null) || typeof data === "function")` - : `typeof data === "${this.domain}"` - - readonly compiledNegation = - this.domain === "object" ? - `((typeof data !== "object" || data === null) && typeof data !== "function")` - : `typeof data !== "${this.domain}"` - - readonly expression = this.domain - readonly literalKeys = getBaseDomainKeys(this.domain) -} - -export const domainImplementation = implementNode({ - kind: "domain", - hasAssociatedError: true, - collapsibleKey: "domain", - keys: { - domain: {} - }, - normalize: def => (typeof def === "string" ? { domain: def } : def), - defaults: { - description: node => domainDescriptions[node.domain], - actual: data => (typeof data === "boolean" ? `${data}` : domainOf(data)) - }, - intersections: { - domain: (l, r) => Disjoint.from("domain", l, r) - } -}) diff --git a/ark/schema/schemas/intersection.ts b/ark/schema/schemas/intersection.ts deleted file mode 100644 index 0e85106249..0000000000 --- a/ark/schema/schemas/intersection.ts +++ /dev/null @@ -1,545 +0,0 @@ -import { - append, - appendUnique, - type array, - conflatenateAll, - entriesOf, - isArray, - isEmptyObject, - type listable, - omit, - pick, - type show, - splitByKeys, - throwInternalError -} from "@arktype/util" -import type { RawConstraint } from "../constraints/constraint.js" -import { - type ExtraneousKeyBehavior, - type ExtraneousKeyRestriction, - PropsGroup -} from "../constraints/props/props.js" -import type { Inner, MutableInner, NodeDef, Prerequisite } from "../kinds.js" -import type { Constraint, Node } from "../node.js" -import type { NodeParseContext } from "../parse.js" -import { RawSchema } from "../schema.js" -import type { RawSchemaScope } from "../scope.js" -import type { NodeCompiler } from "../shared/compile.js" -import { type BaseMeta, type declareNode, metaKeys } from "../shared/declare.js" -import { Disjoint } from "../shared/disjoint.js" -import type { ArkTypeError } from "../shared/errors.js" -import { - constraintKeys, - type ConstraintKind, - implementNode, - type IntersectionChildKind, - type IntersectionContext, - type OpenNodeKind, - propKeys, - type PropKind, - type RefinementKind -} from "../shared/implement.js" -import { intersectNodes } from "../shared/intersections.js" -import type { TraverseAllows, TraverseApply } from "../shared/traversal.js" -import { hasArkKind, isNode } from "../shared/utils.js" -import type { DomainDef, DomainNode } from "./domain.js" -import type { ProtoDef, ProtoNode } from "./proto.js" -import { defineRightwardIntersections } from "./utils.js" - -export type IntersectionBasisKind = "domain" | "proto" - -export type IntersectionInner = show< - BaseMeta & { - domain?: DomainNode - proto?: ProtoNode - } & { - [k in ConditionalIntersectionKey]?: conditionalInnerValueOfKey - } -> - -export type IntersectionDef = show< - BaseMeta & { - domain?: DomainDef - proto?: ProtoDef - } & conditionalSchemaOf -> - -export type IntersectionDeclaration = declareNode<{ - kind: "intersection" - def: IntersectionDef - normalizedDef: IntersectionDef - inner: IntersectionInner - reducibleTo: "intersection" | IntersectionBasisKind - errorContext: { - errors: readonly ArkTypeError[] - } - childKind: IntersectionChildKind -}> - -export class IntersectionNode extends RawSchema { - basis = this.domain ?? this.proto ?? null - refinements = this.children.filter((node): node is Node => - node.isRefinement() - ) - props = maybeCreatePropsGroup(this.inner, this.$) - traversables = conflatenateAll< - Node> | PropsGroup - >(this.basis, this.refinements, this.props, this.predicate) - - expression = - this.props?.expression || - this.children.map(node => node.nestableExpression).join(" & ") || - "unknown" - - traverseAllows: TraverseAllows = (data, ctx) => - this.traversables.every(traversable => - traversable.traverseAllows(data as never, ctx) - ) - - traverseApply: TraverseApply = (data, ctx) => { - if (this.basis) { - this.basis.traverseApply(data, ctx) - if (ctx.hasError()) return - } - if (this.refinements.length) { - for (let i = 0; i < this.refinements.length - 1; i++) { - this.refinements[i].traverseApply(data as never, ctx) - if (ctx.failFast && ctx.hasError()) return - } - this.refinements.at(-1)!.traverseApply(data as never, ctx) - if (ctx.hasError()) return - } - if (this.props) { - this.props.traverseApply(data as never, ctx) - if (ctx.hasError()) return - } - if (this.predicate) { - for (let i = 0; i < this.predicate.length - 1; i++) { - this.predicate[i].traverseApply(data as never, ctx) - if (ctx.failFast && ctx.hasError()) return - } - this.predicate.at(-1)!.traverseApply(data as never, ctx) - } - } - - compile(js: NodeCompiler): void { - if (js.traversalKind === "Allows") { - this.traversables.forEach(traversable => - isNode(traversable) ? js.check(traversable) : traversable.compile(js) - ) - js.return(true) - return - } - - const returnIfFail = () => js.if("ctx.hasError()", () => js.return()) - const returnIfFailFast = () => - js.if("ctx.failFast && ctx.hasError()", () => js.return()) - - if (this.basis) { - js.check(this.basis) - // we only have to return conditionally if this is not the last check - if (this.traversables.length > 1) returnIfFail() - } - if (this.refinements.length) { - for (let i = 0; i < this.refinements.length - 1; i++) { - js.check(this.refinements[i]) - returnIfFailFast() - } - js.check(this.refinements.at(-1)!) - if (this.props || this.predicate) returnIfFail() - } - if (this.props) { - this.props.compile(js) - if (this.predicate) returnIfFail() - } - if (this.predicate) { - for (let i = 0; i < this.predicate.length - 1; i++) { - js.check(this.predicate[i]) - // since predicates can be chained, we have to fail immediately - // if one fails - returnIfFail() - } - js.check(this.predicate.at(-1)!) - } - } - - rawKeyOf(): RawSchema { - return ( - this.basis ? - this.props ? - this.basis.rawKeyOf().or(this.props.keyof()) - : this.basis.rawKeyOf() - : this.props?.keyof() ?? this.$.keywords.never.raw - ) - } -} - -const intersectionChildKeyParser = - (kind: kind) => - ( - def: listable>, - ctx: NodeParseContext - ): intersectionChildInnerValueOf | undefined => { - if (isArray(def)) { - if (def.length === 0) { - // Omit empty lists as input - return - } - return def - .map(schema => ctx.$.node(kind, schema as never)) - .sort((l, r) => (l.innerHash < r.innerHash ? -1 : 1)) as never - } - const child = ctx.$.node(kind, def) - return child.hasOpenIntersection() ? [child] : (child as any) - } - -const intersectIntersections = ( - reduced: IntersectionInner, - raw: IntersectionInner, - ctx: IntersectionContext -): RawSchema | Disjoint => { - // avoid treating adding instance keys as keys of lRoot, rRoot - if (hasArkKind(reduced, "schema") && reduced.hasKind("intersection")) - return intersectIntersections(reduced.inner, raw, ctx) - if (hasArkKind(raw, "schema") && raw.hasKind("intersection")) - return intersectIntersections(reduced, raw.inner, ctx) - - const [reducedConstraintsInner, reducedRoot] = splitByKeys( - reduced, - constraintKeys - ) - const [rawConstraintsInner, rawRoot] = splitByKeys(raw, constraintKeys) - - // since intersection with a left operand of unknown is leveraged for - // reduction, check for the case where r is empty so we can preserve - // metadata and save some time - - const root = - isEmptyObject(reduced) ? rawRoot : ( - intersectRootKeys(reducedRoot, rawRoot, ctx) - ) - - if (root instanceof Disjoint) return root - - const lConstraints = flattenConstraints(reducedConstraintsInner) - const rConstraints = flattenConstraints(rawConstraintsInner) - - const constraintResult = intersectConstraints({ - l: lConstraints, - r: rConstraints, - types: [], - ctx - }) - - if (constraintResult instanceof Disjoint) return constraintResult - - let result: RawSchema | Disjoint = constraintResult.ctx.$.node( - "intersection", - Object.assign(root, unflattenConstraints(constraintResult.l)), - { prereduced: true } - ) - - for (const type of constraintResult.types) { - if (result instanceof Disjoint) return result - - result = intersectNodes(type, result, constraintResult.ctx) - } - - return result -} - -export const intersectionImplementation = - implementNode({ - kind: "intersection", - hasAssociatedError: true, - normalize: schema => schema, - keys: { - domain: { - child: true, - parse: intersectionChildKeyParser("domain") - }, - proto: { - child: true, - parse: intersectionChildKeyParser("proto") - }, - divisor: { - child: true, - parse: intersectionChildKeyParser("divisor") - }, - max: { - child: true, - parse: intersectionChildKeyParser("max") - }, - min: { - child: true, - parse: intersectionChildKeyParser("min") - }, - maxLength: { - child: true, - parse: intersectionChildKeyParser("maxLength") - }, - minLength: { - child: true, - parse: intersectionChildKeyParser("minLength") - }, - exactLength: { - child: true, - parse: intersectionChildKeyParser("exactLength") - }, - before: { - child: true, - parse: intersectionChildKeyParser("before") - }, - after: { - child: true, - parse: intersectionChildKeyParser("after") - }, - regex: { - child: true, - parse: intersectionChildKeyParser("regex") - }, - predicate: { - child: true, - parse: intersectionChildKeyParser("predicate") - }, - prop: { - child: true, - parse: intersectionChildKeyParser("prop") - }, - index: { - child: true, - parse: intersectionChildKeyParser("index") - }, - sequence: { - child: true, - parse: intersectionChildKeyParser("sequence") - }, - onExtraneousKey: { - parse: def => (def === "ignore" ? undefined : def) - } - }, - // leverage reduction logic from intersection and identity to ensure initial - // parse result is reduced - reduce: (inner, $) => - // we cast union out of the result here since that only occurs when intersecting two sequences - // that cannot occur when reducing a single intersection schema using unknown - intersectIntersections({}, inner, { - $, - invert: false, - pipe: false - }) as Node<"intersection" | IntersectionBasisKind>, - defaults: { - description: node => - node.children.length === 0 ? - "unknown" - : node.props?.description ?? - node.children.map(child => child.description).join(" and "), - expected: source => - ` β€’ ${source.errors.map(e => e.expected).join("\n β€’ ")}`, - problem: ctx => `must be...\n${ctx.expected}` - }, - intersections: { - intersection: (l, r, ctx) => { - if (l.props && r.props) { - if (l.onExtraneousKey) { - const lKey = l.props.keyof() - const disjointRKeys = r.props.requiredLiteralKeys.filter( - k => !lKey.allows(k) - ) - if (disjointRKeys.length) { - return Disjoint.from("presence", true, false).withPrefixKey( - disjointRKeys[0] - ) - } - } - if (r.onExtraneousKey) { - const rKey = r.props.keyof() - const disjointLKeys = l.props.requiredLiteralKeys.filter( - k => !rKey.allows(k) - ) - if (disjointLKeys.length) { - return Disjoint.from("presence", true, false).withPrefixKey( - disjointLKeys[0] - ) - } - } - } - return intersectIntersections(l, r, ctx) - }, - ...defineRightwardIntersections("intersection", (l, r, ctx) => { - // if l is unknown, return r - if (l.children.length === 0) return r - - const basis = l.basis ? intersectNodes(l.basis, r, ctx) : r - - return ( - basis instanceof Disjoint ? basis - : l?.basis?.equals(basis) ? - // if the basis doesn't change, return the original intesection - l - // given we've already precluded l being unknown, the result must - // be an intersection with the new basis result integrated - : l.$.node( - "intersection", - Object.assign(omit(l.inner, metaKeys), { - [basis.kind]: basis - }), - { prereduced: true } - ) - ) - }) - } - }) - -const maybeCreatePropsGroup = (inner: IntersectionInner, $: RawSchemaScope) => { - const propsInput = pick(inner, propKeys) - return isEmptyObject(propsInput) ? null : new PropsGroup(propsInput, $) -} - -type IntersectionRoot = Omit - -const intersectRootKeys = ( - l: IntersectionRoot, - r: IntersectionRoot, - ctx: IntersectionContext -): MutableInner<"intersection"> | Disjoint => { - const result: IntersectionRoot = {} - - const lBasis = l.proto ?? l.domain - const rBasis = r.proto ?? r.domain - const resultBasis = - lBasis ? - rBasis ? intersectNodes(lBasis, rBasis, ctx) - : lBasis - : rBasis - if (resultBasis) { - if (resultBasis instanceof Disjoint) return resultBasis - - if (resultBasis.kind === "domain" || resultBasis.kind === "proto") - result[resultBasis.kind] = resultBasis as never - else throwInternalError(`Unexpected basis intersection ${resultBasis}`) - } - - if (l.onExtraneousKey || r.onExtraneousKey) { - result.onExtraneousKey = - l.onExtraneousKey === "error" || r.onExtraneousKey === "error" ? - "error" - : "prune" - } - return result -} - -type ConstraintIntersectionState = { - l: Constraint[] - r: Constraint[] - types: RawSchema[] - ctx: IntersectionContext -} - -const intersectConstraints = ( - s: ConstraintIntersectionState -): ConstraintIntersectionState | Disjoint => { - const head = s.r.shift() - if (!head) return s - let matched = false - for (let i = 0; i < s.l.length; i++) { - const result = intersectNodes(s.l[i], head, s.ctx) - if (result === null) continue - if (result instanceof Disjoint) return result - - if (!matched) { - if (result.isSchema()) s.types.push(result) - else s.l[i] = result as RawConstraint - matched = true - } else if (!s.l.includes(result as never)) { - return throwInternalError( - `Unexpectedly encountered multiple distinct intersection results for refinement ${result}` - ) - } - } - if (!matched) s.l.push(head) - - head.impliedSiblings?.forEach(node => appendUnique(s.r, node)) - return intersectConstraints(s) -} - -const flattenConstraints = (inner: IntersectionInner): Constraint[] => { - const result = entriesOf(inner) - .flatMap(([k, v]) => - k in constraintKeys ? (v as listable) : [] - ) - .sort((l, r) => - l.precedence < r.precedence ? -1 - : l.precedence > r.precedence ? 1 - : l.innerHash < r.innerHash ? -1 - : 1 - ) - - return result -} - -const unflattenConstraints = ( - constraints: array -): IntersectionInner => { - const inner: MutableInner<"intersection"> = {} - for (const constraint of constraints) { - if (constraint.hasOpenIntersection()) { - inner[constraint.kind] = append( - inner[constraint.kind], - constraint - ) as never - } else { - if (inner[constraint.kind]) { - return throwInternalError( - `Unexpected intersection of closed refinements of kind ${constraint.kind}` - ) - } - inner[constraint.kind] = constraint as never - } - } - return inner -} - -export type ConditionalTerminalIntersectionSchema = { - onExtraneousKey?: ExtraneousKeyBehavior -} - -export type ConditionalTerminalIntersectionInner = { - onExtraneousKey?: ExtraneousKeyRestriction -} - -type ConditionalTerminalIntersectionKey = - keyof ConditionalTerminalIntersectionInner - -type ConditionalIntersectionKey = - | ConstraintKind - | keyof ConditionalTerminalIntersectionInner - -export type constraintKindOf = { - [k in ConstraintKind]: t extends Prerequisite ? k : never -}[ConstraintKind] - -type conditionalIntersectionKeyOf = - | constraintKindOf - | (t extends object ? "onExtraneousKey" : never) - -// not sure why explicitly allowing Inner is necessary in these cases, -// but remove if it can be removed without creating type errors -type intersectionChildSchemaValueOf = - k extends OpenNodeKind ? listable | Inner> - : NodeDef | Inner - -type conditionalSchemaValueOfKey = - k extends IntersectionChildKind ? intersectionChildSchemaValueOf - : ConditionalTerminalIntersectionSchema[k & ConditionalTerminalIntersectionKey] - -type intersectionChildInnerValueOf = - k extends OpenNodeKind ? readonly Node[] : Node - -type conditionalInnerValueOfKey = - k extends IntersectionChildKind ? intersectionChildInnerValueOf - : ConditionalTerminalIntersectionInner[k & ConditionalTerminalIntersectionKey] - -export type conditionalSchemaOf = { - [k in conditionalIntersectionKeyOf]?: conditionalSchemaValueOfKey -} diff --git a/ark/schema/schemas/morph.ts b/ark/schema/schemas/morph.ts deleted file mode 100644 index e739c90593..0000000000 --- a/ark/schema/schemas/morph.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { - type array, - arrayFrom, - type BuiltinObjectKind, - type BuiltinObjects, - type listable, - type Primitive, - registeredReference, - throwParseError -} from "@arktype/util" -import type { of } from "../constraints/ast.js" -import type { type } from "../inference.js" -import type { NodeDef } from "../kinds.js" -import type { Node, SchemaDef } from "../node.js" -import { RawSchema, type schemaKindRightOf } from "../schema.js" -import type { StaticArkOption } from "../scope.js" -import { NodeCompiler } from "../shared/compile.js" -import type { BaseMeta, declareNode } from "../shared/declare.js" -import { Disjoint } from "../shared/disjoint.js" -import type { ArkErrors, ArkTypeError } from "../shared/errors.js" -import { basisKinds, implementNode } from "../shared/implement.js" -import { type inferPipe, intersectNodes } from "../shared/intersections.js" -import type { - TraversalContext, - TraverseAllows, - TraverseApply -} from "../shared/traversal.js" -import { defineRightwardIntersections } from "./utils.js" - -export type MorphInputKind = schemaKindRightOf<"morph"> - -export const morphInputKinds = [ - "intersection", - ...basisKinds -] as const satisfies readonly MorphInputKind[] - -export type MorphInputNode = Node - -export type MorphInputDef = NodeDef - -export type Morph = (In: i, ctx: TraversalContext) => o - -export type Out = ["=>", o] - -export type MorphAst = (In: i) => Out - -export interface MorphInner extends BaseMeta { - readonly from: MorphInputNode - readonly to?: RawSchema - readonly morphs: readonly Morph[] -} - -export interface MorphDef extends BaseMeta { - readonly from: MorphInputDef - readonly to?: SchemaDef | undefined - readonly morphs: listable -} - -export type MorphDeclaration = declareNode<{ - kind: "morph" - def: MorphDef - normalizedDef: MorphDef - inner: MorphInner - childKind: MorphInputKind -}> - -export const morphImplementation = implementNode({ - kind: "morph", - hasAssociatedError: false, - keys: { - from: { - child: true, - parse: (def, ctx) => ctx.$.node(morphInputKinds, def) - }, - to: { - child: true, - parse: (def, ctx) => { - if (def === undefined) return - const to = ctx.$.schema(def) - return to.kind === "intersection" && to.children.length === 0 ? - // ignore unknown as an output validator - undefined - : to - } - }, - morphs: { - parse: arrayFrom, - serialize: morphs => morphs.map(registeredReference) - } - }, - normalize: def => def, - defaults: { - description: node => - `a morph from ${node.from.description} to ${node.to?.description ?? "unknown"}` - }, - intersections: { - morph: (l, r, ctx) => { - if (l.morphs.some((morph, i) => morph !== r.morphs[i])) - // TODO: check in for union reduction - return throwParseError("Invalid intersection of morphs") - const from = intersectNodes(l.from, r.from, ctx) - if (from instanceof Disjoint) return from - const to = - l.to ? - r.to ? - intersectNodes(l.to, r.to, ctx) - : l.to - : r.to - if (to instanceof Disjoint) return to - // in case from is a union, we need to distribute the branches - // to can be a union as any schema is allowed - return ctx.$.schema( - from.branches.map(fromBranch => - ctx.$.node("morph", { - morphs: l.morphs, - from: fromBranch, - to - }) - ) - ) - }, - ...defineRightwardIntersections("morph", (l, r, ctx) => { - const from = intersectNodes(l.from, r, ctx) - return ( - from instanceof Disjoint ? from - : from.kind === "union" ? - ctx.$.node( - "union", - from.branches.map(branch => ({ - ...l.inner, - from: branch - })) - ) - : ctx.$.node("morph", { - ...l.inner, - from - }) - ) - }) - } -}) - -export class MorphNode extends RawSchema { - serializedMorphs: string[] = (this.json as any).morphs - compiledMorphs = `[${this.serializedMorphs}]` - outValidator = this.to?.traverseApply ?? null - outValidatorReference: string = - this.to ? - new NodeCompiler("Apply").reference(this.to, { bind: "this" }) - : "null" - - traverseAllows: TraverseAllows = (data, ctx) => - this.from.traverseAllows(data, ctx) - traverseApply: TraverseApply = (data, ctx) => { - ctx.queueMorphs(this.morphs, this.outValidator) - this.from.traverseApply(data, ctx) - } - - expression = `(In: ${this.from.expression}) => Out<${this.to?.expression ?? "unknown"}>` - - compile(js: NodeCompiler): void { - if (js.traversalKind === "Allows") { - js.return(js.invoke(this.from)) - return - } - js.line( - `ctx.queueMorphs(${this.compiledMorphs}, ${this.outValidatorReference})` - ) - js.line(js.invoke(this.from)) - } - - getIo(kind: "in" | "out"): RawSchema { - return kind === "in" ? - this.from - : (this.to?.out as RawSchema) ?? this.$.keywords.unknown.raw - } - - rawKeyOf(): RawSchema { - return this.from.rawKeyOf() - } -} - -export type inferPipes = - pipes extends [infer head extends Morph, ...infer tail extends Morph[]] ? - inferPipes< - head extends type.cast ? inferPipe - : (In: distillConstrainableIn) => Out>, - tail - > - : t - -export type inferMorphOut = Exclude< - ReturnType, - ArkTypeError | ArkErrors -> - -export type distillIn = - includesMorphs extends true ? _distill : t - -export type distillOut = - includesMorphs extends true ? _distill : t - -export type distillConstrainableIn = - includesMorphs extends true ? _distill : t - -export type distillConstrainableOut = - includesMorphs extends true ? _distill : t - -export type includesMorphs = - [t, _distill, t, _distill] extends ( - [_distill, t, _distill, t] - ) ? - false - : true - -type _distill< - t, - io extends "in" | "out", - distilledKind extends "base" | "constrainable" -> = - unknown extends t ? unknown - : t extends MorphAst ? - io extends "in" ? - i - : o - : t extends of ? - distilledKind extends "base" ? - _distill - : t - : t extends TerminallyInferredObjectKind | Primitive ? t - : t extends array ? distillArray - : { - [k in keyof t]: _distill - } - -type distillArray< - t extends array, - io extends "in" | "out", - constraints extends "base" | "constrainable", - prefix extends array -> = - t extends readonly [infer head, ...infer tail] ? - distillArray< - tail, - io, - constraints, - [...prefix, _distill] - > - : [...prefix, ...distillPostfix] - -type distillPostfix< - t extends array, - io extends "in" | "out", - constraints extends "base" | "constrainable", - postfix extends array = [] -> = - t extends readonly [...infer init, infer last] ? - distillPostfix< - init, - io, - constraints, - [_distill, ...postfix] - > - : [...{ [i in keyof t]: _distill }, ...postfix] - -/** Objects we don't want to expand during inference like Date or Promise */ -type TerminallyInferredObjectKind = - | StaticArkOption<"preserve"> - | BuiltinObjects[Exclude] diff --git a/ark/schema/schemas/proto.ts b/ark/schema/schemas/proto.ts deleted file mode 100644 index 559a783512..0000000000 --- a/ark/schema/schemas/proto.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - type BuiltinObjectKind, - type Constructor, - builtinObjectKinds, - constructorExtends, - getExactBuiltinConstructorName, - objectKindDescriptions, - objectKindOrDomainOf, - prototypeKeysOf -} from "@arktype/util" -import type { BaseMeta, declareNode } from "../shared/declare.js" -import { Disjoint } from "../shared/disjoint.js" -import { defaultValueSerializer, implementNode } from "../shared/implement.js" -import type { TraverseAllows } from "../shared/traversal.js" -import { RawBasis } from "./basis.js" - -export interface ProtoInner - extends BaseMeta { - readonly proto: proto -} - -export type NormalizedProtoDef = - ProtoInner - -export type ProtoReference = Constructor | BuiltinObjectKind - -export interface ExpandedProtoDef - extends BaseMeta { - readonly proto: proto -} - -export type ProtoDef = - | proto - | ExpandedProtoDef - -export type ProtoDeclaration = declareNode<{ - kind: "proto" - def: ProtoDef - normalizedDef: NormalizedProtoDef - inner: ProtoInner - errorContext: ProtoInner -}> - -export const protoImplementation = implementNode({ - kind: "proto", - hasAssociatedError: true, - collapsibleKey: "proto", - keys: { - proto: { - serialize: ctor => - getExactBuiltinConstructorName(ctor) ?? defaultValueSerializer(ctor) - } - }, - normalize: def => - typeof def === "string" ? { proto: builtinObjectKinds[def] } - : typeof def === "function" ? { proto: def } - : typeof def.proto === "string" ? - { ...def, proto: builtinObjectKinds[def.proto] } - : (def as ExpandedProtoDef), - defaults: { - description: node => - node.builtinName ? - objectKindDescriptions[node.builtinName] - : `an instance of ${node.proto.name}`, - actual: data => objectKindOrDomainOf(data) - }, - intersections: { - proto: (l, r) => - constructorExtends(l.proto, r.proto) ? l - : constructorExtends(r.proto, l.proto) ? r - : Disjoint.from("proto", l, r), - domain: (proto, domain, ctx) => - domain.domain === "object" ? - proto - : Disjoint.from("domain", ctx.$.keywords.object as never, domain) - } -}) - -export class ProtoNode extends RawBasis { - builtinName = getExactBuiltinConstructorName(this.proto) - serializedConstructor = (this.json as { proto: string }).proto - compiledCondition = `data instanceof ${this.serializedConstructor}` - compiledNegation = `!(${this.compiledCondition})` - literalKeys = prototypeKeysOf(this.proto.prototype) - - traverseAllows: TraverseAllows = data => data instanceof this.proto - expression = this.proto.name - readonly domain = "object" -} diff --git a/ark/schema/schemas/unit.ts b/ark/schema/schemas/unit.ts deleted file mode 100644 index 643888c4f1..0000000000 --- a/ark/schema/schemas/unit.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { - type JsonPrimitive, - domainOf, - printable, - prototypeKeysOf -} from "@arktype/util" -import type { BaseMeta, declareNode } from "../shared/declare.js" -import { Disjoint } from "../shared/disjoint.js" -import { defaultValueSerializer, implementNode } from "../shared/implement.js" -import { RawBasis } from "./basis.js" -import { defineRightwardIntersections } from "./utils.js" - -export type UnitDef = UnitInner - -export interface UnitInner extends BaseMeta { - readonly unit: value -} - -export type UnitDeclaration = declareNode<{ - kind: "unit" - def: UnitDef - normalizedDef: UnitDef - inner: UnitInner - errorContext: UnitInner -}> - -export const unitImplementation = implementNode({ - kind: "unit", - hasAssociatedError: true, - keys: { - unit: { - preserveUndefined: true, - serialize: def => - def instanceof Date ? def.toISOString() : defaultValueSerializer(def) - } - }, - normalize: def => def, - defaults: { - description: node => printable(node.unit) - }, - intersections: { - unit: (l, r) => Disjoint.from("unit", l, r), - ...defineRightwardIntersections("unit", (l, r) => - r.allows(l.unit) ? l : Disjoint.from("assignability", l.unit, r) - ) - } -}) - -export class UnitNode extends RawBasis { - compiledValue: JsonPrimitive = (this.json as any).unit - serializedValue: JsonPrimitive = - typeof this.unit === "string" || this.unit instanceof Date ? - JSON.stringify(this.compiledValue) - : this.compiledValue - literalKeys = prototypeKeysOf(this.unit) - - compiledCondition = compileEqualityCheck(this.unit, this.serializedValue) - compiledNegation = compileEqualityCheck( - this.unit, - this.serializedValue, - "negated" - ) - expression = printable(this.unit) - domain = domainOf(this.unit) - - traverseAllows = - this.unit instanceof Date ? - (data: unknown) => - data instanceof Date && data.toISOString() === this.compiledValue - : (data: unknown) => data === this.unit -} - -const compileEqualityCheck = ( - unit: unknown, - serializedValue: JsonPrimitive, - negated?: "negated" -) => { - if (unit instanceof Date) { - const condition = `data instanceof Date && data.toISOString() === ${serializedValue}` - return negated ? `!(${condition})` : condition - } - return `data ${negated ? "!" : "="}== ${serializedValue}` -} diff --git a/ark/schema/schemas/utils.ts b/ark/schema/schemas/utils.ts deleted file mode 100644 index 7d2333ed0f..0000000000 --- a/ark/schema/schemas/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { flatMorph } from "@arktype/util" -import type { schemaKindRightOf } from "../schema.js" -import { - type SchemaKind, - type TypeIntersection, - schemaKindsRightOf -} from "../shared/implement.js" - -export const defineRightwardIntersections = ( - kind: kind, - implementation: TypeIntersection> -): { [k in schemaKindRightOf]: TypeIntersection } => - flatMorph(schemaKindsRightOf(kind), (i, kind) => [ - kind, - implementation - ]) as never diff --git a/ark/schema/scope.ts b/ark/schema/scope.ts index f68600c94e..172592d59a 100644 --- a/ark/schema/scope.ts +++ b/ark/schema/scope.ts @@ -1,41 +1,47 @@ import { CompiledFunction, - type Dict, DynamicBase, - type Json, - type PartialRecord, - type array, + bound, flatMorph, - type flattenListable, hasDomain, isArray, printable, - type requireKeys, - type show, throwInternalError, - throwParseError + throwParseError, + type Dict, + type Json, + type PartialRecord, + type array, + type flattenListable, + type requireKeys, + type show } from "@arktype/util" import { globalConfig, mergeConfigs } from "./config.js" import { - type GenericSchema, - validateUninstantiatedGenericNode + validateUninstantiatedGenericNode, + type GenericRoot } from "./generic.js" -import type { inferSchema, validateSchema } from "./inference.js" +import type { inferRoot, validateRoot } from "./inference.js" import type { internalKeywords } from "./keywords/internal.js" import type { jsObjects } from "./keywords/jsObjects.js" import type { Ark } from "./keywords/keywords.js" import type { tsKeywords } from "./keywords/tsKeywords.js" import { - type NodeDef, nodeImplementationsByKind, + type Node, + type NodeSchema, + type RootSchema, type reducibleKindOf } from "./kinds.js" -import { type PreparsedNodeResolution, SchemaModule } from "./module.js" -import type { Node, RawNode, SchemaDef } from "./node.js" -import { type NodeParseOptions, parseNode, schemaKindOf } from "./parse.js" -import type { RawSchema, Schema } from "./schema.js" -import { type AliasNode, normalizeAliasDef } from "./schemas/alias.js" -import type { distillIn, distillOut } from "./schemas/morph.js" +import { + RootModule, + type PreparsedNodeResolution, + type SchemaModule +} from "./module.js" +import type { BaseNode } from "./node.js" +import { parseNode, schemaKindOf, type NodeParseOptions } from "./parse.js" +import { normalizeAliasSchema, type AliasNode } from "./roots/alias.js" +import type { BaseRoot, Root } from "./roots/root.js" import { NodeCompiler } from "./shared/compile.js" import type { ActualWriter, @@ -47,19 +53,19 @@ import type { import type { DescriptionWriter, NodeKind, - SchemaKind + RootKind } from "./shared/implement.js" import type { TraverseAllows, TraverseApply } from "./shared/traversal.js" import { arkKind, hasArkKind, - type internalImplementationOf, - isNode + isNode, + type internalImplementationOf } from "./shared/utils.js" -export type nodeResolutions = { [k in keyof keywords]: RawSchema } +export type nodeResolutions = { [k in keyof keywords]: BaseRoot } -export type BaseResolutions = Record +export type BaseResolutions = Record declare global { export interface StaticArkConfig { @@ -157,25 +163,25 @@ export const resolveConfig = ( ): ResolvedArkConfig => extendConfig(extendConfig(defaultConfig, globalConfig), config) as never -export type RawSchemaResolutions = Record +export type RawRootResolutions = Record export type exportedNameOf<$> = Exclude export type PrivateDeclaration = `#${key}` -type toRawScope<$> = RawSchemaScope<{ +type toRawScope<$> = RawRootScope<{ [k in keyof $]: $[k] extends { [arkKind]: infer kind } ? - kind extends "generic" ? GenericSchema - : kind extends "module" ? RawSchemaModule + kind extends "generic" ? GenericRoot + : kind extends "module" ? RawRootModule : never - : RawSchema + : BaseRoot }> export type PrimitiveKeywords = typeof tsKeywords & typeof jsObjects & typeof internalKeywords -export type RawResolution = RawSchema | GenericSchema | RawSchemaModule +export type RawResolution = BaseRoot | GenericRoot | RawRootModule type CachedResolution = string | RawResolution @@ -184,7 +190,7 @@ const schemaBranchesOf = (schema: object) => : "branches" in schema && isArray(schema.branches) ? schema.branches : undefined -const throwMismatchedNodeSchemaError = (expected: NodeKind, actual: NodeKind) => +const throwMismatchedNodeRootError = (expected: NodeKind, actual: NodeKind) => throwParseError( `Node of kind ${actual} is not valid as a ${expected} definition` ) @@ -199,18 +205,22 @@ export type writeDuplicateAliasError = const nodeCountsByPrefix: PartialRecord = {} -const nodesById: Record = {} +const nodesById: Record = {} + +let scopeCount = 0 -export class RawSchemaScope< - $ extends RawSchemaResolutions = RawSchemaResolutions -> implements internalImplementationOf +const scopesById: Record = {} + +export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions> + implements internalImplementationOf { readonly config: ArkConfig - readonly resolvedConfig: ResolvedArkConfig; + readonly resolvedConfig: ResolvedArkConfig + readonly id = `$${++scopeCount}`; readonly [arkKind] = "scope" - readonly referencesById: { [name: string]: RawNode } = {} - references: readonly RawNode[] = [] + readonly referencesById: { [name: string]: BaseNode } = {} + references: readonly BaseNode[] = [] protected readonly resolutions: { [alias: string]: CachedResolution | undefined } = {} @@ -226,13 +236,13 @@ export class RawSchemaScope< /** @internal */ get keywords(): PrimitiveKeywords { - return RawSchemaScope.keywords + return RawRootScope.keywords } - static ambient: RawSchemaScope + static ambient: RawRootScope - get ambient(): RawSchemaScope { - return (this.constructor as typeof RawSchemaScope).ambient + get ambient(): RawRootScope { + return (this.constructor as typeof RawRootScope).ambient } constructor( @@ -263,24 +273,31 @@ export class RawSchemaScope< this.ambient.resolutions, (alias, resolution) => [ alias, - hasArkKind(resolution, "schema") ? + hasArkKind(resolution, "root") ? resolution.bindScope(this) : resolution ] ) } + scopesById[this.id] = this } get raw(): this { return this } - schema = ((def: SchemaDef, opts?: NodeParseOptions): RawSchema => - this.node(schemaKindOf(def), def, opts)).bind(this) + @bound + schema(def: RootSchema, opts?: NodeParseOptions): BaseRoot { + return this.node(schemaKindOf(def), def, opts) + } - defineSchema = ((def: SchemaDef) => def).bind(this) + @bound + defineRoot(def: RootSchema): RootSchema { + return def + } - units = ((values: unknown[], opts?: NodeParseOptions): RawSchema => { + @bound + units(values: array, opts?: NodeParseOptions): BaseRoot { const uniqueValues: unknown[] = [] for (const value of values) if (!uniqueValues.includes(value)) uniqueValues.push(value) @@ -290,10 +307,15 @@ export class RawSchemaScope< ...opts, prereduced: true }) - }).bind(this) + } protected lazyResolutions: AliasNode[] = [] - lazilyResolve(syntheticAlias: string, resolve: () => RawSchema): AliasNode { + lazilyResolve(resolve: () => BaseRoot, syntheticAlias?: string): AliasNode { + if (!syntheticAlias) { + nodeCountsByPrefix.synthetic ??= 0 + syntheticAlias = `synthetic${++nodeCountsByPrefix.synthetic}` + } + const node = this.node( "alias", { @@ -306,95 +328,96 @@ export class RawSchemaScope< return node } - node = (< - kinds extends NodeKind | array, + node: < + kinds extends NodeKind | array, prereduced extends boolean = false >( kinds: kinds, - nodeDef: NodeDef>, + nodeSchema: NodeSchema>, opts?: NodeParseOptions - ): Node< + ) => Node< prereduced extends true ? flattenListable : reducibleKindOf> - > => { - let kind: NodeKind = - typeof kinds === "string" ? kinds : schemaKindOf(nodeDef, kinds) + > = ( + ((kinds, nodeSchema, opts) => { + let kind: NodeKind = + typeof kinds === "string" ? kinds : schemaKindOf(nodeSchema, kinds) - let def: unknown = nodeDef + let schema: unknown = nodeSchema - if (isNode(def) && def.kind === kind) return def.bindScope(this) as never + if (isNode(schema) && schema.kind === kind) + return schema.bindScope(this) as never - if (kind === "alias" && !opts?.prereduced) { - const resolution = this.resolveSchema( - normalizeAliasDef(def as never).alias - ) - def = resolution - kind = resolution.kind - } else if (kind === "union" && hasDomain(def, "object")) { - const branches = schemaBranchesOf(def) - if (branches?.length === 1) { - def = branches[0] - kind = schemaKindOf(def) + if (kind === "alias" && !opts?.prereduced) { + const resolution = this.resolveRoot( + normalizeAliasSchema(schema as never).alias + ) + schema = resolution + kind = resolution.kind + } else if (kind === "union" && hasDomain(schema, "object")) { + const branches = schemaBranchesOf(schema) + if (branches?.length === 1) { + schema = branches[0] + kind = schemaKindOf(schema) + } } - } - const impl = nodeImplementationsByKind[kind] - const normalizedDef = impl.normalize?.(def) ?? def - // check again after normalization in case a node is a valid collapsed - // schema for the kind (e.g. sequence can collapse to element accepting a Node) - if (isNode(normalizedDef)) { - return normalizedDef.kind === kind ? - (normalizedDef.bindScope(this) as never) - : throwMismatchedNodeSchemaError(kind, normalizedDef.kind) - } - - const prefix = opts?.alias ?? kind - nodeCountsByPrefix[prefix] ??= 0 - const id = `${prefix}${++nodeCountsByPrefix[prefix]!}` + const impl = nodeImplementationsByKind[kind] + const normalizedSchema = impl.normalize?.(schema) ?? schema + // check again after normalization in case a node is a valid collapsed + // schema for the kind (e.g. sequence can collapse to element accepting a Node) + if (isNode(normalizedSchema)) { + return normalizedSchema.kind === kind ? + (normalizedSchema.bindScope(this) as never) + : throwMismatchedNodeRootError(kind, normalizedSchema.kind) + } - const node = parseNode(kind, { - ...opts, - id, - $: this, - def: normalizedDef - }).bindScope(this) - - nodesById[id] = node - - if (this.resolved) { - // this node was not part of the original scope, so compile an anonymous scope - // including only its references - if (!this.resolvedConfig.jitless) - bindCompiledScope(node.contributesReferences) - } else { - // we're still parsing the scope itself, so defer compilation but - // add the node as a reference - Object.assign(this.referencesById, node.contributesReferencesById) - } + const prefix = opts?.alias ?? kind + nodeCountsByPrefix[prefix] ??= 0 + const id = `${prefix}${++nodeCountsByPrefix[prefix]!}` + + const node = parseNode(kind, { + ...opts, + id, + $: this, + schema: normalizedSchema + }).bindScope(this) + + nodesById[id] = node + + if (this.resolved) { + // this node was not part of the original scope, so compile an anonymous scope + // including only its references + if (!this.resolvedConfig.jitless) + bindCompiledScope(node.contributesReferences) + } else { + // we're still parsing the scope itself, so defer compilation but + // add the node as a reference + Object.assign(this.referencesById, node.contributesReferencesById) + } - return node as never - }).bind(this) + return node as never + }) satisfies this["node"] + ).bind(this) - parseRoot(def: unknown, opts?: NodeParseOptions): RawSchema { + parseRoot(def: unknown, opts?: NodeParseOptions): BaseRoot { return this.schema(def as never, opts) } - resolveSchema(name: string): RawSchema { + resolveRoot(name: string): BaseRoot { return ( - this.maybeResolveSchema(name) ?? + this.maybeResolveRoot(name) ?? throwParseError(writeUnresolvableMessage(name)) ) } - maybeResolveSchema(name: string): RawSchema | undefined { - const result = this.maybeResolveGenericOrSchema(name) + maybeResolveRoot(name: string): BaseRoot | undefined { + const result = this.maybeResolveGenericOrRoot(name) if (hasArkKind(result, "generic")) return return result } - maybeResolveGenericOrSchema( - name: string - ): RawSchema | GenericSchema | undefined { + maybeResolveGenericOrRoot(name: string): BaseRoot | GenericRoot | undefined { const resolution = this.maybeResolve(name) if (hasArkKind(resolution, "module")) return throwParseError(writeMissingSubmoduleAccessMessage(name)) @@ -428,14 +451,14 @@ export class RawSchemaScope< /** If name is a valid reference to a submodule alias, return its resolution */ protected maybeResolveSubalias( name: string - ): RawSchema | GenericSchema | undefined { + ): BaseRoot | GenericRoot | undefined { return resolveSubalias(this.aliases, name) } import[]>( ...names: names ): show> { - return new SchemaModule( + return new RootModule( flatMorph(this.export(...names) as any, (alias, value) => [ `#${alias}`, value @@ -443,8 +466,8 @@ export class RawSchemaScope< ) as never } - private _exportedResolutions: RawSchemaResolutions | undefined - private _exports: SchemaExportCache | undefined + private _exportedResolutions: RawRootResolutions | undefined + private _exports: RootExportCache | undefined export[]>( ...names: names ): show> { @@ -459,18 +482,18 @@ export class RawSchemaScope< Object.assign( this.json, flatMorph(this._exportedResolutions as Dict, (k, v) => - hasArkKind(v, "schema") ? [k, v.json] : [] + hasArkKind(v, "root") ? [k, v.json] : [] ) ) Object.assign(this.resolutions, this._exportedResolutions) if (this.config.registerKeywords) - Object.assign(RawSchemaScope.keywords, this._exportedResolutions) + Object.assign(RawRootScope.keywords, this._exportedResolutions) this.references = Object.values(this.referencesById) if (!this.resolvedConfig.jitless) bindCompiledScope(this.references) this.resolved = true } const namesToExport = names.length ? names : this.exportedNames - return new SchemaModule( + return new RootModule( flatMorph(namesToExport, (_, name) => [ name, this._exports![name] @@ -488,20 +511,20 @@ export class RawSchemaScope< const resolveSubalias = ( base: Dict, name: string -): RawSchema | GenericSchema | undefined => { +): BaseRoot | GenericRoot | undefined => { const dotIndex = name.indexOf(".") if (dotIndex === -1) return const dotPrefix = name.slice(0, dotIndex) - const prefixDef = base[dotPrefix] + const prefixSchema = base[dotPrefix] // if the name includes ".", but the prefix is not an alias, it // might be something like a decimal literal, so just fall through to return - if (prefixDef === undefined) return - if (!hasArkKind(prefixDef, "module")) + if (prefixSchema === undefined) return + if (!hasArkKind(prefixSchema, "module")) return throwParseError(writeNonSubmoduleDotMessage(dotPrefix)) const subalias = name.slice(dotIndex + 1) - const resolution = prefixDef[subalias] + const resolution = prefixSchema[subalias] // if the first part of name is a submodule but the suffix is // unresolvable, we can throw immediately if (resolution === undefined) { @@ -510,7 +533,7 @@ const resolveSubalias = ( return throwParseError(writeUnresolvableMessage(name)) } - if (hasArkKind(resolution, "schema") || hasArkKind(resolution, "generic")) + if (hasArkKind(resolution, "root") || hasArkKind(resolution, "generic")) return resolution throwInternalError( @@ -520,27 +543,24 @@ const resolveSubalias = ( export type validateAliases = { [k in keyof aliases]: aliases[k] extends PreparsedNodeResolution ? aliases[k] - : validateSchema + : validateRoot } export type instantiateAliases = { [k in keyof aliases]: aliases[k] extends PreparsedNodeResolution ? aliases[k] - : inferSchema + : inferRoot } & unknown export const schemaScope = ( aliases: validateAliases, config?: ArkConfig -): SchemaScope> => new SchemaScope(aliases, config) - -export interface SchemaScope<$ = any> { - $: $ - infer: distillOut<$> - inferIn: distillIn<$> +): RootScope> => new RootScope(aliases, config) +export interface RootScope<$ = any> { + t: $ [arkKind]: "scope" config: ArkConfig - references: readonly RawNode[] + references: readonly BaseNode[] json: Json exportedNames: array> @@ -549,25 +569,25 @@ export interface SchemaScope<$ = any> { aliases: Record raw: toRawScope<$> - schema( - def: def, + schema( + schema: def, opts?: NodeParseOptions - ): Schema, $> + ): Root, $> - defineSchema(def: def): def + defineRoot(schema: def): def units( values: branches, opts?: NodeParseOptions - ): Schema + ): Root - node>( + node>( kinds: kinds, - schema: NodeDef>, + schema: NodeSchema>, opts?: NodeParseOptions ): Node>> - parseRoot(def: unknown, opts?: NodeParseOptions): RawSchema + parseRoot(schema: unknown, opts?: NodeParseOptions): BaseRoot import[]>( ...names: names @@ -579,26 +599,26 @@ export interface SchemaScope<$ = any> { resolve>( name: name - ): destructuredExportContext<$, []>[name] + ): $[name] extends PreparsedNodeResolution ? $[name] : Root<$[name], $> } -export const SchemaScope: new <$ = any>( - ...args: ConstructorParameters -) => SchemaScope<$> = RawSchemaScope as never +export const RootScope: new <$ = any>( + ...args: ConstructorParameters +) => RootScope<$> = RawRootScope as never -export const root: SchemaScope<{}> = new SchemaScope({}) +export const root: RootScope<{}> = new RootScope({}) -export const schema = root.schema -export const node = root.node -export const defineSchema = root.defineSchema -export const units = root.units -export const rawSchema = root.raw.schema -export const rawNode = root.raw.node -export const defineRawSchema = root.raw.defineSchema -export const rawUnits = root.raw.units +export const schema: RootScope["schema"] = root.schema +export const node: RootScope["node"] = root.node +export const defineRoot: RootScope["defineRoot"] = root.defineRoot +export const units: RootScope["units"] = root.units +export const rawRoot: RawRootScope["schema"] = root.raw.schema +export const rawNode: RawRootScope["node"] = root.raw.node +export const defineRawRoot: RawRootScope["defineRoot"] = root.raw.defineRoot +export const rawUnits: RawRootScope["units"] = root.raw.units -export class RawSchemaModule< - resolutions extends RawSchemaResolutions = RawSchemaResolutions +export class RawRootModule< + resolutions extends RawRootResolutions = RawRootResolutions > extends DynamicBase { // TODO: kind? declare readonly [arkKind]: "module" @@ -613,13 +633,13 @@ export type destructuredImportContext<$, names extends exportedNameOf<$>[]> = { string}`]: $[k] } -export type SchemaExportCache = Record< +export type RootExportCache = Record< string, - RawSchema | GenericSchema | RawSchemaModule | undefined + BaseRoot | GenericRoot | RawRootModule | undefined > -const resolutionsOfModule = ($: RawSchemaScope, typeSet: SchemaExportCache) => { - const result: RawSchemaResolutions = {} +const resolutionsOfModule = ($: RawRootScope, typeSet: RootExportCache) => { + const result: RawRootResolutions = {} for (const k in typeSet) { const v = typeSet[k] if (hasArkKind(v, "module")) { @@ -630,7 +650,7 @@ const resolutionsOfModule = ($: RawSchemaScope, typeSet: SchemaExportCache) => { ) Object.assign(result, prefixedResolutions) } else if (hasArkKind(v, "generic")) result[k] = v - else if (hasArkKind(v, "schema")) result[k] = v + else if (hasArkKind(v, "root")) result[k] = v else throwInternalError(`Unexpected scope resolution ${printable(v)}`) } return result @@ -659,7 +679,7 @@ export const writeMissingSubmoduleAccessMessage = ( export type writeMissingSubmoduleAccessMessage = `Reference to submodule '${name}' must specify an alias` -export const bindCompiledScope = (references: readonly RawNode[]): void => { +export const bindCompiledScope = (references: readonly BaseNode[]): void => { const compiledTraversals = compileScope(references) for (const node of references) { if (node.jit) { @@ -669,7 +689,7 @@ export const bindCompiledScope = (references: readonly RawNode[]): void => { node.jit = true node.traverseAllows = compiledTraversals[`${node.id}Allows`].bind(compiledTraversals) - if (node.isSchema() && !node.allowsRequiresContext) { + if (node.isRoot() && !node.allowsRequiresContext) { // if the reference doesn't require context, we can assign over // it directly to avoid having to initialize it node.allows = node.traverseAllows as never @@ -679,7 +699,7 @@ export const bindCompiledScope = (references: readonly RawNode[]): void => { } } -const compileScope = (references: readonly RawNode[]) => { +const compileScope = (references: readonly BaseNode[]) => { return new CompiledFunction() .block("return", js => { references.forEach(node => { diff --git a/ark/schema/shared/compile.ts b/ark/schema/shared/compile.ts index be1a0f5fa0..d4787a0b16 100644 --- a/ark/schema/shared/compile.ts +++ b/ark/schema/shared/compile.ts @@ -1,6 +1,7 @@ import { CompiledFunction } from "@arktype/util" -import type { Node, RawNode } from "../node.js" -import type { Discriminant } from "../schemas/discriminate.js" +import type { Node } from "../kinds.js" +import type { BaseNode } from "../node.js" +import type { Discriminant } from "../roots/discriminate.js" import type { PrimitiveKind } from "./implement.js" import type { TraversalKind } from "./traversal.js" @@ -21,7 +22,7 @@ export class NodeCompiler extends CompiledFunction<["data", "ctx"]> { super("data", "ctx") } - invoke(node: RawNode, opts?: InvokeOptions): string { + invoke(node: BaseNode, opts?: InvokeOptions): string { const arg = opts?.arg ?? this.data if (this.requiresContextFor(node)) return `${this.reference(node, opts)}(${arg}, ${this.ctx})` @@ -29,29 +30,47 @@ export class NodeCompiler extends CompiledFunction<["data", "ctx"]> { return `${this.reference(node, opts)}(${arg})` } - reference(node: RawNode, opts?: ReferenceOptions): string { + reference(node: BaseNode, opts?: ReferenceOptions): string { const invokedKind = opts?.kind ?? this.traversalKind const base = `this.${node.id}${invokedKind}` return opts?.bind ? `${base}.bind(${opts?.bind})` : base } - requiresContextFor(node: RawNode): boolean { + requiresContextFor(node: BaseNode): boolean { return this.traversalKind === "Apply" || node.allowsRequiresContext } - checkReferenceKey(keyExpression: string, node: RawNode): this { + initializeErrorCount(): this { + return this.const("errorCount", "ctx.currentErrorCount") + } + + returnIfFail(): this { + return this.if("ctx.currentErrorCount > errorCount", () => this.return()) + } + + returnIfFailFast(): this { + return this.if("ctx.failFast && ctx.currentErrorCount > errorCount", () => + this.return() + ) + } + + traverseKey( + keyExpression: string, + accessExpression: string, + node: BaseNode + ): this { const requiresContext = this.requiresContextFor(node) if (requiresContext) this.line(`${this.ctx}.path.push(${keyExpression})`) this.check(node, { - arg: `${this.data}${this.index(keyExpression)}` + arg: accessExpression }) if (requiresContext) this.line(`${this.ctx}.path.pop()`) return this } - check(node: RawNode, opts?: InvokeOptions): this { + check(node: BaseNode, opts?: InvokeOptions): this { return this.traversalKind === "Allows" ? this.if(`!${this.invoke(node, opts)}`, () => this.return(false)) : this.line(this.invoke(node, opts)) diff --git a/ark/schema/shared/declare.ts b/ark/schema/shared/declare.ts index fa5eb64ab4..c42e4b8472 100644 --- a/ark/schema/shared/declare.ts +++ b/ark/schema/shared/declare.ts @@ -1,6 +1,5 @@ import type { merge, show } from "@arktype/util" -import type { reducibleKindOf } from "../kinds.js" -import type { Node } from "../node.js" +import type { Node, reducibleKindOf } from "../kinds.js" import type { Disjoint } from "./disjoint.js" import type { NarrowedAttachments, NodeKind } from "./implement.js" @@ -12,8 +11,8 @@ export const metaKeys: { [k in keyof BaseMeta]: 1 } = { description: 1 } interface DeclarationInput { kind: NodeKind - def: unknown - normalizedDef: BaseMeta + schema: unknown + normalizedSchema: BaseMeta inner: BaseMeta reducibleTo?: NodeKind intersectionIsOpen?: true @@ -41,7 +40,6 @@ export type declareNode< prerequisite: prerequisiteOf childKind: never reducibleTo: d["kind"] - errorContext: null }, d & { errorContext: d["errorContext"] extends {} ? BaseErrorContext @@ -57,8 +55,8 @@ export type attachmentsOf = export interface RawNodeDeclaration { kind: NodeKind - def: unknown - normalizedDef: BaseMeta + schema: unknown + normalizedSchema: BaseMeta inner: BaseMeta reducibleTo: NodeKind prerequisite: any diff --git a/ark/schema/shared/disjoint.ts b/ark/schema/shared/disjoint.ts index f446902854..d764ed614c 100644 --- a/ark/schema/shared/disjoint.ts +++ b/ark/schema/shared/disjoint.ts @@ -1,17 +1,17 @@ import { entriesOf, - type entryOf, flatMorph, fromEntries, isArray, printable, register, throwInternalError, - throwParseError + throwParseError, + type entryOf } from "@arktype/util" -import type { Node } from "../node.js" -import type { RawSchema } from "../schema.js" -import type { BoundKind, IntersectionChildKind } from "./implement.js" +import type { Node } from "../kinds.js" +import type { BaseRoot } from "../roots/root.js" +import type { BoundKind, PrimitiveKind } from "./implement.js" import { hasArkKind } from "./utils.js" type DisjointKinds = { @@ -27,16 +27,10 @@ type DisjointKinds = { l: Node<"proto"> r: Node<"proto"> } - // TODO: test - presence?: - | { - l: true - r: false - } - | { - l: false - r: true - } + presence?: { + l: BaseRoot + r: BaseRoot + } range?: { l: Node r: Node @@ -44,15 +38,15 @@ type DisjointKinds = { assignability?: | { l: unknown - r: Node + r: Node } | { - l: Node + l: Node r: unknown } union?: { - l: readonly RawSchema[] - r: readonly RawSchema[] + l: readonly BaseRoot[] + r: readonly BaseRoot[] } indiscriminableMorphs?: { l: Node<"union"> @@ -78,10 +72,12 @@ export type DisjointsAtPath = { export type DisjointSourceEntry = entryOf +export type DisjointSource = Required[DisjointKind] + export type FlatDisjointEntry = { path: SerializedPath kind: DisjointKind - disjoint: Required[DisjointKind] + disjoint: DisjointSource } export type DisjointKind = keyof DisjointKinds @@ -134,17 +130,10 @@ export class Disjoint { const pathString = JSON.parse(path).join(".") return `Intersection${ pathString && ` at ${pathString}` - } of ${describeReason(disjoint.l)} and ${describeReason( - disjoint.r - )} results in an unsatisfiable type` + } of ${describeReasons(disjoint)} results in an unsatisfiable type` } return `The following intersections result in unsatisfiable types:\nβ€’ ${reasons - .map( - ({ path, disjoint }) => - `${path}: ${describeReason( - disjoint.l - )} and ${describeReason(disjoint.r)}` - ) + .map(({ path, disjoint }) => `${path}: ${describeReasons(disjoint)}`) .join("\nβ€’ ")}` } @@ -193,7 +182,10 @@ export class Disjoint { } } +const describeReasons = (source: DisjointSource): string => + `${describeReason(source.l)} and ${describeReason(source.r)}` + const describeReason = (value: unknown): string => - hasArkKind(value, "schema") ? value.expression + hasArkKind(value, "root") ? value.expression : isArray(value) ? value.map(describeReason).join(" | ") : String(value) diff --git a/ark/schema/shared/errors.ts b/ark/schema/shared/errors.ts index 1a2ef56ddc..4839a03ef4 100644 --- a/ark/schema/shared/errors.ts +++ b/ark/schema/shared/errors.ts @@ -1,19 +1,75 @@ import { + CastableBase, ReadonlyArray, - hasDefinedKey, - type optionalizeKeys, + defineProperties, + type propwiseXor, type show } from "@arktype/util" import type { Prerequisite, errorContext } from "../kinds.js" +import type { ResolvedArkConfig } from "../scope.js" import type { NodeKind } from "./implement.js" import type { TraversalContext } from "./traversal.js" import { arkKind, pathToPropString, type TraversalPath } from "./utils.js" -export const throwArkError = (message: string): never => { - throw new ArkError(message) -} +export type ArkErrorResult = ArkError | ArkErrors + +export class ArkError< + code extends ArkErrorCode = ArkErrorCode +> extends CastableBase> { + readonly [arkKind] = "error" + path: TraversalPath + data: Prerequisite + private nodeConfig: ResolvedArkConfig[code] + + constructor( + protected input: ArkErrorContextInput, + ctx: TraversalContext + ) { + super() + defineProperties(this, input) + const data = ctx.data + if (input.code === "union") { + // flatten union errors to avoid repeating context like "foo must be foo must be"... + input.errors = input.errors.flatMap(e => + e.hasCode("union") ? e.errors : e + ) + } + this.nodeConfig = ctx.config[this.code] as never + this.path = input.path ?? [...ctx.path] + if (input.relativePath) this.path.push(...input.relativePath) + this.data = "data" in input ? input.data : data + } + + hasCode(code: code): this is ArkError { + return this.code === code + } + + get propString(): string { + return pathToPropString(this.path) + } + + get expected(): string { + return ( + this.input.expected ?? this.nodeConfig.expected?.(this.input as never) + ) + } + + get actual(): string | null { + // null is a valid value of actual meaning it should be omitted, so + // check for undefined explicitly + return this.input.actual !== undefined ? + this.input.actual + : this.nodeConfig.actual?.(this.data as never) + } + + get problem(): string { + return this.input.problem ?? this.nodeConfig.problem(this as never) + } + + get message(): string { + return this.input.message ?? this.nodeConfig.message(this as never) + } -export class ArkError extends TypeError { toString(): string { return this.message } @@ -23,39 +79,28 @@ export class ArkError extends TypeError { } } -export type ArkTypeError = ArkError & - ArkErrorContext - -export const ArkTypeError: new ( - context: ArkErrorContext -) => ArkTypeError = class extends ArkError { - readonly [arkKind] = "error" - - constructor(context: ArkErrorContext) { - super(context.message) - Object.assign(this, context) - } -} as never - -export class ArkErrors extends ReadonlyArray { +export class ArkErrors extends ReadonlyArray { constructor(protected ctx: TraversalContext) { super() } - byPath: Record = {} + byPath: Record = {} count = 0 - private mutable: ArkTypeError[] = this as never + private mutable: ArkError[] = this as never - add(error: ArkTypeError): void { + add(error: ArkError): void { const existing = this.byPath[error.propString] if (existing) { - const errorIntersection = createError(this.ctx, { - code: "intersection", - errors: - existing.code === "intersection" ? - [...existing.errors, error] - : [existing, error] - }) + const errorIntersection = new ArkError( + { + code: "intersection", + errors: + existing.hasCode("intersection") ? + [...existing.errors, error] + : [existing, error] + }, + this.ctx + ) const existingIndex = this.indexOf(existing) // If existing is found (which it always should be unless this was externally mutated), // replace it with the new problem intersection. In case it isn't for whatever reason, @@ -74,89 +119,49 @@ export class ArkErrors extends ReadonlyArray { return this.toString() } + get message(): string { + return this.toString() + } + toString(): string { return this.join("\n") } throw(): never { - throw new ArkError(`${this}`, { cause: this }) - } -} - -export const createError = ( - ctx: TraversalContext, - input: ArkErrorInput -): ArkTypeError => { - let errCtx: ArkErrorContext - const data = ctx.data - const nodeConfig = ctx.config.predicate - if (typeof input === "string") { - errCtx = { - code: "predicate", - path: [...ctx.path], - propString: pathToPropString(ctx.path), - data, - actual: nodeConfig.actual(data), - expected: input - } satisfies ProblemContext as any - errCtx.problem = nodeConfig.problem(errCtx as never) - errCtx.message = nodeConfig.message(errCtx as never) - } else { - const code = input.code ?? "predicate" - if (input.code === "union") { - // flatten union errors to avoid repeating context like "foo must be foo must be"... - input.errors = input.errors.flatMap(e => - e.code === "union" ? e.errors : e - ) - } - const nodeConfig = ctx.config[code] - const expected = input.expected ?? nodeConfig.expected?.(input as never) - const path = input.path ?? [...ctx.path] - errCtx = { - ...input, - // prioritize these over the raw user provided values so we can - // check for keys with values like undefined - code, - path, - propString: pathToPropString(path), - data: "data" in input ? input.data : data, - actual: - input.actual !== undefined ? - input.actual - : nodeConfig.actual?.(data as never), - expected - } satisfies ProblemContext as any - errCtx.problem = - hasDefinedKey(input, "problem") ? - input.problem - : nodeConfig.problem(errCtx as never) - errCtx.message = - hasDefinedKey(input, "message") ? - input.message - : nodeConfig.message(errCtx as never) + throw this } - return new ArkTypeError(errCtx) } -export interface DerivableErrorContext { +export interface DerivableErrorContext< + code extends ArkErrorCode = ArkErrorCode +> { expected: string actual: string | null problem: string message: string - data: data + data: Prerequisite path: TraversalPath propString: string } +export type DerivableErrorContextInput< + code extends ArkErrorCode = ArkErrorCode +> = Partial> & + propwiseXor<{ path?: TraversalPath }, { relativePath?: TraversalPath }> + export type ArkErrorCode = { [kind in NodeKind]: errorContext extends null ? never : kind }[NodeKind] -export type ArkErrorContext = - errorContext & DerivableErrorContext> +type ArkErrorContextInputsByCode = { + [code in ArkErrorCode]: errorContext & DerivableErrorContextInput +} + +export type ArkErrorContextInput = + ArkErrorContextInputsByCode[code] export type MessageContext = Omit< - ArkErrorContext, + ArkError, "message" > @@ -165,22 +170,12 @@ export type ProblemContext = Omit< "problem" > -type ErrorInputByCode = { - [code in ArkErrorCode]: optionalizeKeys< - ArkErrorContext, - keyof DerivableErrorContext - > -} - export type CustomErrorInput = show< // ensure a custom error can be discriminated on the lack of a code - { code?: undefined } & Partial + { code?: undefined } & DerivableErrorContextInput > -export type ArkErrorInput = - | string - | ErrorInputByCode[ArkErrorCode] - | CustomErrorInput +export type ArkErrorInput = string | ArkErrorContextInput | CustomErrorInput export type ProblemWriter = ( context: ProblemContext diff --git a/ark/schema/shared/implement.ts b/ark/schema/shared/implement.ts index 7f07c7c613..869e9fa158 100644 --- a/ark/schema/shared/implement.ts +++ b/ark/schema/shared/implement.ts @@ -1,51 +1,62 @@ import { + compileSerializedValue, + flatMorph, + printable, + throwParseError, type Entry, - type ErrorMessage, type Json, type JsonData, - compileSerializedValue, type entryOf, - flatMorph, type indexOf, + type keySet, type keySetOf, type listable, - printable, type requireKeys, type show } from "@arktype/util" -import type { PropsGroupInput } from "../constraints/props/props.js" -import type { Declaration, Inner, errorContext } from "../kinds.js" -import type { Node, RawNode } from "../node.js" +import type { Declaration, Inner, Node, errorContext } from "../kinds.js" +import type { BaseNode } from "../node.js" import type { NodeParseContext } from "../parse.js" import type { - RawSchema, + BaseRoot, schemaKindOrRightOf, schemaKindRightOf -} from "../schema.js" -import type { IntersectionInner } from "../schemas/intersection.js" +} from "../roots/root.js" import type { NodeConfig, ParsedUnknownNodeConfig, - RawSchemaScope + RawRootScope } from "../scope.js" +import type { StructureInner } from "../structure/structure.js" import type { BaseErrorContext, BaseMeta, RawNodeDeclaration } from "./declare.js" import type { Disjoint } from "./disjoint.js" -import { throwArkError } from "./errors.js" import { isNode } from "./utils.js" export const basisKinds = ["unit", "proto", "domain"] as const export type BasisKind = (typeof basisKinds)[number] -export const propKinds = ["prop", "index", "sequence"] as const +export const structuralKinds = [ + "required", + "optional", + "index", + "sequence" +] as const -export type PropKind = (typeof propKinds)[number] +export type StructuralKind = (typeof structuralKinds)[number] -export const rangeKinds = [ +export type RangeKind = Exclude + +export type BoundKind = Exclude + +export const refinementKinds = [ + "regex", + "divisor", + "exactLength", "max", "min", "maxLength", @@ -54,25 +65,25 @@ export const rangeKinds = [ "after" ] as const -export type RangeKind = (typeof rangeKinds)[number] - -export const boundKinds = ["exactLength", ...rangeKinds] as const - -export type BoundKind = (typeof boundKinds)[number] - -export const refinementKinds = ["regex", "divisor", ...boundKinds] as const - export type RefinementKind = (typeof refinementKinds)[number] -export const constraintKinds = [ +type orderedConstraintKinds = [ + ...typeof refinementKinds, + ...typeof structuralKinds, + "structure", + "predicate" +] + +export const constraintKinds: orderedConstraintKinds = [ ...refinementKinds, - ...propKinds, + ...structuralKinds, + "structure", "predicate" -] as const +] export type ConstraintKind = (typeof constraintKinds)[number] -export const schemaKinds = [ +export const rootKinds = [ "alias", "union", "morph", @@ -82,24 +93,13 @@ export const schemaKinds = [ "domain" ] as const -export type SchemaKind = (typeof schemaKinds)[number] +export type RootKind = (typeof rootKinds)[number] -export const intersectionChildKinds = [ - "proto", - "domain", - ...constraintKinds -] as const +export type NodeKind = RootKind | ConstraintKind -export type IntersectionChildKind = (typeof intersectionChildKinds)[number] +type orderedNodeKinds = [...typeof rootKinds, ...typeof constraintKinds] -export type NodeKind = SchemaKind | ConstraintKind - -export const nodeKinds = [ - ...schemaKinds, - ...refinementKinds, - ...propKinds, - "predicate" -] as const satisfies NodeKind[] +export const nodeKinds: orderedNodeKinds = [...rootKinds, ...constraintKinds] export type OpenNodeKind = { [k in NodeKind]: Declaration["intersectionIsOpen"] extends true ? k : never @@ -107,33 +107,22 @@ export type OpenNodeKind = { export type ClosedNodeKind = Exclude -export const primitiveKinds = [ - ...basisKinds, - ...refinementKinds, - "predicate" -] as const - -export type PrimitiveKind = (typeof primitiveKinds)[number] +export type PrimitiveKind = RefinementKind | BasisKind | "predicate" export type CompositeKind = Exclude export type OrderedNodeKinds = typeof nodeKinds -export const constraintKeys = flatMorph( +export const constraintKeys: keySet = flatMorph( constraintKinds, (i, kind) => [kind, 1] as const ) -export const propKeys = flatMorph( - [...propKinds, "onExtraneousKey"] satisfies (keyof PropsGroupInput)[], +export const structureKeys: keySetOf = flatMorph( + [...structuralKinds, "undeclared"], (i, k) => [k, 1] as const ) -export const discriminatingIntersectionKeys = { - ...constraintKeys, - onExtraneousKey: 1 -} as const satisfies keySetOf - type RightsByKind = accumulateRightKinds export type kindOrRightOf = kind | kindRightOf @@ -157,7 +146,7 @@ export interface InternalIntersectionOptions { } export interface IntersectionContext extends InternalIntersectionOptions { - $: RawSchemaScope + $: RawRootScope invert: boolean } @@ -168,7 +157,7 @@ export type ConstraintIntersection< l: Node, r: Node, ctx: IntersectionContext -) => RawNode | Disjoint | null +) => BaseNode | Disjoint | null export type ConstraintIntersectionMap = show< { @@ -178,38 +167,38 @@ export type ConstraintIntersectionMap = show< } > -export type TypeIntersection< - lKind extends SchemaKind, +export type RootIntersection< + lKind extends RootKind, rKind extends schemaKindOrRightOf > = ( l: Node, r: Node, ctx: IntersectionContext -) => RawSchema | Disjoint +) => BaseRoot | Disjoint -export type TypeIntersectionMap = { - [rKind in schemaKindOrRightOf]: TypeIntersection +export type TypeIntersectionMap = { + [rKind in schemaKindOrRightOf]: RootIntersection } export type IntersectionMap = - kind extends SchemaKind ? TypeIntersectionMap + kind extends RootKind ? TypeIntersectionMap : ConstraintIntersectionMap export type UnknownIntersectionMap = { [k in NodeKind]?: ( - l: RawNode, - r: RawNode, + l: BaseNode, + r: BaseNode, ctx: IntersectionContext ) => UnknownIntersectionResult } -export type UnknownIntersectionResult = RawNode | Disjoint | null +export type UnknownIntersectionResult = BaseNode | Disjoint | null type PrecedenceByKind = { [i in indexOf as OrderedNodeKinds[i]]: i } -export const precedenceByKind = flatMorph( +export const precedenceByKind: PrecedenceByKind = flatMorph( nodeKinds, (i, kind) => [kind, i] as entryOf ) @@ -218,12 +207,12 @@ export const isNodeKind = (value: unknown): value is NodeKind => typeof value === "string" && value in precedenceByKind export function assertNodeKind( - value: RawNode, + value: BaseNode, kind: kind ): asserts value is Node { const valueIsNode = isNode(value) if (!valueIsNode || value.kind !== kind) { - throwArkError( + throwParseError( `Expected node of kind ${kind} (was ${ valueIsNode ? `${value.kind} node` : printable(value) })` @@ -239,17 +228,17 @@ export const precedenceOfKind = ( export type kindRightOf = RightsByKind[kind] -export const schemaKindsRightOf = ( +export const schemaKindsRightOf = ( kind: kind ): schemaKindRightOf[] => - schemaKinds.slice(precedenceOfKind(kind) + 1) as never + rootKinds.slice(precedenceOfKind(kind) + 1) as never -export type KeyDefinitions = { - [k in keyRequiringDefinition]: NodeKeyImplementation +export type KeySchemainitions = { + [k in keyRequiringSchemainition]: NodeKeyImplementation } -type keyRequiringDefinition = Exclude< - keyof d["normalizedDef"], +type keyRequiringSchemainition = Exclude< + keyof d["normalizedSchema"], keyof BaseMeta > @@ -267,39 +256,37 @@ export const defaultValueSerializer = (v: unknown): JsonData => { export type NodeKeyImplementation< d extends RawNodeDeclaration, - k extends keyof d["normalizedDef"], - instantiated = k extends keyof d["inner"] ? d["inner"][k] : never + k extends keyof d["normalizedSchema"], + instantiated = k extends keyof d["inner"] ? Exclude + : never > = requireKeys< { preserveUndefined?: true meta?: true child?: true - implied?: true - serialize?: ( - schema: instantiated extends listable | undefined ? - ErrorMessage<`Keys with node children cannot specify a custom serializer`> - : instantiated - ) => JsonData + serialize?: (schema: instantiated) => JsonData parse?: ( - schema: Exclude, + schema: Exclude, ctx: NodeParseContext - ) => instantiated + ) => instantiated | undefined }, // require parse if we can't guarantee the schema value will be valid on inner - | (d["normalizedDef"][k] extends instantiated ? never : "parse") + | (d["normalizedSchema"][k] extends instantiated | undefined ? never + : "parse") // require keys containing children specify it - | ([instantiated] extends [listable | undefined] ? "child" : never) + | ([instantiated] extends [listable] ? "child" : never) > interface CommonNodeImplementationInput { kind: d["kind"] - keys: KeyDefinitions - normalize: (schema: d["def"]) => d["normalizedDef"] + keys: KeySchemainitions + normalize: (schema: d["schema"]) => d["normalizedSchema"] hasAssociatedError: d["errorContext"] extends null ? false : true + finalizeJson?: (json: { [k in keyof d["inner"]]: JsonData }) => Json collapsibleKey?: keyof d["inner"] reduce?: ( inner: d["inner"], - $: RawSchemaScope + $: RawRootScope ) => Node | Disjoint | undefined } @@ -329,14 +316,14 @@ export type nodeImplementationOf = export type nodeImplementationInputOf = CommonNodeImplementationInput & { intersections: IntersectionMap - defaults: nodeDefaultsImplementationInputFor + defaults: nodeSchemaaultsImplementationInputFor } & (d["intersectionIsOpen"] extends true ? { intersectionIsOpen: true } : {}) & // if the node is declared as reducible to a kind other than its own, // there must be a reduce implementation (d["reducibleTo"] extends d["kind"] ? {} : { reduce: {} }) -type nodeDefaultsImplementationInputFor = requireKeys< +type nodeSchemaaultsImplementationInputFor = requireKeys< NodeConfig, | "description" // if the node's error context is distinct from its inner definition, ensure it is implemented. @@ -363,10 +350,10 @@ export interface UnknownAttachments { readonly json: object readonly typeJson: object readonly collapsibleJson: JsonData - readonly children: RawNode[] + readonly children: BaseNode[] readonly innerHash: string readonly typeHash: string - readonly $: RawSchemaScope + readonly $: RawRootScope } export interface NarrowedAttachments diff --git a/ark/schema/shared/intersections.ts b/ark/schema/shared/intersections.ts index 6229b8ff82..8d2ce4ee43 100644 --- a/ark/schema/shared/intersections.ts +++ b/ark/schema/shared/intersections.ts @@ -7,15 +7,15 @@ import { type PartialRecord, type show } from "@arktype/util" -import type { of } from "../constraints/ast.js" -import type { RawNode } from "../node.js" -import type { RawSchema } from "../schema.js" -import type { MorphAst, MorphNode, Out } from "../schemas/morph.js" -import type { RawSchemaScope } from "../scope.js" +import type { of } from "../ast.js" +import type { BaseNode } from "../node.js" +import type { MorphAst, MorphNode, Out } from "../roots/morph.js" +import type { BaseRoot } from "../roots/root.js" +import type { RawRootScope } from "../scope.js" import { Disjoint } from "./disjoint.js" import type { IntersectionContext, - SchemaKind, + RootKind, UnknownIntersectionResult } from "./implement.js" import { isNode } from "./utils.js" @@ -62,29 +62,35 @@ type intersectObjects = [l, r] extends [infer lList extends array, infer rList extends array] ? intersectArrays> : show< + // this looks redundant, but should hit the cache anyways and + // preserves index signature + optional keys correctly { [k in keyof l]: k extends keyof r ? _inferIntersection : l[k] - } & Omit + } & { + [k in keyof r]: k extends keyof l ? + _inferIntersection + : r[k] + } > const intersectionCache: PartialRecord = {} -type InternalNodeIntersection = ( +type InternalNodeIntersection = ( l: l, r: r, ctx: ctx -) => l["kind"] | r["kind"] extends SchemaKind ? RawSchema | Disjoint -: RawNode | Disjoint | null +) => l["kind"] | r["kind"] extends RootKind ? BaseRoot | Disjoint +: BaseNode | Disjoint | null -export const intersectNodesRoot: InternalNodeIntersection = ( +export const intersectNodesRoot: InternalNodeIntersection = ( l, r, $ ) => intersectNodes(l, r, { $, invert: false, pipe: false }) -export const pipeNodesRoot: InternalNodeIntersection = ( +export const pipeNodesRoot: InternalNodeIntersection = ( l, r, $ @@ -97,13 +103,13 @@ export const intersectNodes: InternalNodeIntersection = ( ) => { const operator = ctx.pipe ? "|>" : "&" const lrCacheKey = `${l.typeHash}${operator}${r.typeHash}` - if (intersectionCache[lrCacheKey]) + if (intersectionCache[lrCacheKey] !== undefined) return intersectionCache[lrCacheKey]! as never if (!ctx.pipe) { // we can only use this for the commutative & operator const rlCacheKey = `${r.typeHash}${operator}${l.typeHash}` - if (intersectionCache[rlCacheKey]) { + if (intersectionCache[rlCacheKey] !== undefined) { // if the cached result was a Disjoint and the operands originally // appeared in the opposite order, we need to invert it to match const rlResult = intersectionCache[rlCacheKey]! @@ -157,28 +163,28 @@ export const intersectNodes: InternalNodeIntersection = ( export const pipeFromMorph = ( from: MorphNode, - to: RawSchema, + to: BaseRoot, ctx: IntersectionContext ): MorphNode | Disjoint => { - const out = from?.to ? intersectNodes(from.to, to, ctx) : to + const out = from?.out ? intersectNodes(from.out, to, ctx) : to if (out instanceof Disjoint) return out return ctx.$.node("morph", { morphs: from.morphs, - from: from.in, - to: out + in: from.in, + out }) } export const pipeToMorph = ( - from: RawSchema, + from: BaseRoot, to: MorphNode, ctx: IntersectionContext ): MorphNode | Disjoint => { - const result = intersectNodes(from, to.from, ctx) + const result = intersectNodes(from, to.in, ctx) if (result instanceof Disjoint) return result return ctx.$.node("morph", { morphs: to.morphs, - from: result, - to: to.out + in: result, + out: to.out }) } diff --git a/ark/schema/shared/traversal.ts b/ark/schema/shared/traversal.ts index 2bcd532e03..678f3cfa1f 100644 --- a/ark/schema/shared/traversal.ts +++ b/ark/schema/shared/traversal.ts @@ -1,34 +1,37 @@ import type { array } from "@arktype/util" -import type { Morph } from "../schemas/morph.js" +import type { Morph } from "../roots/morph.js" import type { ResolvedArkConfig } from "../scope.js" import { - type ArkErrorCode, - type ArkErrorInput, + ArkError, ArkErrors, - ArkTypeError, - createError + type ArkErrorCode, + type ArkErrorContextInput, + type ArkErrorInput } from "./errors.js" import type { TraversalPath } from "./utils.js" export type QueuedMorphs = { path: TraversalPath morphs: array - to: TraverseApply | null + to?: TraverseApply } export type BranchTraversalContext = { - error: ArkTypeError | undefined + error: ArkError | undefined queuedMorphs: QueuedMorphs[] } +export type QueueMorphOptions = { + outValidator?: TraverseApply +} + export class TraversalContext { path: TraversalPath = [] queuedMorphs: QueuedMorphs[] = [] errors: ArkErrors = new ArkErrors(this) branches: BranchTraversalContext[] = [] - // Qualified - seen: { [name in string]?: object[] } = {} + seen: { [id in string]?: object[] } = {} constructor( public root: unknown, @@ -39,12 +42,12 @@ export class TraversalContext { return this.branches.at(-1) } - queueMorphs(morphs: array, outValidator: TraverseApply | null): void { + queueMorphs(morphs: array, opts?: QueueMorphOptions): void { const input: QueuedMorphs = { path: [...this.path], - morphs, - to: outValidator + morphs } + if (opts?.outValidator) input.to = opts?.outValidator this.currentBranch?.queuedMorphs.push(input) ?? this.queuedMorphs.push(input) } @@ -63,7 +66,7 @@ export class TraversalContext { const result = morph(out, this) if (result instanceof ArkErrors) return result if (this.hasError()) return this.errors - if (result instanceof ArkTypeError) { + if (result instanceof ArkError) { // if an ArkTypeError was returned but wasn't added to these // errors, add it then return this.error(result) @@ -84,7 +87,7 @@ export class TraversalContext { const result = morph(parent[key], this) if (result instanceof ArkErrors) return result if (this.hasError()) return this.errors - if (result instanceof ArkTypeError) { + if (result instanceof ArkError) { this.error(result) return this.errors } @@ -101,10 +104,18 @@ export class TraversalContext { return out } + get currentErrorCount(): number { + return ( + this.currentBranch ? + this.currentBranch.error ? + 1 + : 0 + : this.errors.count + ) + } + hasError(): boolean { - return this.currentBranch ? - this.currentBranch.error !== undefined - : this.errors.count !== 0 + return this.currentErrorCount !== 0 } get failFast(): boolean { @@ -113,14 +124,20 @@ export class TraversalContext { error( input: input - ): ArkTypeError< + ): ArkError< input extends { code: ArkErrorCode } ? input["code"] : "predicate" > { - const error = createError(this, input) + const errCtx: ArkErrorContextInput = + typeof input === "object" ? + input.code ? + input + : { ...input, code: "predicate" } + : { code: "predicate", expected: input } + const error = new ArkError(errCtx, this) if (this.currentBranch) this.currentBranch.error = error else this.errors.add(error) - return error + return error as never } get data(): unknown { diff --git a/ark/schema/shared/utils.ts b/ark/schema/shared/utils.ts index 249b7999d1..27303a5391 100644 --- a/ark/schema/shared/utils.ts +++ b/ark/schema/shared/utils.ts @@ -1,17 +1,18 @@ import { - type array, flatMorph, isArray, isDotAccessible, - type mutable, printable, + type array, + type mutable, type show } from "@arktype/util" -import type { GenericSchema } from "../generic.js" -import type { Constraint, RawNode } from "../node.js" -import type { RawSchema } from "../schema.js" -import type { RawSchemaModule, RawSchemaScope } from "../scope.js" -import type { ArkTypeError } from "./errors.js" +import type { BaseConstraint } from "../constraint.js" +import type { GenericRoot } from "../generic.js" +import type { BaseNode } from "../node.js" +import type { BaseRoot } from "../roots/root.js" +import type { RawRootModule, RawRootScope } from "../scope.js" +import type { ArkError } from "./errors.js" export const makeRootAndArrayPropertiesMutable = ( o: o @@ -52,44 +53,28 @@ export const pathToPropString = (path: TraversalPath): string => { return propAccessChain[0] === "." ? propAccessChain.slice(1) : propAccessChain } -export const arkKind = Symbol("ArkTypeInternalKind") +export const arkKind: unique symbol = Symbol("ArkTypeInternalKind") export interface ArkKinds { - constraint: Constraint - schema: RawSchema - scope: RawSchemaScope - generic: GenericSchema - module: RawSchemaModule - error: ArkTypeError + constraint: BaseConstraint + root: BaseRoot + scope: RawRootScope + generic: GenericRoot + module: RawRootModule + error: ArkError } export type ArkKind = show -export const addArkKind = ( - value: Omit & { - [arkKind]?: kind - }, - kind: kind -): ArkKinds[kind] => - Object.defineProperty(value, arkKind, { - value: kind, - enumerable: false - }) as never - -export type addArkKind< - kind extends ArkKind, - t extends Omit -> = t & { [arkKind]: kind } - export const hasArkKind = ( value: unknown, kind: kind ): value is ArkKinds[kind] => (value as any)?.[arkKind] === kind -export const isNode = (value: unknown): value is RawNode => - hasArkKind(value, "schema") || hasArkKind(value, "constraint") +export const isNode = (value: unknown): value is BaseNode => + hasArkKind(value, "root") || hasArkKind(value, "constraint") // ideally this could be just declared since it is not used at runtime, // but it doesn't play well with typescript-eslint: https://github.com/typescript-eslint/typescript-eslint/issues/4608 // easiest solution seems to be just having it declared as a value so it doesn't break when we import at runtime -export const inferred = Symbol("inferred") +export const inferred: unique symbol = Symbol("inferred") diff --git a/ark/schema/structure/index.ts b/ark/schema/structure/index.ts new file mode 100644 index 0000000000..ff2169f535 --- /dev/null +++ b/ark/schema/structure/index.ts @@ -0,0 +1,146 @@ +import { + printable, + stringAndSymbolicEntriesOf, + throwParseError +} from "@arktype/util" +import { BaseConstraint } from "../constraint.js" +import type { Node, RootSchema } from "../kinds.js" +import type { BaseRoot } from "../roots/root.js" +import type { UnitNode } from "../roots/unit.js" +import type { BaseMeta, declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import { + implementNode, + type RootKind, + type nodeImplementationOf +} from "../shared/implement.js" +import { intersectNodes } from "../shared/intersections.js" +import type { TraverseAllows, TraverseApply } from "../shared/traversal.js" + +export type IndexKeyKind = Exclude + +export type IndexKeyNode = Node + +export interface IndexSchema extends BaseMeta { + readonly signature: RootSchema + readonly value: RootSchema +} + +export interface IndexInner extends BaseMeta { + readonly signature: IndexKeyNode + readonly value: BaseRoot +} + +export interface IndexDeclaration + extends declareNode<{ + kind: "index" + schema: IndexSchema + normalizedSchema: IndexSchema + inner: IndexInner + prerequisite: object + intersectionIsOpen: true + childKind: RootKind + }> {} + +export const indexImplementation: nodeImplementationOf = + implementNode({ + kind: "index", + hasAssociatedError: false, + intersectionIsOpen: true, + keys: { + signature: { + child: true, + parse: (schema, ctx) => { + const key = ctx.$.schema(schema) + if (!key.extends(ctx.$.keywords.propertyKey)) { + return throwParseError( + writeInvalidPropertyKeyMessage(key.expression) + ) + } + const enumerableBranches = key.branches.filter((b): b is UnitNode => + b.hasKind("unit") + ) + if (enumerableBranches.length) { + return throwParseError( + writeEnumerableIndexBranches( + enumerableBranches.map(b => printable(b.unit)) + ) + ) + } + return key as IndexKeyNode + } + }, + value: { + child: true, + parse: (schema, ctx) => ctx.$.schema(schema) + } + }, + normalize: schema => schema, + defaults: { + description: node => + `[${node.signature.expression}]: ${node.value.description}` + }, + intersections: { + index: (l, r, ctx) => { + if (l.signature.equals(r.signature)) { + const valueIntersection = intersectNodes(l.value, r.value, ctx) + const value = + valueIntersection instanceof Disjoint ? + ctx.$.keywords.never.raw + : valueIntersection + return ctx.$.node("index", { signature: l.signature, value }) + } + + // if r constrains all of l's keys to a subtype of l's value, r is a subtype of l + if (l.signature.extends(r.signature) && l.value.subsumes(r.value)) + return r + // if l constrains all of r's keys to a subtype of r's value, l is a subtype of r + if (r.signature.extends(l.signature) && r.value.subsumes(l.value)) + return l + + // other relationships between index signatures can't be generally reduced + return null + } + } + }) + +export class IndexNode extends BaseConstraint { + impliedBasis: BaseRoot = this.$.keywords.object.raw + expression = `[${this.signature.expression}]: ${this.value.expression}` + + traverseAllows: TraverseAllows = (data, ctx) => + stringAndSymbolicEntriesOf(data).every(entry => { + if (this.signature.traverseAllows(entry[0], ctx)) { + // ctx will be undefined if this node isn't context-dependent + ctx?.path.push(entry[0]) + const allowed = this.value.traverseAllows(entry[1], ctx) + ctx?.path.pop() + return allowed + } + return true + }) + + traverseApply: TraverseApply = (data, ctx) => + stringAndSymbolicEntriesOf(data).forEach(entry => { + if (this.signature.traverseAllows(entry[0], ctx)) { + ctx.path.push(entry[0]) + this.value.traverseApply(entry[1], ctx) + ctx.path.pop() + } + }) + + compile(): void { + // this is currently handled by StructureNode + } +} + +export const writeEnumerableIndexBranches = (keys: string[]): string => + `Index keys ${keys.join(", ")} should be specified as named props.` + +export const writeInvalidPropertyKeyMessage = ( + indexSchema: indexSchema +): writeInvalidPropertyKeyMessage => + `Indexed key definition '${indexSchema}' must be a string, number or symbol` + +export type writeInvalidPropertyKeyMessage = + `Indexed key definition '${indexSchema}' must be a string, number or symbol` diff --git a/ark/schema/structure/optional.ts b/ark/schema/structure/optional.ts new file mode 100644 index 0000000000..a5ceb102b9 --- /dev/null +++ b/ark/schema/structure/optional.ts @@ -0,0 +1,60 @@ +import type { declareNode } from "../shared/declare.js" +import { + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import { + BaseProp, + intersectProps, + type BasePropDeclaration, + type BasePropInner, + type BasePropSchema +} from "./prop.js" + +export interface OptionalSchema extends BasePropSchema { + default?: unknown +} + +export interface OptionalInner extends BasePropInner { + default?: unknown +} + +export type Default = ["=", v] + +export type DefaultableAst = (In?: t) => Default + +export type OptionalDeclaration = declareNode< + BasePropDeclaration<"optional"> & { + schema: OptionalSchema + normalizedSchema: OptionalSchema + inner: OptionalInner + } +> + +export const optionalImplementation: nodeImplementationOf = + implementNode({ + kind: "optional", + hasAssociatedError: false, + intersectionIsOpen: true, + keys: { + key: {}, + value: { + child: true, + parse: (schema, ctx) => ctx.$.schema(schema) + }, + default: { + preserveUndefined: true + } + }, + normalize: schema => schema, + defaults: { + description: node => `${node.compiledKey}?: ${node.value.description}` + }, + intersections: { + optional: intersectProps + } + }) + +export class OptionalNode extends BaseProp<"optional"> { + expression = `${this.compiledKey}?: ${this.value.expression}` +} diff --git a/ark/schema/structure/prop.ts b/ark/schema/structure/prop.ts new file mode 100644 index 0000000000..77eae2f390 --- /dev/null +++ b/ark/schema/structure/prop.ts @@ -0,0 +1,150 @@ +import { + compileSerializedValue, + printable, + registeredReference, + throwParseError, + unset, + type Key +} from "@arktype/util" +import { BaseConstraint } from "../constraint.js" +import type { Node, RootSchema } from "../kinds.js" +import type { Morph } from "../roots/morph.js" +import type { BaseRoot } from "../roots/root.js" +import type { NodeCompiler } from "../shared/compile.js" +import type { BaseMeta } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import type { IntersectionContext, RootKind } from "../shared/implement.js" +import { intersectNodes } from "../shared/intersections.js" +import type { TraverseAllows, TraverseApply } from "../shared/traversal.js" +import type { OptionalDeclaration, OptionalNode } from "./optional.js" +import type { RequiredDeclaration } from "./required.js" + +export type PropKind = "required" | "optional" + +export type PropNode = Node + +export interface BasePropSchema extends BaseMeta { + readonly key: Key + readonly value: RootSchema +} + +export interface BasePropInner extends BasePropSchema { + readonly value: BaseRoot +} + +export type BasePropDeclaration = { + kind: kind + prerequisite: object + intersectionIsOpen: true + childKind: RootKind +} + +export const intersectProps = ( + l: Node, + r: Node, + ctx: IntersectionContext +): Node | Disjoint | null => { + if (l.key !== r.key) return null + + const key = l.key + let value = intersectNodes(l.value, r.value, ctx) + const kind: PropKind = l.required || r.required ? "required" : "optional" + if (value instanceof Disjoint) { + if (kind === "optional") value = ctx.$.keywords.never.raw + else return value.withPrefixKey(l.compiledKey) + } + + if (kind === "required") { + return ctx.$.node("required", { + key, + value + }) + } + + const defaultIntersection = + l.hasDefault() ? + r.hasDefault() ? + l.default === r.default ? + l.default + : throwParseError( + `Invalid intersection of default values ${printable(l.default)} & ${printable(r.default)}` + ) + : l.default + : r.hasDefault() ? r.default + : unset + + return ctx.$.node("optional", { + key, + value, + // unset is stripped during parsing + default: defaultIntersection + }) +} + +export abstract class BaseProp< + kind extends PropKind = PropKind +> extends BaseConstraint< + kind extends "required" ? RequiredDeclaration : OptionalDeclaration +> { + required: boolean = this.kind === "required" + impliedBasis: BaseRoot = this.$.keywords.object.raw + serializedKey: string = compileSerializedValue(this.key) + compiledKey: string = + typeof this.key === "string" ? this.key : this.serializedKey + + private defaultValueMorphs: Morph[] = [ + data => { + data[this.key] = (this as OptionalNode).default + return data + } + ] + + private defaultValueMorphsReference = registeredReference( + this.defaultValueMorphs + ) + + hasDefault(): this is OptionalNode & { default: unknown } { + return "default" in this + } + + traverseAllows: TraverseAllows = (data, ctx) => { + if (this.key in data) { + // ctx will be undefined if this node isn't context-dependent + ctx?.path.push(this.key) + const allowed = this.value.traverseAllows((data as any)[this.key], ctx) + ctx?.path.pop() + return allowed + } + return !this.required + } + + traverseApply: TraverseApply = (data, ctx) => { + if (this.key in data) { + ctx.path.push(this.key) + this.value.traverseApply((data as any)[this.key], ctx) + ctx.path.pop() + } else if (this.hasKind("required")) ctx.error(this.errorContext) + else if (this.hasKind("optional") && this.hasDefault()) + ctx.queueMorphs(this.defaultValueMorphs) + } + + compile(js: NodeCompiler): void { + js.if(`${this.serializedKey} in data`, () => + js.traverseKey(this.serializedKey, `data${js.prop(this.key)}`, this.value) + ) + + if (this.hasKind("required")) { + js.else(() => { + if (js.traversalKind === "Apply") + return js.line(`ctx.error(${this.compiledErrorContext})`) + else return js.return(false) + }) + } else if (js.traversalKind === "Apply" && "default" in this) { + js.else(() => + js.line(`ctx.queueMorphs(${this.defaultValueMorphsReference})`) + ) + } + + if (js.traversalKind === "Allows") js.return(true) + } +} diff --git a/ark/schema/structure/required.ts b/ark/schema/structure/required.ts new file mode 100644 index 0000000000..95ee71344b --- /dev/null +++ b/ark/schema/structure/required.ts @@ -0,0 +1,67 @@ +import type { BaseErrorContext, declareNode } from "../shared/declare.js" +import type { ArkErrorContextInput } from "../shared/errors.js" +import { + compileErrorContext, + implementNode, + type nodeImplementationOf +} from "../shared/implement.js" +import { + BaseProp, + intersectProps, + type BasePropDeclaration, + type BasePropInner, + type BasePropSchema +} from "./prop.js" + +export interface RequiredErrorContext extends BaseErrorContext<"required"> { + missingValueDescription: string +} + +export interface RequiredSchema extends BasePropSchema {} + +export interface RequiredInner extends BasePropInner {} + +export type RequiredDeclaration = declareNode< + BasePropDeclaration<"required"> & { + schema: RequiredSchema + normalizedSchema: RequiredSchema + inner: RequiredInner + errorContext: RequiredErrorContext + } +> + +export class RequiredNode extends BaseProp<"required"> { + expression = `${this.compiledKey}: ${this.value.expression}` + + errorContext: ArkErrorContextInput<"required"> = Object.freeze({ + code: "required", + missingValueDescription: this.value.description, + relativePath: [this.key] + }) + + compiledErrorContext: string = compileErrorContext(this.errorContext) +} + +export const requiredImplementation: nodeImplementationOf = + implementNode({ + kind: "required", + hasAssociatedError: true, + intersectionIsOpen: true, + keys: { + key: {}, + value: { + child: true, + parse: (schema, ctx) => ctx.$.schema(schema) + } + }, + normalize: schema => schema, + defaults: { + description: node => `${node.compiledKey}: ${node.value.description}`, + expected: ctx => ctx.missingValueDescription, + actual: () => "missing" + }, + intersections: { + required: intersectProps, + optional: intersectProps + } + }) diff --git a/ark/schema/structure/sequence.ts b/ark/schema/structure/sequence.ts new file mode 100644 index 0000000000..7474e1bdec --- /dev/null +++ b/ark/schema/structure/sequence.ts @@ -0,0 +1,458 @@ +import { + append, + throwInternalError, + throwParseError, + type array, + type mutable, + type satisfy +} from "@arktype/util" +import { BaseConstraint } from "../constraint.js" +import type { MutableInner, RootSchema } from "../kinds.js" +import type { MaxLengthNode } from "../refinements/maxLength.js" +import type { MinLengthNode } from "../refinements/minLength.js" +import type { BaseRoot } from "../roots/root.js" +import type { NodeCompiler } from "../shared/compile.js" +import type { BaseMeta, declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import { + implementNode, + type IntersectionContext, + type NodeKeyImplementation, + type RootKind, + type nodeImplementationOf +} from "../shared/implement.js" +import { intersectNodes } from "../shared/intersections.js" +import type { TraverseAllows, TraverseApply } from "../shared/traversal.js" + +export interface NormalizedSequenceSchema extends BaseMeta { + readonly prefix?: array + readonly optionals?: array + readonly variadic?: RootSchema + readonly minVariadicLength?: number + readonly postfix?: array +} + +export type SequenceSchema = NormalizedSequenceSchema | RootSchema + +export interface SequenceInner extends BaseMeta { + // a list of fixed position elements starting at index 0 + readonly prefix?: array + // a list of optional elements following prefix + readonly optionals?: array + // the variadic element (only checked if all optional elements are present) + readonly variadic?: BaseRoot + readonly minVariadicLength?: number + // a list of fixed position elements, the last being the last element of the array + readonly postfix?: array +} + +export interface SequenceDeclaration + extends declareNode<{ + kind: "sequence" + schema: SequenceSchema + normalizedSchema: NormalizedSequenceSchema + inner: SequenceInner + prerequisite: array + reducibleTo: "sequence" + childKind: RootKind + }> {} + +const fixedSequenceKeySchemaDefinition: NodeKeyImplementation< + SequenceDeclaration, + "prefix" | "postfix" | "optionals" +> = { + child: true, + parse: (schema, ctx) => + schema.length === 0 ? + // empty affixes are omitted. an empty array should therefore + // be specified as `{ proto: Array, length: 0 }` + undefined + : schema.map(element => ctx.$.schema(element)) +} + +export const sequenceImplementation: nodeImplementationOf = + implementNode({ + kind: "sequence", + hasAssociatedError: false, + collapsibleKey: "variadic", + keys: { + prefix: fixedSequenceKeySchemaDefinition, + optionals: fixedSequenceKeySchemaDefinition, + variadic: { + child: true, + parse: (schema, ctx) => ctx.$.schema(schema, ctx) + }, + minVariadicLength: { + // minVariadicLength is reflected in the id of this node, + // but not its IntersectionNode parent since it is superceded by the minLength + // node it implies + parse: min => (min === 0 ? undefined : min) + }, + postfix: fixedSequenceKeySchemaDefinition + }, + normalize: schema => { + if (typeof schema === "string") return { variadic: schema } + + if ( + "variadic" in schema || + "prefix" in schema || + "optionals" in schema || + "postfix" in schema || + "minVariadicLength" in schema + ) { + if (schema.postfix?.length) { + if (!schema.variadic) + return throwParseError(postfixWithoutVariadicMessage) + + if (schema.optionals?.length) + return throwParseError(postfixFollowingOptionalMessage) + } + if (schema.minVariadicLength && !schema.variadic) { + return throwParseError( + "minVariadicLength may not be specified without a variadic element" + ) + } + return schema + } + return { variadic: schema } + }, + reduce: (raw, $) => { + let minVariadicLength = raw.minVariadicLength ?? 0 + const prefix = raw.prefix?.slice() ?? [] + const optional = raw.optionals?.slice() ?? [] + const postfix = raw.postfix?.slice() ?? [] + if (raw.variadic) { + // optional elements equivalent to the variadic parameter are redundant + while (optional.at(-1)?.equals(raw.variadic)) optional.pop() + + if (optional.length === 0) { + // If there are no optional, normalize prefix + // elements adjacent and equivalent to variadic: + // { variadic: number, prefix: [string, number] } + // reduces to: + // { variadic: number, prefix: [string], minVariadicLength: 1 } + while (prefix.at(-1)?.equals(raw.variadic)) { + prefix.pop() + minVariadicLength++ + } + } + // Normalize postfix elements adjacent and equivalent to variadic: + // { variadic: number, postfix: [number, number, 5] } + // reduces to: + // { variadic: number, postfix: [5], minVariadicLength: 2 } + while (postfix[0]?.equals(raw.variadic)) { + postfix.shift() + minVariadicLength++ + } + } else if (optional.length === 0) { + // if there's no variadic or optional parameters, + // postfix can just be appended to prefix + prefix.push(...postfix.splice(0)) + } + if ( + // if any variadic adjacent elements were moved to minVariadicLength + minVariadicLength !== raw.minVariadicLength || + // or any postfix elements were moved to prefix + (raw.prefix && raw.prefix.length !== prefix.length) + ) { + // reparse the reduced def + return $.node( + "sequence", + { + ...raw, + // empty lists will be omitted during parsing + prefix, + postfix, + optionals: optional, + minVariadicLength + }, + { prereduced: true } + ) + } + }, + defaults: { + description: node => { + if (node.isVariadicOnly) return `${node.variadic!.nestableExpression}[]` + const innerDescription = node.tuple + .map(element => + element.kind === "optionals" ? `${element.node.nestableExpression}?` + : element.kind === "variadic" ? + `...${element.node.nestableExpression}[]` + : element.node.expression + ) + .join(", ") + return `[${innerDescription}]` + } + }, + intersections: { + sequence: (l, r, ctx) => { + const rootState = _intersectSequences({ + l: l.tuple, + r: r.tuple, + disjoint: new Disjoint({}), + result: [], + fixedVariants: [], + ctx + }) + + const viableBranches = + rootState.disjoint.isEmpty() ? + [rootState, ...rootState.fixedVariants] + : rootState.fixedVariants + + return ( + viableBranches.length === 0 ? rootState.disjoint! + : viableBranches.length === 1 ? + ctx.$.node( + "sequence", + sequenceTupleToInner(viableBranches[0].result) + ) + : ctx.$.node( + "union", + viableBranches.map(state => ({ + proto: Array, + sequence: sequenceTupleToInner(state.result) + })) + ) + ) + } + + // exactLength, minLength, and maxLength don't need to be defined + // here since impliedSiblings guarantees they will be added + // directly to the IntersectionNode parent of the SequenceNode + // they exist on + } + }) + +export class SequenceNode extends BaseConstraint { + impliedBasis: BaseRoot = this.$.keywords.Array.raw + prefix: array = this.inner.prefix ?? [] + optionals: array = this.inner.optionals ?? [] + prevariadic: BaseRoot[] = [...this.prefix, ...this.optionals] + postfix: array = this.inner.postfix ?? [] + isVariadicOnly: boolean = this.prevariadic.length + this.postfix.length === 0 + minVariadicLength: number = this.inner.minVariadicLength ?? 0 + minLength: number = + this.prefix.length + this.minVariadicLength + this.postfix.length + minLengthNode: MinLengthNode | null = + this.minLength === 0 ? null : this.$.node("minLength", this.minLength) + maxLength: number | null = + this.variadic ? null : this.minLength + this.optionals.length + maxLengthNode: MaxLengthNode | null = + this.maxLength === null ? null : this.$.node("maxLength", this.maxLength) + impliedSiblings: array = + this.minLengthNode ? + this.maxLengthNode ? + [this.minLengthNode, this.maxLengthNode] + : [this.minLengthNode] + : this.maxLengthNode ? [this.maxLengthNode] + : [] + + protected childAtIndex(data: array, index: number): BaseRoot { + if (index < this.prevariadic.length) return this.prevariadic[index] + const firstPostfixIndex = data.length - this.postfix.length + if (index >= firstPostfixIndex) + return this.postfix[index - firstPostfixIndex] + return ( + this.variadic ?? + throwInternalError( + `Unexpected attempt to access index ${index} on ${this}` + ) + ) + } + + // minLength/maxLength should be checked by Intersection before either traversal + traverseAllows: TraverseAllows = (data, ctx) => { + for (let i = 0; i < data.length; i++) + if (!this.childAtIndex(data, i).traverseAllows(data[i], ctx)) return false + + return true + } + + traverseApply: TraverseApply = (data, ctx) => { + for (let i = 0; i < data.length; i++) { + ctx.path.push(i) + this.childAtIndex(data, i).traverseApply(data[i], ctx) + ctx.path.pop() + } + } + + // minLength/maxLength compilation should be handled by Intersection + compile(js: NodeCompiler): void { + this.prefix.forEach((node, i) => js.traverseKey(`${i}`, `data[${i}]`, node)) + this.optionals.forEach((node, i) => { + const dataIndex = `${i + this.prefix.length}` + js.if(`${dataIndex} >= ${js.data}.length`, () => + js.traversalKind === "Allows" ? js.return(true) : js.return() + ) + js.traverseKey(dataIndex, `data[${dataIndex}]`, node) + }) + + if (this.variadic) { + if (this.postfix.length) { + js.const( + "firstPostfixIndex", + `${js.data}.length${this.postfix.length ? `- ${this.postfix.length}` : ""}` + ) + } + js.for( + `i < ${this.postfix.length ? "firstPostfixIndex" : "data.length"}`, + () => js.traverseKey("i", "data[i]", this.variadic!), + this.prevariadic.length + ) + this.postfix.forEach((node, i) => { + const keyExpression = `firstPostfixIndex + ${i}` + js.traverseKey(keyExpression, `data[${keyExpression}]`, node) + }) + } + + if (js.traversalKind === "Allows") js.return(true) + } + + tuple: SequenceTuple = sequenceInnerToTuple(this.inner) + // this depends on tuple so needs to come after it + expression: string = this.description +} + +const sequenceInnerToTuple = (inner: SequenceInner): SequenceTuple => { + const tuple: mutable = [] + inner.prefix?.forEach(node => tuple.push({ kind: "prefix", node })) + inner.optionals?.forEach(node => tuple.push({ kind: "optionals", node })) + if (inner.variadic) tuple.push({ kind: "variadic", node: inner.variadic }) + inner.postfix?.forEach(node => tuple.push({ kind: "postfix", node })) + return tuple +} + +const sequenceTupleToInner = (tuple: SequenceTuple): SequenceInner => + tuple.reduce>((result, node) => { + if (node.kind === "variadic") result.variadic = node.node + else result[node.kind] = append(result[node.kind], node.node) + + return result + }, {}) + +export const postfixFollowingOptionalMessage = + "A postfix required element cannot follow an optional element" + +export type postfixFollowingOptionalMessage = + typeof postfixFollowingOptionalMessage + +export const postfixWithoutVariadicMessage = + "A postfix element requires a variadic element" + +export type postfixWithoutVariadicMessage = typeof postfixWithoutVariadicMessage + +export type SequenceElementKind = satisfy< + keyof SequenceInner, + "prefix" | "optionals" | "variadic" | "postfix" +> + +export type SequenceElement = { + kind: SequenceElementKind + node: BaseRoot +} +export type SequenceTuple = array + +type SequenceIntersectionState = { + l: SequenceTuple + r: SequenceTuple + disjoint: Disjoint + result: SequenceTuple + fixedVariants: SequenceIntersectionState[] + ctx: IntersectionContext +} + +const _intersectSequences = ( + s: SequenceIntersectionState +): SequenceIntersectionState => { + const [lHead, ...lTail] = s.l + const [rHead, ...rTail] = s.r + + if (!lHead || !rHead) return s + + const lHasPostfix = lTail.at(-1)?.kind === "postfix" + const rHasPostfix = rTail.at(-1)?.kind === "postfix" + + const kind: SequenceElementKind = + lHead.kind === "prefix" || rHead.kind === "prefix" ? "prefix" + : lHead.kind === "optionals" || rHead.kind === "optionals" ? + // if either operand has postfix elements, the full-length + // intersection can't include optional elements (though they may + // exist in some of the fixed length variants) + lHasPostfix || rHasPostfix ? + "prefix" + : "optionals" + : lHead.kind === "postfix" || rHead.kind === "postfix" ? "postfix" + : "variadic" + + if (lHead.kind === "prefix" && rHead.kind === "variadic" && rHasPostfix) { + const postfixBranchResult = _intersectSequences({ + ...s, + fixedVariants: [], + r: rTail.map(element => ({ ...element, kind: "prefix" })) + }) + if (postfixBranchResult.disjoint.isEmpty()) + s.fixedVariants.push(postfixBranchResult) + } else if ( + rHead.kind === "prefix" && + lHead.kind === "variadic" && + lHasPostfix + ) { + const postfixBranchResult = _intersectSequences({ + ...s, + fixedVariants: [], + l: lTail.map(element => ({ ...element, kind: "prefix" })) + }) + if (postfixBranchResult.disjoint.isEmpty()) + s.fixedVariants.push(postfixBranchResult) + } + + const result = intersectNodes(lHead.node, rHead.node, s.ctx) + if (result instanceof Disjoint) { + if (kind === "prefix" || kind === "postfix") { + s.disjoint.add( + result.withPrefixKey( + // TODO: more precise path handling for Disjoints + kind === "prefix" ? `${s.result.length}` : `-${lTail.length + 1}` + ) + ) + s.result = [...s.result, { kind, node: s.ctx.$.keywords.never.raw }] + } else if (kind === "optionals") { + // if the element result is optional and unsatisfiable, the + // intersection can still be satisfied as long as the tuple + // ends before the disjoint element would occur + return s + } else { + // if the element is variadic and unsatisfiable, the intersection + // can be satisfied with a fixed length variant including zero + // variadic elements + return _intersectSequences({ + ...s, + fixedVariants: [], + // if there were any optional elements, there will be no postfix elements + // so this mapping will never occur (which would be illegal otherwise) + l: lTail.map(element => ({ ...element, kind: "prefix" })), + r: lTail.map(element => ({ ...element, kind: "prefix" })) + }) + } + } else s.result = [...s.result, { kind, node: result }] + + const lRemaining = s.l.length + const rRemaining = s.r.length + + if ( + lHead.kind !== "variadic" || + (lRemaining >= rRemaining && + (rHead.kind === "variadic" || rRemaining === 1)) + ) + s.l = lTail + + if ( + rHead.kind !== "variadic" || + (rRemaining >= lRemaining && + (lHead.kind === "variadic" || lRemaining === 1)) + ) + s.r = rTail + + return _intersectSequences(s) +} diff --git a/ark/schema/structure/shared.ts b/ark/schema/structure/shared.ts new file mode 100644 index 0000000000..f37eb45484 --- /dev/null +++ b/ark/schema/structure/shared.ts @@ -0,0 +1,6 @@ +import { registeredReference } from "@arktype/util" + +export const arrayIndexMatcher: RegExp = /(?:0|(?:[1-9]\\d*))$/ + +export const arrayIndexMatcherReference: `$ark.${string}` = + registeredReference(arrayIndexMatcher) diff --git a/ark/schema/structure/structure.ts b/ark/schema/structure/structure.ts new file mode 100644 index 0000000000..7aaeb70cbc --- /dev/null +++ b/ark/schema/structure/structure.ts @@ -0,0 +1,483 @@ +import { + append, + cached, + flatMorph, + registeredReference, + spliterate, + type array, + type Key, + type RegisteredReference +} from "@arktype/util" +import { + BaseConstraint, + constraintKeyParser, + flattenConstraints, + intersectConstraints +} from "../constraint.js" +import type { MutableInner } from "../kinds.js" +import type { BaseRoot } from "../roots/root.js" +import type { UnitNode } from "../roots/unit.js" +import type { RawRootScope } from "../scope.js" +import type { NodeCompiler } from "../shared/compile.js" +import type { BaseMeta, declareNode } from "../shared/declare.js" +import { Disjoint } from "../shared/disjoint.js" +import { + implementNode, + type nodeImplementationOf, + type StructuralKind +} from "../shared/implement.js" +import { intersectNodesRoot } from "../shared/intersections.js" +import type { + TraversalContext, + TraversalKind, + TraverseAllows, + TraverseApply +} from "../shared/traversal.js" +import { makeRootAndArrayPropertiesMutable } from "../shared/utils.js" +import type { IndexNode, IndexSchema } from "./index.js" +import type { OptionalNode, OptionalSchema } from "./optional.js" +import type { PropNode } from "./prop.js" +import type { RequiredNode, RequiredSchema } from "./required.js" +import type { SequenceNode, SequenceSchema } from "./sequence.js" +import { arrayIndexMatcher, arrayIndexMatcherReference } from "./shared.js" + +export type UndeclaredKeyBehavior = "ignore" | UndeclaredKeyHandling + +export type UndeclaredKeyHandling = "reject" | "delete" + +export interface StructureSchema extends BaseMeta { + readonly optional?: readonly OptionalSchema[] + readonly required?: readonly RequiredSchema[] + readonly index?: readonly IndexSchema[] + readonly sequence?: SequenceSchema + readonly undeclared?: UndeclaredKeyBehavior +} + +export interface StructureInner extends BaseMeta { + readonly optional?: readonly OptionalNode[] + readonly required?: readonly RequiredNode[] + readonly index?: readonly IndexNode[] + readonly sequence?: SequenceNode + readonly undeclared?: UndeclaredKeyHandling +} + +export interface StructureDeclaration + extends declareNode<{ + kind: "structure" + schema: StructureSchema + normalizedSchema: StructureSchema + inner: StructureInner + prerequisite: object + childKind: StructuralKind + }> {} + +export class StructureNode extends BaseConstraint { + impliedBasis: BaseRoot = this.$.keywords.object.raw + impliedSiblings = this.children.flatMap( + n => (n.impliedSiblings as BaseConstraint[]) ?? [] + ) + + props: array = + this.required ? + this.optional ? + [...this.required, ...this.optional] + : this.required + : this.optional ?? [] + + propsByKey: Record = flatMorph( + this.props, + (i, node) => [node.key, node] as const + ) + + propsByKeyReference: RegisteredReference = registeredReference( + this.propsByKey + ) + + expression: string = structuralExpression(this) + + requiredLiteralKeys: Key[] = this.required?.map(node => node.key) ?? [] + + optionalLiteralKeys: Key[] = this.optional?.map(node => node.key) ?? [] + + literalKeys: Key[] = [ + ...this.requiredLiteralKeys, + ...this.optionalLiteralKeys + ] + + @cached + keyof(): BaseRoot { + let branches = this.$.units(this.literalKeys).branches + this.index?.forEach(({ signature: index }) => { + branches = branches.concat(index.branches) + }) + return this.$.node("union", branches) + } + + readonly exhaustive: boolean = + this.undeclared !== undefined || this.index !== undefined + + omit(...keys: array): StructureNode { + return this.$.node("structure", omitFromInner(this.inner, keys)) + } + + merge(r: StructureNode): StructureNode { + const inner = makeRootAndArrayPropertiesMutable( + omitFromInner(this.inner, [r.keyof()]) + ) + if (r.required) inner.required = append(inner.required, r.required) + if (r.optional) inner.optional = append(inner.optional, r.optional) + if (r.index) inner.index = append(inner.index, r.index) + if (r.sequence) inner.sequence = r.sequence + if (r.undeclared) inner.undeclared = r.undeclared + else delete inner.undeclared + return this.$.node("structure", inner) + } + + traverseAllows: TraverseAllows = (data, ctx) => + this._traverse("Allows", data, ctx) + + traverseApply: TraverseApply = (data, ctx) => + this._traverse("Apply", data, ctx) + + protected _traverse = ( + traversalKind: TraversalKind, + data: object, + ctx: TraversalContext + ): boolean => { + const errorCount = ctx?.currentErrorCount ?? 0 + for (let i = 0; i < this.props.length; i++) { + if (traversalKind === "Allows") { + if (!this.props[i].traverseAllows(data, ctx)) return false + } else { + this.props[i].traverseApply(data as never, ctx) + if (ctx.failFast && ctx.currentErrorCount > errorCount) return false + } + } + + if (this.sequence) { + if (traversalKind === "Allows") { + if (!this.sequence.traverseAllows(data as never, ctx)) return false + } else { + this.sequence.traverseApply(data as never, ctx) + if (ctx.failFast && ctx.currentErrorCount > errorCount) return false + } + } + + if (!this.exhaustive) return true + + const keys: Key[] = Object.keys(data) + keys.push(...Object.getOwnPropertySymbols(data)) + + for (let i = 0; i < keys.length; i++) { + const k = keys[i] + + let matched = false + + if (this.index) { + for (const node of this.index) { + if (node.signature.traverseAllows(k, ctx)) { + if (traversalKind === "Allows") { + ctx?.path.push(k) + const result = node.value.traverseAllows(data[k as never], ctx) + ctx?.path.pop() + if (!result) return false + } else { + ctx.path.push(k) + node.value.traverseApply(data[k as never], ctx) + ctx.path.pop() + if (ctx.failFast && ctx.currentErrorCount > errorCount) + return false + } + + matched = true + } + } + } + + if (this.undeclared) { + matched ||= k in this.propsByKey + matched ||= + this.sequence !== undefined && + typeof k === "string" && + arrayIndexMatcher.test(k) + if (!matched) { + if (traversalKind === "Allows") return false + if (this.undeclared === "reject") + ctx.error({ expected: "removed", actual: null, relativePath: [k] }) + else { + ctx.queueMorphs([ + data => { + delete data[k] + return data + } + ]) + } + + if (ctx.failFast) return false + } + } + + ctx?.path.pop() + } + + return true + } + + compile(js: NodeCompiler): void { + if (js.traversalKind === "Apply") js.initializeErrorCount() + + this.props.forEach(prop => { + js.check(prop) + if (js.traversalKind === "Apply") js.returnIfFailFast() + }) + + if (this.sequence) { + js.check(this.sequence) + if (js.traversalKind === "Apply") js.returnIfFailFast() + } + + if (this.exhaustive) { + js.const("keys", "Object.keys(data)") + js.line("keys.push(...Object.getOwnPropertySymbols(data))") + js.for("i < keys.length", () => this.compileExhaustiveEntry(js)) + } + + if (js.traversalKind === "Allows") js.return(true) + } + + protected compileExhaustiveEntry(js: NodeCompiler): NodeCompiler { + js.const("k", "keys[i]") + + if (this.undeclared) js.let("matched", false) + + this.index?.forEach(node => { + js.if( + `${js.invoke(node.signature, { arg: "k", kind: "Allows" })}`, + () => { + js.traverseKey("k", "data[k]", node.value) + if (this.undeclared) js.set("matched", true) + return js + } + ) + }) + + if (this.undeclared) { + if (this.props?.length !== 0) + js.line(`matched ||= k in ${this.propsByKeyReference}`) + + if (this.sequence) { + js.line( + `matched ||= typeof k === "string" && ${arrayIndexMatcherReference}.test(k)` + ) + } + + js.if("!matched", () => { + if (js.traversalKind === "Allows") return js.return(false) + return this.undeclared === "reject" ? + js + .line( + `ctx.error({ expected: "removed", actual: null, relativePath: [k] })` + ) + .if("ctx.failFast", () => js.return()) + : js.line(`ctx.queueMorphs([data => { delete data[k]; return data }])`) + }) + } + + return js + } +} + +const omitFromInner = ( + inner: StructureInner, + keys: array +): StructureInner => { + const result = { ...inner } + keys.forEach(k => { + if (result.required) { + result.required = result.required.filter(b => + typeof k === "function" ? !k.allows(b.key) : k !== b.key + ) + } + if (result.optional) { + result.optional = result.optional.filter(b => + typeof k === "function" ? !k.allows(b.key) : k !== b.key + ) + } + if (result.index && typeof k === "function") { + // we only have to filter index nodes if the input was a node, as + // literal keys should never subsume an index + result.index = result.index.filter(n => !n.signature.extends(k)) + } + }) + return result +} + +const createStructuralWriter = + (childStringProp: "expression" | "description") => (node: StructureNode) => { + if (node.props.length || node.index) { + const parts = node.index?.map(String) ?? [] + node.props.forEach(node => parts.push(node[childStringProp])) + + if (node.undeclared) parts.push(`+ (undeclared): ${node.undeclared}`) + + const objectLiteralDescription = `{ ${parts.join(", ")} }` + return node.sequence ? + `${objectLiteralDescription} & ${node.sequence.description}` + : objectLiteralDescription + } + return node.sequence?.description ?? "{}" + } + +const structuralDescription = createStructuralWriter("description") +const structuralExpression = createStructuralWriter("expression") + +export const structureImplementation: nodeImplementationOf = + implementNode({ + kind: "structure", + hasAssociatedError: false, + normalize: schema => schema, + keys: { + required: { + child: true, + parse: constraintKeyParser("required") + }, + optional: { + child: true, + parse: constraintKeyParser("optional") + }, + index: { + child: true, + parse: constraintKeyParser("index") + }, + sequence: { + child: true, + parse: constraintKeyParser("sequence") + }, + undeclared: { + parse: behavior => (behavior === "ignore" ? undefined : behavior) + } + }, + defaults: { + description: structuralDescription + }, + intersections: { + structure: (l, r, ctx) => { + const lInner = { ...l.inner } + const rInner = { ...r.inner } + if (l.undeclared) { + const lKey = l.keyof() + const disjointRKeys = r.requiredLiteralKeys.filter( + k => !lKey.allows(k) + ) + if (disjointRKeys.length) { + return Disjoint.from( + "presence", + ctx.$.keywords.never.raw, + r.propsByKey[disjointRKeys[0]]!.value + ).withPrefixKey(disjointRKeys[0]) + } + + if (rInner.optional) + rInner.optional = rInner.optional.filter(n => lKey.allows(n.key)) + if (rInner.index) { + rInner.index = rInner.index.flatMap(n => { + if (n.signature.extends(lKey)) return n + const indexOverlap = intersectNodesRoot(lKey, n.signature, ctx.$) + if (indexOverlap instanceof Disjoint) return [] + const normalized = normalizeIndex(indexOverlap, n.value, ctx.$) + if (normalized.required) { + rInner.required = + rInner.required ? + [...rInner.required, ...normalized.required] + : normalized.required + } + return normalized.index ?? [] + }) + } + } + if (r.undeclared) { + const rKey = r.keyof() + const disjointLKeys = l.requiredLiteralKeys.filter( + k => !rKey.allows(k) + ) + if (disjointLKeys.length) { + return Disjoint.from( + "presence", + l.propsByKey[disjointLKeys[0]]!.value, + ctx.$.keywords.never.raw + ).withPrefixKey(disjointLKeys[0]) + } + + if (lInner.optional) + lInner.optional = lInner.optional.filter(n => rKey.allows(n.key)) + if (lInner.index) { + lInner.index = lInner.index.flatMap(n => { + if (n.signature.extends(rKey)) return n + const indexOverlap = intersectNodesRoot(rKey, n.signature, ctx.$) + if (indexOverlap instanceof Disjoint) return [] + const normalized = normalizeIndex(indexOverlap, n.value, ctx.$) + if (normalized.required) { + lInner.required = + lInner.required ? + [...lInner.required, ...normalized.required] + : normalized.required + } + return normalized.index ?? [] + }) + } + } + + const baseInner: MutableInner<"structure"> = {} + + if (l.undeclared || r.undeclared) { + baseInner.undeclared = + l.undeclared === "reject" || r.undeclared === "reject" ? + "reject" + : "delete" + } + + return intersectConstraints({ + kind: "structure", + baseInner, + l: flattenConstraints(lInner), + r: flattenConstraints(rInner), + roots: [], + ctx + }) + } + } + }) + +export type NormalizedIndex = { + index?: IndexNode + required?: RequiredNode[] +} + +/** extract enumerable named props from an index signature */ +export const normalizeIndex = ( + signature: BaseRoot, + value: BaseRoot, + $: RawRootScope +): NormalizedIndex => { + const [enumerableBranches, nonEnumerableBranches] = spliterate( + signature.branches, + (k): k is UnitNode => k.hasKind("unit") + ) + + if (!enumerableBranches.length) + return { index: $.node("index", { signature, value }) } + + const normalized: NormalizedIndex = {} + + normalized.required = enumerableBranches.map(n => + $.node("required", { key: n.unit as Key, value }) + ) + if (nonEnumerableBranches.length) { + normalized.index = $.node("index", { + signature: nonEnumerableBranches, + value + }) + } + + return normalized +} diff --git a/ark/schema/tsconfig.build.json b/ark/schema/tsconfig.build.json index 80a796b963..f74ef64d4c 120000 --- a/ark/schema/tsconfig.build.json +++ b/ark/schema/tsconfig.build.json @@ -1 +1 @@ -../repo/tsconfig.build.json \ No newline at end of file +../repo/tsconfig.esm.json \ No newline at end of file diff --git a/ark/type/__tests__/array.test.ts b/ark/type/__tests__/array.test.ts index 28479c93ed..2f5209b1c8 100644 --- a/ark/type/__tests__/array.test.ts +++ b/ark/type/__tests__/array.test.ts @@ -377,15 +377,12 @@ value at [1] must be a number (was false)`) const expected = type("number[]>=3") attest(t.json).equals(expected.json) }) + + it("multiple errors", () => { + const stringArray = type("string[]") + attest(stringArray([1, 2]).toString()) + .snap(`value at [0] must be a string (was number) +value at [1] must be a string (was number)`) + }) } ) - -// TODO: reenable -// describe("traversal", () => { -// it("multiple errors", () => { -// const stringArray = type("string[]") -// attest(stringArray([1, 2]).toString()).snap( -// "Item at index 0 must be a string (was number)\nItem at index 1 must be a string (was number)" -// ) -// }) -// }) diff --git a/ark/type/__tests__/badDefinitionType.test.ts b/ark/type/__tests__/badDefinitionType.test.ts index 2114205c7d..ede42aee53 100644 --- a/ark/type/__tests__/badDefinitionType.test.ts +++ b/ark/type/__tests__/badDefinitionType.test.ts @@ -51,6 +51,12 @@ contextualize(() => { attest<{ bad: any }>(t.infer) }) + it("never", () => { + // can't error + const t = type({ bad: {} as never }) + attest<{ bad: never }>(t.infer) + }) + it("unknown", () => { // @ts-expect-error just results in base completions, so we just check there's an error attest(() => type({ bad: {} as unknown })).type.errors("") diff --git a/ark/type/__tests__/bounds.test.ts b/ark/type/__tests__/bounds.test.ts index d2521ddbc8..fe7719d399 100644 --- a/ark/type/__tests__/bounds.test.ts +++ b/ark/type/__tests__/bounds.test.ts @@ -1,5 +1,5 @@ import { attest, contextualize } from "@arktype/attest" -import { rawSchema, writeUnboundableMessage } from "@arktype/schema" +import { rawRoot, writeUnboundableMessage } from "@arktype/schema" import { writeMalformedNumericLiteralMessage } from "@arktype/util" import { type } from "arktype" import { writeDoubleRightBoundMessage } from "../parser/semantic/bounds.js" @@ -19,7 +19,7 @@ contextualize( it(">", () => { const t = type("number>0") attest(t.infer) - attest(t).type.toString.snap() + attest(t).type.toString.snap("Type, {}>") attest(t.json).snap({ domain: "number", min: { exclusive: true, rule: 0 } @@ -29,8 +29,8 @@ contextualize( it("<", () => { const t = type("number<10") attest(t.infer) - attest(t).type.toString.snap() - const expected = rawSchema({ + attest(t).type.toString.snap("Type, {}>") + const expected = rawRoot({ domain: "number", max: { rule: 10, exclusive: true } }) @@ -40,8 +40,8 @@ contextualize( it("<=", () => { const t = type("number<=-49") attest(t.infer) - attest(t).type.toString.snap() - const expected = rawSchema({ + attest(t).type.toString.snap("Type, {}>") + const expected = rawRoot({ domain: "number", max: { rule: -49, exclusive: false } }) @@ -51,16 +51,16 @@ contextualize( it("==", () => { const t = type("number==3211993") attest<3211993>(t.infer) - attest(t).type.toString.snap() - const expected = rawSchema({ unit: 3211993 }) + attest(t).type.toString.snap("Type<3211993, {}>") + const expected = rawRoot({ unit: 3211993 }) attest(t.json).equals(expected.json) }) it("<,<=", () => { const t = type("-5 & AtMost<5>>, {}>") attest(t.infer) - const expected = rawSchema({ + const expected = rawRoot({ domain: "number", min: { rule: -5, exclusive: true }, max: 5 @@ -70,9 +70,11 @@ contextualize( it("<=,<", () => { const t = type("-3.23<=number<4.654") - attest(t).type.toString.snap() + attest(t).type.toString.snap( + "Type & LessThan<4.654>>, {}>" + ) attest(t.infer) - const expected = rawSchema({ + const expected = rawRoot({ domain: "number", min: { rule: -3.23 }, max: { rule: 4.654, exclusive: true } @@ -82,9 +84,9 @@ contextualize( it("whitespace following comparator", () => { const t = type("number > 3") - attest(t).type.toString.snap() + attest(t).type.toString.snap("Type, {}>") attest(t.infer) - const expected = rawSchema({ + const expected = rawRoot({ domain: "number", min: { rule: 3, exclusive: true } }) @@ -94,7 +96,7 @@ contextualize( it("single Date", () => { const t = type("Date(t.infer) - attest(t).type.toString.snap() + attest(t).type.toString.snap('Type, {}>') attest(t.json).snap({ proto: "Date", before: { exclusive: true, rule: "2023-01-12T05:00:00.000Z" } @@ -104,7 +106,7 @@ contextualize( it("Date equality", () => { const t = type("Date==d'2020-1-1'") attest(t.infer) - attest(t).type.toString.snap() + attest(t).type.toString.snap('Type, {}>') attest(t.json).snap({ unit: "2020-01-01T05:00:00.000Z" }) attest(t.allows(new Date("2020/01/01"))).equals(true) attest(t.allows(new Date("2020/01/02"))).equals(false) @@ -113,7 +115,9 @@ contextualize( it("double Date", () => { const t = type("d'2001/10/10'(t.infer) - attest(t).type.toString.snap() + attest(t).type.toString.snap( + 'Type & Before<"2005/10/10">>, {}>' + ) attest(t.json).snap({ proto: "Date", before: { exclusive: true, rule: "2005-10-10T04:00:00.000Z" }, @@ -128,7 +132,9 @@ contextualize( const now = new Date() const t = type(`d'2000'(t.infer) - attest(t).type.toString.snap() + attest(t).type.toString.snap( + 'Type & AtOrBefore>, {}>' + ) attest(t.allows(new Date(now.valueOf() - 1000))).equals(true) attest(t.allows(now)).equals(true) attest(t.allows(new Date(now.valueOf() + 1000))).equals(false) @@ -203,7 +209,7 @@ contextualize( }) it("number", () => { - attest(type("number==-3.14159").infer) + attest<-3.14159>(type("number==-3.14159").infer) }) it("string", () => { diff --git a/ark/type/__tests__/declared.test.ts b/ark/type/__tests__/declared.test.ts index 1b13158ca2..ec30bcf8b9 100644 --- a/ark/type/__tests__/declared.test.ts +++ b/ark/type/__tests__/declared.test.ts @@ -43,7 +43,7 @@ contextualize(() => { attest( // @ts-expect-error declare<[string, number]>().type(["string", "number", "number"]) - ).type.errors(`Source has 3 element(s) but target requires 2`) + ).type.errors(`Source has 3 element(s) but target allows only 2`) }) it("tuple expression", () => { @@ -95,7 +95,7 @@ contextualize(() => { a: "string" }) ).type.errors( - `Property 'b' is missing in type '{ a: "string"; }' but required in type '{ a: "string"; "b?": unknown; }'.` + `Property 'b' is missing in type '{ a: "string"; }' but required in type '{ a: "string"; b: number; }'.` ) }) @@ -106,7 +106,7 @@ contextualize(() => { a: "string" }) ).type.errors( - `Property '"b?"' is missing in type '{ a: "string"; }' but required in type '{ a: "string"; "b?": unknown; }'.` + `Property '"b?"' is missing in type '{ a: "string"; }' but required in type '{ a: "string"; "b?": number | undefined; }'.` ) }) diff --git a/ark/type/__tests__/defaults.test.ts b/ark/type/__tests__/defaults.test.ts new file mode 100644 index 0000000000..16c5ef034d --- /dev/null +++ b/ark/type/__tests__/defaults.test.ts @@ -0,0 +1,94 @@ +import { attest, contextualize } from "@arktype/attest" +import { type } from "arktype" +import { invalidDefaultKeyKindMessage } from "../parser/objectLiteral.js" + +contextualize( + "parsing and traversal", + () => { + it("base", () => { + const o = type({ foo: "string", bar: ["number", "=", 5] }) + + // ensure type ast displays is exactly as expected + attest(o.t).type.toString.snap( + "{ foo: string; bar: (In?: number | undefined) => Default<5>; }" + ) + attest<{ foo: string; bar?: number }>(o.inferIn) + attest<{ foo: string; bar: number }>(o.infer) + + attest(o.json).snap({ + required: [{ key: "foo", value: "string" }], + optional: [{ default: 5, key: "bar", value: "number" }], + domain: "object" + }) + + attest(o({ foo: "", bar: 4 })).equals({ foo: "", bar: 4 }) + attest(o({ foo: "" })).equals({ foo: "", bar: 5 }) + attest(o({ bar: 4 }).toString()).snap( + "foo must be a string (was missing)" + ) + attest(o({ foo: "", bar: "" }).toString()).snap( + "bar must be a number (was string)" + ) + }) + + it("defined with wrong type", () => { + attest(() => + // @ts-expect-error + type({ foo: "string", bar: ["number", "=", "5"] }) + ) + .throws.snap( + 'ParseError: Default value at "bar" must be a number (was string)' + ) + .type.errors("Type 'string' is not assignable to type 'number'") + }) + + it("optional with default", () => { + attest(() => + // @ts-expect-error + type({ foo: "string", "bar?": ["number", "=", 5] }) + ).throwsAndHasTypeError(invalidDefaultKeyKindMessage) + }) + }, + "intersection", + () => { + it("two optionals, one default", () => { + const l = type({ bar: ["number", "=", 5] }) + const r = type({ "bar?": "5" }) + + const result = l.and(r) + attest(result.json).snap({ + optional: [{ default: 5, key: "bar", value: { unit: 5 } }], + domain: "object" + }) + }) + it("same default", () => { + const l = type({ bar: ["number", "=", 5] }) + const r = type({ bar: ["5", "=", 5] }) + + const result = l.and(r) + attest(result.json).snap({ + optional: [{ default: 5, key: "bar", value: { unit: 5 } }], + domain: "object" + }) + }) + + it("removed when intersected with required", () => { + const l = type({ bar: ["number", "=", 5] }) + const r = type({ bar: "number" }) + + const result = l.and(r) + attest(result.json).snap({ + required: [{ key: "bar", value: "number" }], + domain: "object" + }) + }) + + it("errors on multiple defaults", () => { + const l = type({ bar: ["number", "=", 5] }) + const r = type({ bar: ["number", "=", 6] }) + attest(() => l.and(r)).throws.snap( + "ParseError: Invalid intersection of default values 5 & 6" + ) + }) + } +) diff --git a/ark/type/__tests__/expressions.test.ts b/ark/type/__tests__/expressions.test.ts index b80fe36c34..1bcf65ecf4 100644 --- a/ark/type/__tests__/expressions.test.ts +++ b/ark/type/__tests__/expressions.test.ts @@ -1,5 +1,5 @@ import { attest, contextualize } from "@arktype/attest" -import { rawSchema, writeUnresolvableMessage } from "@arktype/schema" +import { rawRoot, writeUnresolvableMessage } from "@arktype/schema" import { type } from "arktype" import { writeMissingRightOperandMessage } from "../parser/string/shift/operand/unenclosed.js" @@ -39,8 +39,8 @@ contextualize( "any", "never", "unknown", - "parse", "keyof", + "parse", "void", "url", "alpha", @@ -86,8 +86,9 @@ contextualize( "any", "never", "unknown", - "parse", + "&", "keyof", + "parse", "void", "[]", "url", @@ -101,7 +102,6 @@ contextualize( "semver", "Record", "|", - "&", ":", "=>", "@", @@ -153,14 +153,14 @@ contextualize( it("instanceof single", () => { const t = type("instanceof", RegExp) attest(t.infer) - const expected = rawSchema(RegExp) + const expected = rawRoot(RegExp) attest(t.json).equals(expected.json) }) it("instanceof branches", () => { const t = type("instanceof", Array, Date) attest(t.infer) - const expected = rawSchema([Array, Date]) + const expected = rawRoot([Array, Date]) attest(t.json).equals(expected.json) }) diff --git a/ark/type/__tests__/imports.test.ts b/ark/type/__tests__/imports.test.ts index 1f528b2f1d..950ef23851 100644 --- a/ark/type/__tests__/imports.test.ts +++ b/ark/type/__tests__/imports.test.ts @@ -1,28 +1,26 @@ import { attest, contextualize } from "@arktype/attest" import { writeDuplicateAliasError } from "@arktype/schema" -import { lazily } from "@arktype/util" import { type Module, scope, type } from "arktype" import { writePrefixedPrivateReferenceMessage } from "../parser/semantic/validate.js" -const threeSixtyNoScope = lazily(() => - scope({ - three: "3", - sixty: "60", - no: "'no'" - }) -) -const yesScope = lazily(() => scope({ yes: "'yes'" })) +const threeSixtyNoScope = scope({ + three: "3", + sixty: "60", + no: "'no'" +}) -const threeSixtyNoModule = lazily(() => threeSixtyNoScope.export()) -const yesModule = lazily(() => yesScope.export()) +const yesScope = scope({ yes: "'yes'" }) + +const threeSixtyNoModule = threeSixtyNoScope.export() +const yesModule = yesScope.export() contextualize(() => { it("single", () => { - const $ = scope({ + const types = scope({ ...threeSixtyNoModule, threeSixtyNo: "three|sixty|no" - }) - attest<{ threeSixtyNo: 3 | 60 | "no" }>($.infer) + }).export() + attest>(types) }) it("multiple", () => { @@ -37,7 +35,9 @@ contextualize(() => { a: "three|sixty|no|yes|extra" }) - attest<{ a: 3 | 60 | "no" | "yes" | true }>(imported.infer) + const exports = imported.export() + + attest>(exports) }) it("import & export", () => { diff --git a/ark/type/__tests__/instanceof.test.ts b/ark/type/__tests__/instanceof.test.ts index e84016904c..73a99dc397 100644 --- a/ark/type/__tests__/instanceof.test.ts +++ b/ark/type/__tests__/instanceof.test.ts @@ -1,6 +1,6 @@ import { attest, contextualize } from "@arktype/attest" -import { rawSchema } from "@arktype/schema" -import { type Type, type } from "arktype" +import { rawRoot } from "@arktype/schema" +import { type, type Type } from "arktype" import { writeInvalidConstructorMessage } from "../parser/tuple.js" contextualize( @@ -9,11 +9,13 @@ contextualize( it("base", () => { const t = type(["instanceof", Error]) attest(t.infer) - const expected = rawSchema(Error) + const expected = rawRoot(Error) attest(t.json).equals(expected.json) const e = new Error() attest(t(e)).equals(e) + attest(t(e)).equals(e) attest(t({}).toString()).snap("must be an Error (was object)") + attest(t(undefined).toString()).snap("must be an Error (was undefined)") }) it("inherited", () => { const t = type(["instanceof", TypeError]) @@ -53,7 +55,7 @@ contextualize( private isArk = true } const ark = type(["instanceof", ArkClass]) - attest>(ark) + attest>(ark) // not expanded since there are no morphs attest(ark.infer).type.toString("ArkClass") attest(ark.in.infer).type.toString("ArkClass") @@ -76,7 +78,7 @@ contextualize( } const ark = type(["instanceof", ArkClass]) - attest>(ark) + attest>(ark) // not expanded since there are no morphs attest(ark.infer).type.toString("ArkClass") attest(ark.in.infer).type.toString("ArkClass") diff --git a/ark/type/__tests__/keyTraversal.test.ts b/ark/type/__tests__/keyTraversal.test.ts deleted file mode 100644 index 40498ef417..0000000000 --- a/ark/type/__tests__/keyTraversal.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -// TODO: reenable -// describe("key traversal", () => { -// const getExtraneousB = () => ({ a: "ok", b: "why?" }) -// it("loose by default", () => { -// const t = type({ -// a: "string" -// }) -// const dataWithExtraneousB = getExtraneousB() -// attest(t(dataWithExtraneousB)).equals(dataWithExtraneousB) -// }) -// -// it("invalid union", () => { -// const o = type([{ a: "string" }, "|", { b: "boolean" }]).configure({ -// keys: "strict" -// }) -// attest(o({ a: 2 }).toString()).snap( -// 'a must be a string or removed (was {"a":2})' -// ) -// }) -// -// it("distilled type", () => { -// const t = type({ -// a: "string" -// }).configure({ keys: "distilled" }) -// attest(t({ a: "ok" })).equals({ a: "ok" }) -// attest(t(getExtraneousB())).snap({ a: "ok" }) -// }) -// -// it("distilled array", () => { -// const o = type({ a: "email[]" }).configure({ -// keys: "distilled" -// }) -// attest(o({ a: ["shawn@arktype.io"] })).snap({ -// a: ["shawn@arktype.io"] -// }) -// attest(o({ a: ["notAnEmail"] }).toString()).snap( -// "a/0 must be a valid email (was 'notAnEmail')" -// ) -// // can handle missing keys -// attest(o({ b: ["shawn"] }).toString()).snap("a must be defined") -// }) -// -// it("distilled union", () => { -// const o = type([{ a: "string" }, "|", { b: "boolean" }]).configure({ -// keys: "distilled" -// }) -// // can distill to first branch -// attest(o({ a: "to", z: "bra" })).snap({ a: "to" }) -// // can distill to second branch -// attest(o({ b: true, c: false })).snap({ b: true }) -// // can handle missing keys -// attest(o({ a: 2 }).toString()).snap( -// 'a must be a string or b must be defined (was {"a":2})' -// ) -// }) -// -// it("strict type", () => { -// const t = type({ -// a: "string" -// }).configure({ keys: "strict" }) -// attest(t({ a: "ok" })).equals({ a: "ok" }) -// attest(t(getExtraneousB()).toString()).snap("b must be removed") -// }) -// -// it("strict array", () => { -// const o = type({ a: "string[]" }).configure({ -// keys: "strict" -// }) -// attest(o({ a: ["shawn"] })).snap({ a: ["shawn"] }) -// attest(o({ a: [2] }).toString()).snap( -// "a/0 must be a string (was number)" -// ) -// attest(o({ b: ["shawn"] }).toString()).snap( -// "b must be removed\na must be defined" -// ) -// }) -// }) diff --git a/ark/type/__tests__/keyof.test.ts b/ark/type/__tests__/keyof.test.ts index 57f714d867..d88db0dfb4 100644 --- a/ark/type/__tests__/keyof.test.ts +++ b/ark/type/__tests__/keyof.test.ts @@ -1,5 +1,5 @@ import { attest, contextualize } from "@arktype/attest" -import { rawSchema, writeUnresolvableMessage } from "@arktype/schema" +import { rawRoot, writeUnresolvableMessage } from "@arktype/schema" import { type } from "arktype" import { writeMissingRightOperandMessage } from "../parser/string/shift/operand/unenclosed.js" @@ -12,7 +12,7 @@ contextualize(() => { it("root expression", () => { const t = type("keyof", "Date") attest(t.infer) - const expected = rawSchema(Date).keyof() + const expected = rawRoot(Date).keyof() attest(t.json).equals(expected.json) }) diff --git a/ark/type/__tests__/keywords.test.ts b/ark/type/__tests__/keywords.test.ts index 8512a549ae..ed4a0ecc4b 100644 --- a/ark/type/__tests__/keywords.test.ts +++ b/ark/type/__tests__/keywords.test.ts @@ -1,5 +1,5 @@ import { attest, contextualize } from "@arktype/attest" -import { rawSchema } from "@arktype/schema" +import { rawRoot } from "@arktype/schema" import { ark, type } from "arktype" contextualize( @@ -48,7 +48,7 @@ contextualize( it("boolean", () => { const boolean = type("boolean") attest(boolean.infer) - const expected = rawSchema([{ unit: false }, { unit: true }]) + const expected = rawRoot([{ unit: false }, { unit: true }]) // should be simplified to simple checks for true and false literals attest(boolean.json).equals(expected.json) // TODO: @@ -60,12 +60,11 @@ contextualize( it("never", () => { const never = type("never") attest(never.infer) - const expected = rawSchema([]) + const expected = rawRoot([]) // should be equivalent to a zero-branch union attest(never.json).equals(expected.json) }) - // TODO: ?? it("never in union", () => { const t = type("string|never") attest(t.infer) @@ -73,7 +72,7 @@ contextualize( }) it("unknown", () => { - const expected = rawSchema({}) + const expected = rawRoot({}) // should be equivalent to an unconstrained predicate attest(type("unknown").json).equals(expected.json) }) diff --git a/ark/type/__tests__/literal.test.ts b/ark/type/__tests__/literal.test.ts index 58c1e66a23..a4900d18e8 100644 --- a/ark/type/__tests__/literal.test.ts +++ b/ark/type/__tests__/literal.test.ts @@ -32,7 +32,7 @@ contextualize( // get the name ahead of time const anonName = printable(anon) const t = type(["===", anon]) - attest(t.infer) + attest(t.infer) attest(t(anon)).equals(anon) attest(t("test").toString()).equals(`must be ${anonName} (was "test")`) }) diff --git a/ark/type/__tests__/objectLiteral.test.ts b/ark/type/__tests__/objectLiteral.test.ts index 1ed1df99cf..bf0f077931 100644 --- a/ark/type/__tests__/objectLiteral.test.ts +++ b/ark/type/__tests__/objectLiteral.test.ts @@ -6,7 +6,10 @@ import { } from "@arktype/schema" import { printable, registeredReference } from "@arktype/util" import { scope, type } from "arktype" -import { writeInvalidSpreadTypeMessage } from "../parser/objectLiteral.js" +import { + writeInvalidSpreadTypeMessage, + writeInvalidUndeclaredBehaviorMessage +} from "../parser/objectLiteral.js" contextualize( "named", @@ -21,7 +24,7 @@ contextualize( attest<{ a: string; b: number }>(o.infer) attest(o.json).snap({ domain: "object", - prop: [ + required: [ { key: "a", value: "string" }, { key: "b", value: "number" } ] @@ -33,10 +36,8 @@ contextualize( attest<{ a?: string; b: number }>(o.infer) attest(o.json).snap({ domain: "object", - prop: [ - { key: "a", optional: true, value: "string" }, - { key: "b", value: "number" } - ] + required: [{ key: "b", value: "number" }], + optional: [{ key: "a", value: "string" }] }) }) @@ -49,7 +50,7 @@ contextualize( attest<{ [s]: string }>(t.infer) attest(t.json).snap({ domain: "object", - prop: [{ key: name, value: "string" }] + required: [{ key: name, value: "string" }] }) }) @@ -87,6 +88,10 @@ contextualize( it("escaped optional token", () => { const t = type({ "a\\?": "string" }) attest<{ "a?": string }>(t.infer) + attest(t.json).snap({ + required: [{ key: "a?", value: "string" }], + domain: "object" + }) }) it("traverse optional", () => { @@ -96,23 +101,6 @@ contextualize( attest(o({ a: 1 }).toString()).snap("a must be a string (was number)") }) - // it("traverse strict optional", () => { - // // TODO: strict - // const o = type({ "a?": "string" }) - // attest(o({ a: "a" })).snap({ a: "a" }) - // attest(o({})).snap({}) - // attest(o({ a: 1 }).toString()).snap("a must be a string (was number)") - // }) - - // it("multiple bad strict", () => { - // const t = type({ a: "string", b: "boolean" }).configure({ - // keys: "strict" - // }) - // attest(t({ a: 1, b: 2 }).toString()).snap( - // "a must be a string (was number)\nb must be boolean (was number)" - // ) - // }) - // it("optional symbol", () => { // const s = Symbol() // const name = reference(s) @@ -137,7 +125,7 @@ contextualize( attest<{ isAdmin: true; name: string }>(s.admin.infer) attest(s.admin.json).equals({ domain: "object", - prop: [ + required: [ { key: "isAdmin", value: { unit: true } }, { key: "name", value: "string" } ] @@ -151,7 +139,7 @@ contextualize( attest<{ isAdmin: true; name: string }>(admin.infer) attest(admin.json).snap({ domain: "object", - prop: [ + required: [ { key: "isAdmin", value: { unit: true } }, { key: "name", value: "string" } ] @@ -175,7 +163,7 @@ contextualize( attest(t.json).snap({ domain: "object", - prop: [ + required: [ { key: "inherited", value: [{ unit: false }, { unit: true }] @@ -194,14 +182,14 @@ contextualize( attest(t.json).snap({ domain: "object", - prop: [{ key: "...", value: "string" }] + required: [{ key: "...", value: "string" }] }) }) it("with non-object", () => { // @ts-expect-error attest(() => type({ "...": "string" })).throwsAndHasTypeError( - writeInvalidSpreadTypeMessage(printable("string")) + writeInvalidSpreadTypeMessage("string") ) }) @@ -215,7 +203,7 @@ contextualize( attest<{ isAdmin: true; name: string }>(adminUser.infer) attest(adminUser.json).snap({ domain: "object", - prop: [ + required: [ { key: "isAdmin", value: { unit: true } }, { key: "name", value: "string" } ] @@ -229,7 +217,7 @@ contextualize( attest<{ [x: string]: string }>(o.infer) attest(o.json).snap({ domain: "object", - index: [{ key: "string", value: "string" }] + index: [{ signature: "string", value: "string" }] }) attest(o({})).equals({}) @@ -249,7 +237,7 @@ b must be a string (was false)`) attest<{ [x: symbol]: 1 }>(o.infer) attest(o.json).snap({ domain: "object", - index: [{ key: "symbol", value: { unit: 1 } }] + index: [{ signature: "symbol", value: { unit: 1 } }] }) attest(o({})).equals({}) @@ -291,7 +279,7 @@ value at [${zildjianName}] must be 1 (was undefined)`) attest<{ [x: string]: string; [x: symbol]: string }>(o.infer) attest(o.json).snap({ domain: "object", - index: [{ key: ["string", "symbol"], value: "string" }] + index: [{ signature: ["string", "symbol"], value: "string" }] }) }) @@ -302,11 +290,11 @@ value at [${zildjianName}] must be 1 (was undefined)`) }) attest<{ [x: string]: string; [x: symbol]: number }>(o.infer) attest(o.json).snap({ - domain: "object", index: [ - { key: "string", value: "string" }, - { key: "symbol", value: "number" } - ] + { value: "string", signature: "string" }, + { value: "number", signature: "symbol" } + ], + domain: "object" }) attest(o({})).equals({}) @@ -343,11 +331,9 @@ value at [${symName}] must be a number (was string)`) ) attest(o.json).snap({ domain: "object", - prop: [ - { key: "optional", optional: true, value: { unit: "bar" } }, - { key: "required", value: { unit: "foo" } } - ], - index: [{ key: "string", value: "string" }] + required: [{ key: "required", value: { unit: "foo" } }], + optional: [{ key: "optional", value: { unit: "bar" } }], + index: [{ signature: "string", value: "string" }] }) const valid: typeof o.infer = { required: "foo", other: "bar" } @@ -357,8 +343,8 @@ value at [${symName}] must be a number (was string)`) optional: "wrongString", other: 0n }).toString() - ).snap(`optional must be "bar" (was "wrongString") -required must be "foo" (was missing) + ).snap(`required must be "foo" (was missing) +optional must be "bar" (was "wrongString") other must be a string (was bigint)`) }) @@ -382,6 +368,28 @@ other must be a string (was bigint)`) attest(types.obj.json).snap(expected.json) }) + it("intersection with named", () => { + const t = type({ "[string]": "4" }).and({ "a?": "1" }) + attest<{ + [k: string]: 4 + a?: never + }>(t.infer) + attest(t.json).snap({ + optional: [{ key: "a", value: { unit: 1 } }], + index: [{ value: { unit: 4 }, signature: "string" }], + domain: "object" + }) + }) + + it("intersction with right required", () => { + const t = type({ "a?": "true" }).and({ a: "boolean" }) + attest<{ a: true }>(t.infer) + const expected = type({ + a: "true" + }) + attest(t.json).equals(expected.json) + }) + it("syntax error in index definition", () => { attest(() => type({ @@ -435,7 +443,31 @@ other must be a string (was bigint)`) attest<{ "[string]": string }>(o.infer) attest(o.json).snap({ domain: "object", - prop: [{ key: "[string]", value: "string" }] + required: [{ key: "[string]", value: "string" }] + }) + }) + }, + "undeclared", + () => { + it("can parse an undeclared restriction", () => { + const t = type({ "+": "reject" }) + attest<{}>(t.infer) + attest(t.json).snap({ undeclared: "reject", domain: "object" }) + }) + it("fails on type definition for undeclared", () => { + // @ts-expect-error + attest(() => type({ "+": "string" })) + .throws(writeInvalidUndeclaredBehaviorMessage("string")) + .type.errors.snap( + "Type '\"string\"' is not assignable to type 'UndeclaredKeyBehavior'." + ) + }) + it("can escape undeclared meta key", () => { + const t = type({ "\\+": "string" }) + attest<{ "+": string }>(t.infer) + attest(t.json).snap({ + required: [{ key: "+", value: "string" }], + domain: "object" }) }) } diff --git a/ark/type/__tests__/pipe.test.ts b/ark/type/__tests__/pipe.test.ts index ae367874b9..515b644fba 100644 --- a/ark/type/__tests__/pipe.test.ts +++ b/ark/type/__tests__/pipe.test.ts @@ -165,7 +165,7 @@ contextualize(() => { attest Out>>(types.aAndB) attest(types.aAndB.json).snap({ - from: { unit: 3.14 }, + in: { unit: 3.14 }, morphs: types.aAndB.raw.serializedMorphs }) attest(types.bAndA) @@ -183,9 +183,9 @@ contextualize(() => { // attest string>>(types.c) assertNodeKind(types.c.raw, "morph") attest(types.c.json).snap({ - from: { + in: { domain: "object", - prop: [ + required: [ { key: "a", value: { unit: 1 } }, { key: "b", value: { unit: 2 } } ] @@ -205,7 +205,7 @@ contextualize(() => { const serializedMorphs = types.aOrB.raw.firstReferenceOfKindOrThrow("morph").serializedMorphs attest(types.aOrB.json).snap([ - { from: "number", morphs: serializedMorphs }, + { in: "number", morphs: serializedMorphs }, { unit: false }, { unit: true } ]) @@ -239,14 +239,14 @@ contextualize(() => { types.a.raw.firstReferenceOfKindOrThrow("morph").serializedMorphs attest(types.c.json).snap([ - { domain: "object", prop: [{ key: "a", value: "Function" }] }, + { domain: "object", required: [{ key: "a", value: "Function" }] }, { domain: "object", - prop: [ + required: [ { key: "a", value: { - from: { + in: { domain: "number", min: { exclusive: true, rule: 0 } }, @@ -267,7 +267,7 @@ contextualize(() => { attest Out>>(types.b) assertNodeKind(types.b.raw, "morph") attest(types.b.json).snap({ - from: "string", + in: "string", morphs: types.b.raw.serializedMorphs }) }) @@ -283,13 +283,13 @@ contextualize(() => { assertNodeKind(types.b.raw, "morph") assertNodeKind(types.a.raw, "morph") attest(types.b.json).snap({ - from: { + in: { domain: "object", - prop: [ + required: [ { key: "a", value: { - from: "string", + in: "string", morphs: types.a.raw.serializedMorphs } } @@ -300,25 +300,25 @@ contextualize(() => { }) it("directly nested", () => { - const t = type([ + const t = type( { - a: ["string", "=>", s => s.length] + // doesn't work with a nested tuple expression here due to a TS limitation + a: type("string", "=>", s => s.length) }, "=>", ({ a }) => a === 0 - ]) - // TODO: check - // attest Out>>(t) + ) + attest Out>>(t) assertNodeKind(t.raw, "morph") const nestedMorph = t.raw.firstReferenceOfKindOrThrow("morph") attest(t.json).snap({ - from: { + in: { domain: "object", - prop: [ + required: [ { key: "a", value: { - from: "string", + in: "string", morphs: nestedMorph.serializedMorphs } } diff --git a/ark/type/__tests__/realWorld.test.ts b/ark/type/__tests__/realWorld.test.ts index 02d079e4c4..77019a6c7a 100644 --- a/ark/type/__tests__/realWorld.test.ts +++ b/ark/type/__tests__/realWorld.test.ts @@ -1,63 +1,249 @@ -// import { attest, contextualize } from "@arktype/attest" -// import { scope, type type } from "arktype" - -// class TimeStub { -// declare readonly isoString: string -// /** -// * @remarks constructor is private to enforce using factory functions -// */ -// private constructor() {} -// /** -// * Creates a new {@link TimeStub} from an ISO date string -// * @param isoString - An ISO date string. -// * @returns A new {@link TimeStub} -// * @throws TypeError if a string is not provided, or RangeError if item -// * is not a valid date -// */ -// declare static from: (isoString: string) => TimeStub -// /** -// * Creates a new {@link TimeStub} from a Javascript `Date` -// * @param date - A Javascript `Date` -// * @returns A new {@link TimeStub} -// */ -// declare static fromDate: (date: Date) => TimeStub -// /** -// * Get a copy of the `TimeStub` converted to a Javascript `Date`. Does not -// * mutate the existing `TimeStub` value. -// * @returns A `Date` -// */ -// declare toDate: () => Date -// /** -// * Override default string conversion -// * @returns the string representation of a `TimeStub` -// */ -// declare toString: () => string -// } - -// TODO: cyclic -// contextualize(() => { -// it("time stub w/ private constructor", () => { -// const types = scope({ -// timeStub: ["instanceof", TimeStub] as type.cast, -// account: "clientDocument&accountData", -// clientDocument: { -// "id?": "string", -// "coll?": "string", -// "ts?": "timeStub", -// "ttl?": "timeStub" -// }, -// accountData: { -// user: "user|timeStub", -// provider: "provider", -// providerUserId: "string" -// }, -// user: { -// name: "string", -// "accounts?": "account[]" -// }, -// provider: "'GitHub'|'Google'" -// }).export() - -// attest(types.account.infer).type.toString.snap() -// }) -// }) +import { attest, contextualize } from "@arktype/attest" +import { scope, type, type Type } from "arktype" + +contextualize(() => { + // https://github.com/arktypeio/arktype/issues/915 + it("time stub w/ private constructor", () => { + class TimeStub { + declare readonly isoString: string + + private constructor() {} + + declare static from: (isoString: string) => TimeStub + + declare static fromDate: (date: Date) => TimeStub + + declare toDate: () => Date + + declare toString: () => string + } + + const types = scope({ + timeStub: ["instanceof", TimeStub] as type.cast, + account: "clientDocument&accountData", + clientDocument: { + "id?": "string", + "coll?": "string", + "ts?": "timeStub", + "ttl?": "timeStub" + }, + accountData: { + user: "user|timeStub", + provider: "provider", + providerUserId: "string" + }, + user: { + name: "string", + "accounts?": "account[]" + }, + provider: "'GitHub'|'Google'" + }).export() + + attest(types.account.infer).type.toString.snap( + '{ id?: string; coll?: string; ts?: TimeStub; ttl?: TimeStub; user: TimeStub | { name: string; accounts?: ...[]; }; provider: "GitHub" | "Google"; providerUserId: string; }' + ) + attest(types.account.json).snap({ + required: [ + { key: "provider", value: [{ unit: "GitHub" }, { unit: "Google" }] }, + { key: "providerUserId", value: "string" }, + { + key: "user", + value: [ + { + required: [{ key: "name", value: "string" }], + optional: [ + { + key: "accounts", + value: { sequence: "$account", proto: "Array" } + } + ], + domain: "object" + }, + "$ark.TimeStub" + ] + } + ], + optional: [ + { key: "coll", value: "string" }, + { key: "id", value: "string" }, + { key: "ts", value: "$ark.TimeStub" }, + { key: "ttl", value: "$ark.TimeStub" } + ], + domain: "object" + }) + }) + it("nested bound traversal", () => { + // https://github.com/arktypeio/arktype/issues/898 + const user = type({ + name: "string", + email: "email", + tags: "(string>=2)[]>=3", + score: "integer>=0" + }) + + const out = user({ + name: "Ok", + email: "", + tags: ["AB", "B"], + score: 0 + }) + + attest(out.toString()).snap(`email must be a valid email (was "") +tags must be at least length 3 (was 2)`) + }) + + it("multiple refinement errors", () => { + const nospacePattern = /^\S*$/ + + const schema = type({ + name: "string", + email: "email", + tags: "(string>=2)[]>=3", + score: "integer>=0", + "date?": "Date", + "nospace?": nospacePattern, + extra: "string|null" + }) + + const data = { + name: "Ok", + email: "", + tags: ["AB", "B"], + score: -1, + date: undefined, + nospace: "One space" + } + + const out = schema(data) + + attest(out.toString()).snap(`email must be a valid email (was "") +extra must be a string or null (was missing) +score must be at least 0 (was -1) +tags must be at least length 3 (was 2) +date must be a Date (was undefined) +nospace must be matched by ^\\S*$ (was "One space")`) + }) + + it("discrimination false negative", () => { + // https://github.com/arktypeio/arktype/issues/910 + const badScope = scope({ + a: { + x: "'x1'", + y: "'y1'", + z: "string" + }, + b: { + x: "'x1'", + y: "'y2'", + z: "number" + }, + c: { + x: "'x2'", + y: "'y3'", + z: "string" + }, + union: "a | b | c" + }).export() + + const badType = badScope.union + + type Test = typeof badType.infer + + const value: Test = { + x: "x2", + y: "y3", + z: "" + } // no type error + + const out = badType(value) // matches scope union item 'c'; should not fail + attest(out).equals(value) + }) + it("morph path", () => { + // https://github.com/arktypeio/arktype/issues/754 + const withMorph = type({ + key: type("string").pipe(type("3<=string<=4"), s => s.trim()) + }) + + const outWithMorph = withMorph({ + key: " This is too long " + }) + + attest(outWithMorph.toString()).snap( + "key must be at most length 4 (was 20)" + ) + + const withoutMorph = type({ + key: type("3<=string<=4") + }) + + const outWithoutMorph = withoutMorph({ + key: " This is too long " + }) + + attest(outWithoutMorph.toString()).snap( + "key must be at most length 4 (was 20)" + ) + }) + + it("cross scope reference", () => { + // https://github.com/arktypeio/arktype/issues/700 + const A = type({ + required: "boolean" + }) + + const B = scope({ A }).type({ + a: "A" + }) + + const C = scope({ + B + }).type({ + b: "B" + }) + + attest< + Type< + { + b: { + a: { + required: boolean + } + } + }, + { + B: { + a: { + required: boolean + } + } + } + > + >(C) + + attest(C.json).snap({ + domain: "object", + required: [ + { + key: "b", + value: { + domain: "object", + required: [ + { + key: "a", + value: { + domain: "object", + required: [ + { + key: "required", + value: [{ unit: false }, { unit: true }] + } + ] + } + } + ] + } + } + ] + }) + }) +}) diff --git a/ark/type/__tests__/scope.test.ts b/ark/type/__tests__/scope.test.ts index 4332ca45a9..5f7659ba0b 100644 --- a/ark/type/__tests__/scope.test.ts +++ b/ark/type/__tests__/scope.test.ts @@ -47,17 +47,17 @@ contextualize(() => { it("doesn't try to validate any in scope", () => { const $ = scope({ a: {} as any }) - attest<{ a: never }>($.infer) - attest<[number, never]>($.type(["number", "a"]).infer) + attest($.resolve("a").infer) + attest<[number, any]>($.type(["number", "a"]).infer) }) it("infers input and output", () => { const $ = scope({ a: ["string", "=>", s => s.length] }) - attest<{ a: number }>($.infer) + attest($.resolve("a").infer) - attest<{ a: string }>($.inferIn) + attest($.resolve("a").in.infer) }) it("infers its own helpers", () => { @@ -100,7 +100,7 @@ contextualize(() => { $.export() }) .throws(writeUnboundableMessage("boolean")) - .type.errors(writeUnboundableMessage("'b'")) + .type.errors(writeUnboundableMessage("b")) }) it("errors on ridiculous unexpected alias scenario", () => { @@ -188,12 +188,16 @@ contextualize(() => { ) // Type hint displays as "..." on hitting cycle (or any if "noErrorTruncation" is true) - attest({} as typeof types.a.infer).type.toString.snap() - attest({} as typeof types.b.infer.a.b.a.b.a.b.a).type.toString.snap() + attest({} as typeof types.a.infer).type.toString.snap( + "{ b: { a: ...; }; }" + ) + attest({} as typeof types.b.infer.a.b.a.b.a.b.a).type.toString.snap( + "{ b: { a: ...; }; }" + ) // @ts-expect-error attest({} as typeof types.a.infer.b.a.b.c).type.errors.snap( - `Property 'c' does not exist on type '{ a: { b: ...; }; }'.` + "Property 'c' does not exist on type '{ a: { b: ...; }; }'." ) }) @@ -210,7 +214,7 @@ contextualize(() => { } }) - type Package = ReturnType["infer"]["package"] + type Package = ReturnType["t"]["package"] const getCyclicData = () => { const packageData = { @@ -223,22 +227,22 @@ contextualize(() => { } it("cyclic union", () => { - const $ = scope({ + const types = scope({ a: { b: "b|false" }, b: { a: "a|true" } - }) - attest($.infer).type.toString.snap( - "{ a: { b: false | { a: true | any; }; }; b: { a: true | { b: false | any; }; }; }" + }).export() + attest(types).type.toString.snap( + "Module<{ a: { b: false | { a: true | ...; }; }; b: { a: true | { b: false | ...; }; }; }>" ) }) it("cyclic intersection", () => { - const $ = scope({ + const types = scope({ a: { b: "b&a" }, b: { a: "a&b" } - }) - attest($.infer).type.toString.snap( - "{ a: { b: { a: { b: any; a: any; }; b: any; }; }; b: { a: { b: { a: any; b: any; }; a: any; }; }; }" + }).export() + attest(types).type.toString.snap( + "Module<{ a: { b: { a: { b: ...; a: ...; }; b: ...; }; }; b: { a: { b: { a: ...; b: ...; }; a: ...; }; }; }>" ) }) @@ -256,9 +260,11 @@ contextualize(() => { const types = getCyclicScope().export() const data = getCyclicData() data.contributors[0].email = "ssalbdivad" - attest(types.package(data).toString()).snap( - 'contributors[0].email must be a valid email (was "ssalbdivad")' - ) + // ideally would only include one error, see: + // https://github.com/arktypeio/arktype/issues/924 + attest(types.package(data).toString()) + .snap(`contributors[0].email must be a valid email (was "ssalbdivad") +dependencies[1].contributors[0].email must be a valid email (was "ssalbdivad")`) }) it("can include cyclic data in message", () => { @@ -282,16 +288,16 @@ contextualize(() => { a: "a|3" } }).export() - attest(types.a.infer).type.toString.snap("{ b: { a: 3 | any; }") + attest(types.a.infer).type.toString.snap("{ b: { a: 3 | ...; }; }") attest(types.a.json).snap({ domain: "object", - prop: [ + required: [ { key: "b", value: { domain: "object", - prop: [{ key: "a", value: ["$a", { unit: 3 }] }] + required: [{ key: "a", value: ["$a", { unit: 3 }] }] } } ] @@ -310,36 +316,38 @@ contextualize(() => { 'b.a.b.a must be an object or 3 (was number, 4) or b.a must be 3 (was {"b":{"a":4}})' ) - attest(types.b.infer).type.toString.snap("{ a: 3 | { b: any; }; }") + attest(types.b.infer).type.toString.snap("{ a: 3 | { b: ...; }; }") attest(types.b.json).snap({ domain: "object", - prop: [{ key: "a", value: ["$a", { unit: 3 }] }] + required: [{ key: "a", value: ["$a", { unit: 3 }] }] }) }) it("intersect cyclic reference", () => { const types = scope({ - a: { - b: "b" + arf: { + b: "bork" }, - b: { - c: "a&b" + bork: { + c: "arf&bork" } }).export() - attest(types.a.infer).type.toString.snap() - attest(types.b.infer).type.toString.snap() + attest(types.arf.infer).type.toString.snap( + "{ b: { c: { b: ...; c: ...; }; }; }" + ) + attest(types.bork.infer).type.toString.snap("{ c: { b: ...; c: ...; }; }") const expectedCyclicJson = - types.a.raw.firstReferenceOfKindOrThrow("alias").json + types.arf.raw.firstReferenceOfKindOrThrow("alias").json - attest(types.a.json).snap({ + attest(types.arf.json).snap({ domain: "object", - prop: [ + required: [ { key: "b", value: { domain: "object", - prop: [ + required: [ { key: "c", value: expectedCyclicJson @@ -349,20 +357,20 @@ contextualize(() => { } ] }) - const a = {} as typeof types.a.infer - const b = { c: {} } as typeof types.b.infer + const a = {} as typeof types.arf.infer + const b = { c: {} } as typeof types.bork.infer a.b = b b.c.b = b b.c.c = b.c - attest(types.a(a)).equals(a) - attest(types.a({ b: { c: {} } }).toString()) - .snap(`b.c.b must be { c: a&b } (was missing) -b.c.c must be a&b (was missing)`) + attest(types.arf(a)).equals(a) + attest(types.arf({ b: { c: {} } }).toString()) + .snap(`b.c.b must be { c: arf&bork } (was missing) +b.c.c must be arf&bork (was missing)`) - attest(types.b.json).snap({ + attest(types.bork.json).snap({ domain: "object", - prop: [ + required: [ { key: "c", value: expectedCyclicJson @@ -370,5 +378,19 @@ b.c.c must be a&b (was missing)`) ] }) }) + // https://github.com/arktypeio/arktype/issues/930 + // it("intersect cyclic reference with repeat name", () => { + // const types = scope({ + // arf: { + // b: "bork" + // }, + // bork: { + // c: "arf&bork" + // } + // }).export() + // attest(types.arf({ b: { c: {} } }).toString()) + // .snap(`b.c.b must be { c: arf&bork } (was missing) + // b.c.c must be arf&bork (was missing)`) + // }) }) }) diff --git a/ark/type/__tests__/thunk.test.ts b/ark/type/__tests__/thunk.test.ts index ee8f32a012..01d7dc692f 100644 --- a/ark/type/__tests__/thunk.test.ts +++ b/ark/type/__tests__/thunk.test.ts @@ -27,7 +27,7 @@ contextualize(() => { b: { a: string } - }>($.infer) + }>($["t"]) const types = $.export() attest<{ diff --git a/ark/type/__tests__/traverse.test.ts b/ark/type/__tests__/traverse.test.ts index 68c894fff1..d61334a834 100644 --- a/ark/type/__tests__/traverse.test.ts +++ b/ark/type/__tests__/traverse.test.ts @@ -138,4 +138,40 @@ contextualize(() => { β€’ more than 0` ) }) + + it("relative path", () => { + const signup = type({ + email: "email", + password: "string", + repeatPassword: "string" + }).narrow( + (d, ctx) => + d.password === d.repeatPassword || + ctx.invalid({ + expected: "identical to password", + actual: null, + relativePath: ["repeatPassword"] + }) + ) + + // ensure the relativePath is relative + const nestedSignup = type({ + user: signup + }) + + const validSignup: typeof signup.infer = { + email: "david@arktype.io", + password: "secure", + repeatPassword: "secure" + } + + const valid: typeof nestedSignup.infer = { user: validSignup } + + attest(nestedSignup(valid)).equals(valid) + attest( + nestedSignup({ + user: { ...validSignup, repeatPassword: "insecure" } + }).toString() + ).snap("user.repeatPassword must be identical to password") + }) }) diff --git a/ark/type/__tests__/type.test.ts b/ark/type/__tests__/type.test.ts index c27c4106ae..45b08aa851 100644 --- a/ark/type/__tests__/type.test.ts +++ b/ark/type/__tests__/type.test.ts @@ -1,5 +1,5 @@ import { attest, contextualize } from "@arktype/attest" -import { ArkError, type } from "arktype" +import { type } from "arktype" import { AssertionError } from "node:assert" contextualize(() => { @@ -21,14 +21,13 @@ contextualize(() => { attest(t.allows(5)).equals(false) }) - // TODO: ? it("errors can be thrown", () => { const t = type("number") try { const result = t("invalid") attest(result instanceof type.errors && result.throw()) } catch (e) { - attest(e instanceof ArkError).equals(true) + attest(e instanceof type.errors).equals(true) return } throw new AssertionError({ message: "Expected to throw" }) diff --git a/ark/type/__tests__/undeclaredKeys.test.ts b/ark/type/__tests__/undeclaredKeys.test.ts new file mode 100644 index 0000000000..843ae1da6f --- /dev/null +++ b/ark/type/__tests__/undeclaredKeys.test.ts @@ -0,0 +1,65 @@ +import { attest, contextualize } from "@arktype/attest" +import { type } from "arktype" + +contextualize("traversal", () => { + const getExtraneousB = () => ({ a: "ok", b: "why?" }) + + it("loose by default", () => { + const t = type({ + a: "string" + }) + + attest(t).equals(t.onUndeclaredKey("ignore")) + + const dataWithExtraneousB = getExtraneousB() + attest(t(dataWithExtraneousB)).equals(dataWithExtraneousB) + }) + + it("delete keys", () => { + const t = type({ + a: "string" + }).onUndeclaredKey("delete") + attest(t({ a: "ok" })).equals({ a: "ok" }) + attest(t(getExtraneousB())).snap({ a: "ok" }) + }) + + it("delete union key", () => { + const o = type([{ a: "string" }, "|", { b: "boolean" }]).onUndeclaredKey( + "delete" + ) + // can distill to first branch + attest(o({ a: "to", z: "bra" })).snap({ a: "to" }) + // can distill to second branch + attest(o({ b: true, c: false })).snap({ b: true }) + // can handle missing keys + attest(o({ a: 2 }).toString()).snap( + "a must be a string (was number) or b must be boolean (was missing)" + ) + }) + + it("reject key", () => { + const t = type({ + a: "string" + }).onUndeclaredKey("reject") + attest(t({ a: "ok" })).equals({ a: "ok" }) + attest(t(getExtraneousB()).toString()).snap("b must be removed") + }) + + it("reject array key", () => { + const o = type({ "+": "reject", a: "string[]" }) + attest(o({ a: ["shawn"] })).snap({ a: ["shawn"] }) + attest(o({ a: [2] }).toString()).snap("a[0] must be a string (was number)") + attest(o({ b: ["shawn"] }).toString()) + .snap(`a must be string[] (was missing) +b must be removed`) + }) + + it("reject key from union", () => { + const o = type([{ a: "string" }, "|", { b: "boolean" }]).onUndeclaredKey( + "reject" + ) + attest(o({ a: 2, b: true }).toString()).snap( + "a must be a string or removed (was number)" + ) + }) +}) diff --git a/ark/type/__tests__/union.test.ts b/ark/type/__tests__/union.test.ts index 875c52ecfc..f760431169 100644 --- a/ark/type/__tests__/union.test.ts +++ b/ark/type/__tests__/union.test.ts @@ -1,7 +1,7 @@ import { attest, contextualize } from "@arktype/attest" import { keywordNodes, - rawSchema, + rawRoot, writeIndivisibleMessage, writeUnresolvableMessage } from "@arktype/schema" @@ -30,7 +30,6 @@ contextualize(() => { }) it("multiple subtypes pruned", () => { - // TODO: check base type union reduction const t = type("'foo'|'bar'|string|'baz'|/.*/") const expected = type("string") attest(t.infer) @@ -110,17 +109,17 @@ contextualize(() => { }) const expected = () => - rawSchema([ + rawRoot([ { domain: "object", - prop: { + required: { key: "a", value: { domain: "string" } } }, { domain: "object", - prop: { + required: { key: "b", value: { domain: "number" } } @@ -154,13 +153,13 @@ contextualize(() => { it("root autocompletions", () => { // @ts-expect-error - attest(() => type({ a: "s" }, "|", { b: "boolean" })).type.errors( - `Type '"s"' is not assignable to type '"string" | "symbol" | "semver"'` - ) + attest(() => type({ a: "s" }, "|", { b: "boolean" })).completions({ + s: ["string", "symbol", "semver"] + }) // @ts-expect-error - attest(() => type({ a: "string" }, "|", { b: "b" })).type.errors( - `Type '"b"' is not assignable to type '"bigint" | "boolean"'` - ) + attest(() => type({ a: "string" }, "|", { b: "b" })).completions({ + b: ["bigint", "boolean"] + }) }) it("bad reference", () => { diff --git a/ark/type/main.ts b/ark/type/api.ts similarity index 80% rename from ark/type/main.ts rename to ark/type/api.ts index 330bd04e13..ca01717f62 100644 --- a/ark/type/main.ts +++ b/ark/type/api.ts @@ -1,4 +1,4 @@ -export { ArkError, ArkErrors, ArkTypeError } from "@arktype/schema" +export { ArkError as ArkError, ArkErrors } from "@arktype/schema" export type { Ark, ArkConfig, Out } from "@arktype/schema" export { ambient, ark, declare, define, match, type } from "./ark.js" export { Module } from "./module.js" diff --git a/ark/type/ark.ts b/ark/type/ark.ts index 3e1638561c..14350c0893 100644 --- a/ark/type/ark.ts +++ b/ark/type/ark.ts @@ -1,13 +1,13 @@ import { + keywordNodes, type Ark, type ArkErrors, - type inferred, - keywordNodes + type inferred } from "@arktype/schema" import type { Generic } from "./generic.js" import type { MatchParser } from "./match.js" import type { Module } from "./module.js" -import { RawScope, type Scope, scope } from "./scope.js" +import { RawScope, scope, type Scope } from "./scope.js" import type { DeclarationParser, DefinitionParser, TypeParser } from "./type.js" type TsGenericsExports<$ = Ark> = { diff --git a/ark/type/config.ts b/ark/type/config.ts new file mode 100644 index 0000000000..b70997f9d6 --- /dev/null +++ b/ark/type/config.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +export * from "@arktype/schema/config" diff --git a/ark/type/generic.ts b/ark/type/generic.ts index 4451900ffe..4a47fad1e3 100644 --- a/ark/type/generic.ts +++ b/ark/type/generic.ts @@ -1,7 +1,7 @@ import { type GenericNodeInstantiation, type GenericProps, - type SchemaScope, + type RootScope, arkKind } from "@arktype/schema" import { Callable, type conform } from "@arktype/util" @@ -54,7 +54,7 @@ export class Generic public params: params, public def: def, // TODO: should be Scope<$>, but breaks inference - public $: SchemaScope<$> + public $: RootScope<$> ) { super((...args: unknown[]) => { // const argNodes = flatMorph(params, (i, param: string) => [ diff --git a/ark/type/match.ts b/ark/type/match.ts index b695104f39..f6f2f3df77 100644 --- a/ark/type/match.ts +++ b/ark/type/match.ts @@ -159,7 +159,7 @@ export type MatchInvocation = < export const createMatchParser = <$>($: Scope): MatchParser<$> => { return (() => {}).bind($) as never // const matchParser = (isRestricted: boolean) => { - // const handledCases: { when: RawSchema; then: Morph }[] = [] + // const handledCases: { when: RawRoot; then: Morph }[] = [] // let defaultCase: ((x: unknown) => unknown) | null = null // const parser = { @@ -174,17 +174,17 @@ export const createMatchParser = <$>($: Scope): MatchParser<$> => { // const branches = handledCases.flatMap(({ when, then }) => { // if (when.kind === "union") { // return when.branches.map((branch) => ({ - // from: branch, + // in: branch, // morph: then // })) // } // if (when.kind === "morph") { - // return [{ from: when, morph: [when.morph, then] }] + // return [{ in: when, morph: [when.morph, then] }] // } - // return [{ from: when, morph: then }] + // return [{ in: when, morph: then }] // }) // if (defaultCase) { - // branches.push({ from: keywordNodes.unknown, morph: defaultCase }) + // branches.push({ in: keywordNodes.unknown, morph: defaultCase }) // } // const matchers = $.node("union", { // branches, diff --git a/ark/type/module.ts b/ark/type/module.ts index bf66af7b81..d810c6a3f0 100644 --- a/ark/type/module.ts +++ b/ark/type/module.ts @@ -1,15 +1,16 @@ -import type { PreparsedNodeResolution, arkKind } from "@arktype/schema" -import { DynamicBase, type isAnyOrNever } from "@arktype/util" +import { RootModule, type PreparsedNodeResolution } from "@arktype/schema" +import type { anyOrNever } from "@arktype/util" import type { Type } from "./type.js" type exportScope<$> = { [k in keyof $]: $[k] extends PreparsedNodeResolution ? - isAnyOrNever<$[k]> extends true ? + [$[k]] extends [anyOrNever] ? Type<$[k], $> : $[k] : Type<$[k], $> } -export class Module<$ = any> extends DynamicBase> { - declare readonly [arkKind]: "module" -} +export const Module: new <$ = {}>(types: exportScope<$>) => Module<$> = + RootModule as never + +export type Module<$ = {}> = RootModule> diff --git a/ark/type/package.json b/ark/type/package.json index 56b00c78e8..42bf1a9e1a 100644 --- a/ark/type/package.json +++ b/ark/type/package.json @@ -1,7 +1,7 @@ { "name": "arktype", "description": "TypeScript's 1:1 validator, optimized from editor to runtime", - "version": "2.0.0-dev.10", + "version": "2.0.0-dev.12", "license": "MIT", "author": { "name": "David Blass", @@ -13,25 +13,18 @@ "url": "https://github.com/arktypeio/arktype.git" }, "type": "module", - "main": "./out/main.js", - "types": "./out/main.d.ts", + "main": "./out/api.js", + "types": "./out/api.d.ts", "exports": { - ".": { - "types": "./out/main.d.ts", - "default": "./out/main.js" - }, - "./config": { - "types": "./out/config.d.ts", - "default": "./out/config.js" - }, - "./internal/*": { - "default": "./out/*" - } + ".": "./out/api.js", + "./config": "./out/config.js", + "./internal/*": "./out/*" + }, + "imports": { + "#foo": "../schema/api.ts" }, "files": [ - "out", - "!__tests__", - "**/*.ts" + "out" ], "scripts": { "build": "tsx ../repo/build.ts", diff --git a/ark/type/parser/definition.ts b/ark/type/parser/definition.ts index d505028278..34948ee31a 100644 --- a/ark/type/parser/definition.ts +++ b/ark/type/parser/definition.ts @@ -1,43 +1,43 @@ -import { type RawSchema, hasArkKind, type string } from "@arktype/schema" +import { hasArkKind, type BaseRoot, type string } from "@arktype/schema" import { + isThunk, + objectKindOf, + printable, + throwParseError, type Dict, type ErrorMessage, type Primitive, + type anyOrNever, type array, type defined, type equals, - type isAny, - isThunk, type isUnknown, - objectKindOf, type objectKindOrDomainOf, type optionalKeyOf, - printable, type requiredKeyOf, - type show, - throwParseError + type show } from "@arktype/util" import type { type } from "../ark.js" import type { ParseContext } from "../scope.js" import { - type inferObjectLiteral, parseObjectLiteral, + type inferObjectLiteral, type validateObjectLiteral } from "./objectLiteral.js" import type { validateString } from "./semantic/validate.js" import type { BaseCompletions, inferString } from "./string/string.js" import { + parseTuple, type TupleExpression, type inferTuple, - parseTuple, type validateTuple } from "./tuple.js" -export const parseObject = (def: object, ctx: ParseContext): RawSchema => { +export const parseObject = (def: object, ctx: ParseContext): BaseRoot => { const objectKind = objectKindOf(def) switch (objectKind) { case undefined: - if (hasArkKind(def, "schema")) return def + if (hasArkKind(def, "root")) return def return parseObjectLiteral(def as Dict, ctx) case "Array": return parseTuple(def as array, ctx) @@ -52,7 +52,7 @@ export const parseObject = (def: object, ctx: ParseContext): RawSchema => { ) case "Function": { const resolvedDef = isThunk(def) ? def() : def - if (hasArkKind(resolvedDef, "schema")) return resolvedDef + if (hasArkKind(resolvedDef, "root")) return resolvedDef return throwParseError(writeBadDefinitionTypeMessage("Function")) } default: @@ -63,7 +63,7 @@ export const parseObject = (def: object, ctx: ParseContext): RawSchema => { } export type inferDefinition = - isAny extends true ? never + [def] extends [anyOrNever] ? def : def extends type.cast | ThunkCast ? t : def extends string ? inferString : def extends array ? inferTuple @@ -78,7 +78,7 @@ export type validateDefinition = : def extends string ? validateString : def extends array ? validateTuple : def extends BadDefinitionType ? - writeBadDefinitionTypeMessage> + ErrorMessage>> : isUnknown extends true ? // this allows the initial list of autocompletions to be populated when a user writes "type()", // before having specified a definition @@ -98,7 +98,7 @@ type validateInference = { [i in keyof declared]: i extends keyof def ? validateInference - : unknown + : declared[i] } : show> : def extends object ? @@ -106,13 +106,13 @@ type validateInference = { [k in requiredKeyOf]: k extends keyof def ? validateInference - : unknown + : declared[k] } & { [k in optionalKeyOf & string as `${k}?`]: `${k}?` extends ( keyof def ) ? validateInference, $, args> - : unknown + : declared[k] } > : validateShallowInference diff --git a/ark/type/parser/objectLiteral.ts b/ark/type/parser/objectLiteral.ts index b6367b558e..9ce5a41f15 100644 --- a/ark/type/parser/objectLiteral.ts +++ b/ark/type/parser/objectLiteral.ts @@ -1,19 +1,29 @@ import { - tsKeywords, - type NodeDef, - type RawSchema, - type UnitNode, + ArkErrors, + normalizeIndex, + type BaseRoot, + type Default, + type MutableInner, + type NodeSchema, + type PropKind, + type StructureNode, + type UndeclaredKeyBehavior, type writeInvalidPropertyKeyMessage } from "@arktype/schema" import { + append, + isArray, printable, - spliterate, stringAndSymbolicEntriesOf, throwParseError, + unset, type Dict, type ErrorMessage, type Key, + type anyOrNever, + type keyError, type merge, + type mutable, type show } from "@arktype/util" import type { ParseContext } from "../scope.js" @@ -22,95 +32,111 @@ import type { astToString } from "./semantic/utils.js" import type { validateString } from "./semantic/validate.js" import { Scanner } from "./string/shift/scanner.js" -export const parseObjectLiteral = (def: Dict, ctx: ParseContext): RawSchema => { - const propNodes: NodeDef<"prop">[] = [] - const indexNodes: NodeDef<"index">[] = [] +export const parseObjectLiteral = (def: Dict, ctx: ParseContext): BaseRoot => { + let spread: StructureNode | undefined + const structure: mutable, 2> = {} // We only allow a spread operator to be used as the first key in an object // because to match JS behavior any keys before the spread are overwritten // by the values in the target object, so there'd be no useful purpose in having it // anywhere except for the beginning. const parsedEntries = stringAndSymbolicEntriesOf(def).map(parseEntry) - if (parsedEntries[0]?.kind === "spread") { + if (parsedEntries[0]?.kind === "...") { // remove the spread entry so we can iterate over the remaining entries // expecting non-spread entries const spreadEntry = parsedEntries.shift()! const spreadNode = ctx.$.parse(spreadEntry.value, ctx) - if ( - !spreadNode.hasKind("intersection") || - !spreadNode.extends(tsKeywords.object) - ) { + if (!spreadNode.hasKind("intersection") || !spreadNode.structure) { return throwParseError( - writeInvalidSpreadTypeMessage(printable(spreadEntry.value)) + writeInvalidSpreadTypeMessage( + typeof spreadEntry.value === "string" ? + spreadEntry.value + : printable(spreadEntry.value) + ) ) } - // TODO: move to props group merge in schema - // For each key on spreadNode, add it to our object. - // We filter out keys from the spreadNode that will be defined later on this same object - // because the currently parsed definition will overwrite them. - spreadNode.prop?.forEach( - spreadRequired => - !parsedEntries.some( - ({ inner: innerKey }) => innerKey === spreadRequired.key - ) && propNodes.push(spreadRequired) - ) + spread = spreadNode.structure } for (const entry of parsedEntries) { - if (entry.kind === "spread") return throwParseError(nonLeadingSpreadError) - + if (entry.kind === "...") return throwParseError(nonLeadingSpreadError) + if (entry.kind === "+") { + if ( + entry.value !== "reject" && + entry.value !== "delete" && + entry.value !== "ignore" + ) + throwParseError(writeInvalidUndeclaredBehaviorMessage(entry.value)) + structure.undeclared = entry.value + continue + } if (entry.kind === "index") { // handle key parsing first to match type behavior - const key = ctx.$.parse(entry.inner, ctx) + const key = ctx.$.parse(entry.key, ctx) const value = ctx.$.parse(entry.value, ctx) - // extract enumerable named props from the index signature - // TODO: remove explicit annotation once we can use TS 5.5 - const [enumerable, nonEnumerable] = spliterate( - key.branches, - (k): k is UnitNode => k.hasKind("unit") - ) - - if (enumerable.length) { - propNodes.push( - ...enumerable.map(k => - ctx.$.node("prop", { key: k.unit as Key, value }) - ) + const normalizedSignature = normalizeIndex(key, value, ctx.$) + if (normalizedSignature.required) { + structure.required = append( + structure.required, + normalizedSignature.required ) - if (nonEnumerable.length) - indexNodes.push(ctx.$.node("index", { key: nonEnumerable, value })) - } else indexNodes.push(ctx.$.node("index", { key, value })) + } + if (normalizedSignature.index) + structure.index = append(structure.index, normalizedSignature.index) } else { const value = ctx.$.parse(entry.value, ctx) - propNodes.push({ - key: entry.inner, - value, - optional: entry.kind === "optional" - }) + const inner: MutableInner = { key: entry.key, value } + if (entry.default !== unset) { + const out = value(entry.default) + if (out instanceof ArkErrors) + throwParseError(`Default value at ${printable(entry.key)} ${out}`) + + value.assert(entry.default) + ;(inner as MutableInner<"optional">).default = entry.default + } + + structure[entry.kind] = append(structure[entry.kind], inner) } } + const structureNode = ctx.$.node("structure", structure) + return ctx.$.schema({ domain: "object", - prop: propNodes, - index: indexNodes + structure: spread?.merge(structureNode) ?? structureNode }) } +export const writeInvalidUndeclaredBehaviorMessage = ( + actual: unknown +): string => + `Value of '+' key must be 'reject', 'delete', or 'ignore' (was ${printable(actual)})` + export const nonLeadingSpreadError = "Spread operator may only be used as the first key in an object" +export type inferObjectLiteral = show< + "..." extends keyof def ? + merge< + inferDefinition, + _inferObjectLiteral + > + : _inferObjectLiteral +> + /** * Infers the contents of an object literal, ignoring a spread definition - * You probably want to use {@link inferObjectLiteral} instead. */ -type inferObjectLiteralInner = { +type _inferObjectLiteral = { // since def is a const parameter, we remove the readonly modifier here // support for builtin readonly tracked here: // https://github.com/arktypeio/arktype/issues/808 - -readonly [k in keyof def as nonOptionalKeyFrom]: inferDefinition< - def[k], - $, - args - > + -readonly [k in keyof def as nonOptionalKeyFrom]: def[k] extends ( + readonly [infer defaultableDef, "=", infer v] + ) ? + def[k] extends anyOrNever ? + def[k] + : (In?: inferDefinition) => Default + : inferDefinition } & { -readonly [k in keyof def as optionalKeyFrom]?: inferDefinition< def[k], @@ -119,37 +145,41 @@ type inferObjectLiteralInner = { > } -export type inferObjectLiteral = show< - "..." extends keyof def ? - merge< - inferDefinition, - inferObjectLiteralInner - > - : inferObjectLiteralInner -> - export type validateObjectLiteral = { [k in keyof def]: k extends IndexKey ? validateString extends ErrorMessage ? // add a nominal type here to avoid allowing the error message as input - indexParseError + keyError : inferDefinition extends PropertyKey ? // if the indexDef is syntactically and semantically valid, // move on to the validating the value definition validateDefinition - : indexParseError> + : keyError> : k extends "..." ? inferDefinition extends object ? validateDefinition - : indexParseError>> - : validateDefinition + : keyError>> + : k extends "+" ? UndeclaredKeyBehavior + : validatePossibleDefaultValue } +type validatePossibleDefaultValue = + def[k] extends readonly [infer defaultDef, "=", unknown] ? + parseKey["kind"] extends "required" ? + readonly [ + validateDefinition, + "=", + inferDefinition + ] + : ErrorMessage + : validateDefinition + type nonOptionalKeyFrom = parseKey extends PreparsedKey<"required", infer inner> ? inner : parseKey extends PreparsedKey<"index", infer inner> ? inferDefinition & Key - : // spread key is handled at the type root so is handled neither here nor in optionalKeyFrom + : // "..." is handled at the type root so is handled neither here nor in optionalKeyFrom + // "+" has no effect on inference never type optionalKeyFrom = @@ -160,60 +190,99 @@ type PreparsedKey< inner extends Key = Key > = { kind: kind - inner: inner + key: inner } namespace PreparsedKey { export type from = t } -type ParsedKeyKind = "required" | "optional" | "index" | "spread" +type ParsedKeyKind = "required" | "optional" | "index" | MetaKey + +export type MetaKey = "..." | "+" export type IndexKey = `[${def}]` -type PreparsedEntry = PreparsedKey & { value: unknown } +interface PreparsedEntry extends PreparsedKey { + value: unknown + default: unknown +} + +export const parseEntry = ([key, value]: readonly [ + Key, + unknown +]): PreparsedEntry => { + const parsedKey = parseKey(key) + + if (isArray(value) && value[1] === "=") { + if (parsedKey.kind !== "required") + throwParseError(invalidDefaultKeyKindMessage) -export const parseEntry = (entry: readonly [Key, unknown]): PreparsedEntry => - Object.assign(parseKey(entry[0]), { value: entry[1] }) + return { + kind: "optional", + key: parsedKey.key, + value: value[0], + default: value[2] + } + } + + return { + kind: parsedKey.kind, + key: parsedKey.key, + value, + default: unset + } +} + +// single quote use here is better for TypeScript's inlined error to avoid escapes +export const invalidDefaultKeyKindMessage = `Only required keys may specify default values, e.g. { ark: ['string', '=', 'β›΅'] }` + +export type invalidDefaultKeyKindMessage = typeof invalidDefaultKeyKindMessage const parseKey = (key: Key): PreparsedKey => - typeof key === "symbol" ? { inner: key, kind: "required" } + typeof key === "symbol" ? { kind: "required", key } : key.at(-1) === "?" ? key.at(-2) === Scanner.escapeToken ? - { inner: `${key.slice(0, -2)}?`, kind: "required" } + { kind: "required", key: `${key.slice(0, -2)}?` } : { - inner: key.slice(0, -1), - kind: "optional" + kind: "optional", + key: key.slice(0, -1) } : key[0] === "[" && key.at(-1) === "]" ? - { inner: key.slice(1, -1), kind: "index" } + { kind: "index", key: key.slice(1, -1) } : key[0] === Scanner.escapeToken && key[1] === "[" && key.at(-1) === "]" ? - { inner: key.slice(1), kind: "required" } - : key === "..." ? { inner: "...", kind: "spread" } - : { inner: key === "\\..." ? "..." : key, kind: "required" } + { kind: "required", key: key.slice(1) } + : key === "..." || key === "+" ? { kind: key, key } + : { + kind: "required", + key: + key === "\\..." ? "..." + : key === "\\+" ? "+" + : key + } type parseKey = k extends `${infer inner}?` ? inner extends `${infer baseName}${Scanner.EscapeToken}` ? PreparsedKey.from<{ kind: "required" - inner: `${baseName}?` + key: `${baseName}?` }> : PreparsedKey.from<{ kind: "optional" - inner: inner + key: inner }> - : k extends "..." ? PreparsedKey.from<{ kind: "spread"; inner: "..." }> - : k extends `${Scanner.EscapeToken}...` ? - PreparsedKey.from<{ kind: "required"; inner: "..." }> + : k extends MetaKey ? PreparsedKey.from<{ kind: k; key: k }> + : k extends `${Scanner.EscapeToken}${infer escapedMeta extends MetaKey}` ? + PreparsedKey.from<{ kind: "required"; key: escapedMeta }> : k extends IndexKey ? PreparsedKey.from<{ kind: "index" - inner: def + key: def }> : PreparsedKey.from<{ kind: "required" - inner: k extends ( + key: k extends ( `${Scanner.EscapeToken}${infer escapedIndexKey extends IndexKey}` ) ? escapedIndexKey @@ -221,12 +290,6 @@ type parseKey = : `${k & number}` }> -declare const indexParseSymbol: unique symbol - -export type indexParseError = { - [indexParseSymbol]: message -} - export const writeInvalidSpreadTypeMessage = ( def: def ): writeInvalidSpreadTypeMessage => diff --git a/ark/type/parser/semantic/bounds.ts b/ark/type/parser/semantic/bounds.ts index 8c14356efd..e3e7e3de4d 100644 --- a/ark/type/parser/semantic/bounds.ts +++ b/ark/type/parser/semantic/bounds.ts @@ -1,9 +1,6 @@ import type { LimitLiteral, writeUnboundableMessage } from "@arktype/schema" import type { ErrorMessage, array } from "@arktype/util" -import type { - Comparator, - InvertedComparators -} from "../string/reduce/shared.js" +import type { Comparator } from "../string/reduce/shared.js" import type { BoundExpressionKind, writeInvalidLimitMessage @@ -13,8 +10,7 @@ import type { astToString } from "./utils.js" import type { validateAst } from "./validate.js" export type validateRange = - l extends LimitLiteral ? - validateBound + l extends LimitLiteral ? validateBound : l extends [infer leftAst, Comparator, unknown] ? ErrorMessage>> : validateBound diff --git a/ark/type/parser/semantic/divisor.ts b/ark/type/parser/semantic/divisor.ts index ebabea1a48..4890672f69 100644 --- a/ark/type/parser/semantic/divisor.ts +++ b/ark/type/parser/semantic/divisor.ts @@ -1,4 +1,4 @@ -import type { Schema, writeIndivisibleMessage } from "@arktype/schema" +import type { Root, writeIndivisibleMessage } from "@arktype/schema" import type { ErrorMessage } from "@arktype/util" import type { inferAstIn } from "./infer.js" import type { validateAst } from "./validate.js" @@ -7,5 +7,5 @@ export type validateDivisor = inferAstIn extends infer data ? [data] extends [number] ? validateAst - : ErrorMessage>> + : ErrorMessage>> : never diff --git a/ark/type/parser/semantic/infer.ts b/ark/type/parser/semantic/infer.ts index 0d22c28dbd..68bcef210d 100644 --- a/ark/type/parser/semantic/infer.ts +++ b/ark/type/parser/semantic/infer.ts @@ -38,12 +38,12 @@ export type inferExpression = ast extends GenericInstantiationAst ? inferDefinition< generic["def"], - generic["$"]["$"] extends UnparsedScope ? + generic["$"]["t"] extends UnparsedScope ? // If the generic was defined in the current scope, its definition can be // resolved using the same scope as that of the input args. $ : // Otherwise, use the scope that was explicitly associated with it. - generic["$"]["$"], + generic["$"]["t"], { // Using keyof g["params"] & number here results in the element types // being mixed- another reason TS should not have separate `${number}` and number keys! diff --git a/ark/type/parser/semantic/validate.ts b/ark/type/parser/semantic/validate.ts index ff83440905..511881fc57 100644 --- a/ark/type/parser/semantic/validate.ts +++ b/ark/type/parser/semantic/validate.ts @@ -5,11 +5,11 @@ import type { writeMissingSubmoduleAccessMessage } from "@arktype/schema" import type { + anyOrNever, BigintLiteral, charsAfterFirst, Completion, ErrorMessage, - isAnyOrNever, writeMalformedNumericLiteralMessage } from "@arktype/util" import type { Comparator } from "../string/reduce/shared.js" @@ -82,7 +82,7 @@ type validateStringAst = ErrorMessage> : undefined : maybeExtractAlias extends infer alias extends keyof $ ? - isAnyOrNever<$[alias]> extends true ? def + [$[alias]] extends [anyOrNever] ? def : def extends PrivateDeclaration ? ErrorMessage> : // these problems would've been caught during a fullStringParse, but it's most diff --git a/ark/type/parser/string/reduce/dynamic.ts b/ark/type/parser/string/reduce/dynamic.ts index 987856c917..0db0e573ea 100644 --- a/ark/type/parser/string/reduce/dynamic.ts +++ b/ark/type/parser/string/reduce/dynamic.ts @@ -1,4 +1,4 @@ -import type { LimitLiteral, RawSchema } from "@arktype/schema" +import type { BaseRoot, LimitLiteral } from "@arktype/schema" import { isKeyOf, type requireKeys, @@ -28,8 +28,8 @@ import { type BranchState = { prefixes: StringifiablePrefixOperator[] leftBound: OpenLeftBound | null - intersection: RawSchema | null - union: RawSchema | null + intersection: BaseRoot | null + union: BaseRoot | null } export type DynamicStateWithRoot = requireKeys @@ -37,7 +37,7 @@ export type DynamicStateWithRoot = requireKeys export class DynamicState { readonly scanner: Scanner // set root type to `any` so that all constraints can be applied - root: RawSchema | undefined + root: BaseRoot | undefined branches: BranchState = { prefixes: [], leftBound: null, @@ -62,7 +62,7 @@ export class DynamicState { return this.root !== undefined } - setRoot(root: RawSchema): void { + setRoot(root: BaseRoot): void { this.root = root } @@ -72,7 +72,7 @@ export class DynamicState { return value } - constrainRoot(...args: Parameters["constrain"]>): void { + constrainRoot(...args: Parameters["constrain"]>): void { this.root = this.root!.constrain(args[0], args[1]) } diff --git a/ark/type/parser/string/reduce/shared.ts b/ark/type/parser/string/reduce/shared.ts index 477c368b24..c78ee030c4 100644 --- a/ark/type/parser/string/reduce/shared.ts +++ b/ark/type/parser/string/reduce/shared.ts @@ -17,22 +17,30 @@ export const maxComparators = { export type MaxComparator = keyof typeof maxComparators export const comparators = { - ...minComparators, - ...maxComparators, + ">": true, + ">=": true, + "<": true, + "<=": true, "==": true } export type Comparator = keyof typeof comparators -export const invertedComparators = { +export const invertedComparators: InvertedComparators = { "<": ">", ">": "<", "<=": ">=", ">=": "<=", "==": "==" -} as const satisfies Record +} -export type InvertedComparators = typeof invertedComparators +export type InvertedComparators = { + "<": ">" + ">": "<" + "<=": ">=" + ">=": "<=" + "==": "==" +} export type OpenLeftBound = { limit: LimitLiteral; comparator: MinComparator } diff --git a/ark/type/parser/string/shift/operand/enclosed.ts b/ark/type/parser/string/shift/operand/enclosed.ts index 5085e9f050..3d083cd56d 100644 --- a/ark/type/parser/string/shift/operand/enclosed.ts +++ b/ark/type/parser/string/shift/operand/enclosed.ts @@ -72,7 +72,8 @@ export type EnclosingQuote = keyof typeof enclosingQuote export const enclosingChar = { "/": 1, - ...enclosingQuote + "'": 1, + '"': 1 } as const export const enclosingTokens = { diff --git a/ark/type/parser/string/shift/operand/genericArgs.ts b/ark/type/parser/string/shift/operand/genericArgs.ts index afbe0d9d64..051c4cd042 100644 --- a/ark/type/parser/string/shift/operand/genericArgs.ts +++ b/ark/type/parser/string/shift/operand/genericArgs.ts @@ -1,4 +1,4 @@ -import type { RawSchema } from "@arktype/schema" +import type { BaseRoot } from "@arktype/schema" import type { ErrorMessage, join } from "@arktype/util" import type { DynamicState } from "../../reduce/dynamic.js" import { writeUnclosedGroupMessage } from "../../reduce/shared.js" @@ -17,7 +17,7 @@ export const parseGenericArgs = ( name: string, params: string[], s: DynamicState -): ParsedArgs => _parseGenericArgs(name, params, s, [], []) +): ParsedArgs => _parseGenericArgs(name, params, s, [], []) export type parseGenericArgs< name extends string, @@ -32,8 +32,8 @@ const _parseGenericArgs = ( params: string[], s: DynamicState, argDefs: string[], - argNodes: RawSchema[] -): ParsedArgs => { + argNodes: BaseRoot[] +): ParsedArgs => { const argState = s.parseUntilFinalizer() // remove the finalizing token from the argDef argDefs.push(argState.scanner.scanned.slice(0, -1)) diff --git a/ark/type/parser/string/shift/operand/unenclosed.ts b/ark/type/parser/string/shift/operand/unenclosed.ts index e19af09cba..d3c5f221f8 100644 --- a/ark/type/parser/string/shift/operand/unenclosed.ts +++ b/ark/type/parser/string/shift/operand/unenclosed.ts @@ -1,22 +1,22 @@ import { + BaseRoot, + hasArkKind, + writeUnresolvableMessage, type GenericProps, type PrivateDeclaration, - RawSchema, type arkKind, - hasArkKind, - type writeNonSubmoduleDotMessage, - writeUnresolvableMessage + type writeNonSubmoduleDotMessage } from "@arktype/schema" import { - type BigintLiteral, - type Completion, - type ErrorMessage, - type isAnyOrNever, - type join, printable, throwParseError, tryParseNumber, - tryParseWellFormedBigint + tryParseWellFormedBigint, + type BigintLiteral, + type Completion, + type ErrorMessage, + type anyOrNever, + type join } from "@arktype/util" import type { Generic } from "../../../../generic.js" import type { GenericInstantiationAst } from "../../../semantic/infer.js" @@ -26,9 +26,9 @@ import type { StaticState, state } from "../../reduce/static.js" import type { BaseCompletions } from "../../string.js" import type { Scanner } from "../scanner.js" import { - type ParsedArgs, parseGenericArgs, - writeInvalidGenericArgsMessage + writeInvalidGenericArgsMessage, + type ParsedArgs } from "./genericArgs.js" export const parseUnenclosed = (s: DynamicState): void => { @@ -45,8 +45,7 @@ export type parseUnenclosed = : tryResolve extends infer result ? result extends ErrorMessage ? state.error : result extends keyof $ ? - isAnyOrNever<$[result]> extends true ? - state.setRoot + [$[result]] extends [anyOrNever] ? state.setRoot : $[result] extends GenericProps ? parseGenericInstantiation< token, @@ -64,7 +63,7 @@ export const parseGenericInstantiation = ( name: string, g: Generic, s: DynamicState -): RawSchema => { +): BaseRoot => { s.scanner.shiftUntilNonWhitespace() const lookahead = s.scanner.shift() if (lookahead !== "<") @@ -98,7 +97,7 @@ export type parseGenericInstantiation< : never : state.error> -const unenclosedToNode = (s: DynamicState, token: string): RawSchema => +const unenclosedToNode = (s: DynamicState, token: string): BaseRoot => maybeParseReference(s, token) ?? maybeParseUnenclosedLiteral(s, token) ?? s.error( @@ -111,10 +110,10 @@ const unenclosedToNode = (s: DynamicState, token: string): RawSchema => const maybeParseReference = ( s: DynamicState, token: string -): RawSchema | undefined => { +): BaseRoot | undefined => { if (s.ctx.args?.[token]) return s.ctx.args[token].raw const resolution = s.ctx.$.maybeResolve(token) - if (resolution instanceof RawSchema) return resolution + if (resolution instanceof BaseRoot) return resolution if (resolution === undefined) return if (hasArkKind(resolution, "generic")) return parseGenericInstantiation(token, resolution as Generic, s) @@ -124,7 +123,7 @@ const maybeParseReference = ( const maybeParseUnenclosedLiteral = ( s: DynamicState, token: string -): RawSchema | undefined => { +): BaseRoot | undefined => { const maybeNumber = tryParseNumber(token, { strict: true }) if (maybeNumber !== undefined) return s.ctx.$.node("unit", { unit: maybeNumber }) diff --git a/ark/type/parser/string/shift/operator/bounds.ts b/ark/type/parser/string/shift/operator/bounds.ts index 304290c34c..07dd22545d 100644 --- a/ark/type/parser/string/shift/operator/bounds.ts +++ b/ark/type/parser/string/shift/operator/bounds.ts @@ -1,9 +1,9 @@ import { + type BaseRoot, type BoundKind, type DateLiteral, type LimitLiteral, - type NodeDef, - type RawSchema, + type NodeSchema, internalKeywords, jsObjects, tsKeywords, @@ -104,13 +104,15 @@ type shiftComparator< : start extends OneCharComparator ? [start, unscanned] : state.error -export const writeIncompatibleRangeMessage = (l: BoundKind, r: BoundKind) => - `Bound kinds ${l} and ${r} are incompatible` as const +export const writeIncompatibleRangeMessage = ( + l: BoundKind, + r: BoundKind +): string => `Bound kinds ${l} and ${r} are incompatible` export const getBoundKinds = ( comparator: Comparator, limit: LimitLiteral, - root: RawSchema, + root: BaseRoot, boundKind: BoundExpressionKind ): BoundKind[] => { if (root.extends(tsKeywords.number)) { @@ -152,9 +154,9 @@ export const singleEqualsMessage = "= is not a valid comparator. Use == to check for equality" type singleEqualsMessage = typeof singleEqualsMessage -const openLeftBoundToSchema = ( +const openLeftBoundToRoot = ( leftBound: OpenLeftBound -): NodeDef => ({ +): NodeSchema => ({ rule: isDateLiteral(leftBound.limit) ? extractDateLiteralSource(leftBound.limit) @@ -208,10 +210,7 @@ export const parseRightBound = ( previousRoot, "left" ) - s.constrainRoot( - lowerBoundKind[0], - openLeftBoundToSchema(s.branches.leftBound) - ) + s.constrainRoot(lowerBoundKind[0], openLeftBoundToRoot(s.branches.leftBound)) s.branches.leftBound = null } @@ -257,7 +256,7 @@ export const writeInvalidLimitMessage = < boundKind === "left" ? invertedComparators[comparator] : (comparator as any) } must be ${ boundKind === "left" ? "preceded" : ("followed" as any) - } by a corresponding literal (was '${limit}')` + } by a corresponding literal (was ${limit})` export type writeInvalidLimitMessage< comparator extends Comparator, @@ -265,6 +264,6 @@ export type writeInvalidLimitMessage< boundKind extends BoundExpressionKind > = `Comparator ${boundKind extends "left" ? InvertedComparators[comparator] : comparator} must be ${boundKind extends "left" ? "preceded" -: "followed"} by a corresponding literal (was '${limit}')` +: "followed"} by a corresponding literal (was ${limit})` export type BoundExpressionKind = "left" | "right" diff --git a/ark/type/parser/string/string.ts b/ark/type/parser/string/string.ts index 81cba3a7ff..b39abad89a 100644 --- a/ark/type/parser/string/string.ts +++ b/ark/type/parser/string/string.ts @@ -1,4 +1,4 @@ -import type { RawSchema } from "@arktype/schema" +import type { BaseRoot } from "@arktype/schema" import { type ErrorMessage, throwInternalError, @@ -41,7 +41,7 @@ export type BaseCompletions<$, args, otherSuggestions extends string = never> = | StringifiablePrefixOperator | otherSuggestions -export const fullStringParse = (s: DynamicState): RawSchema => { +export const fullStringParse = (s: DynamicState): BaseRoot => { s.parseOperand() const result = parseUntilFinalizer(s).root if (!result) { @@ -72,7 +72,7 @@ export type parseUntilFinalizer = parseUntilFinalizer, $, args> : s -const next = (s: DynamicState) => +const next = (s: DynamicState): void => s.hasRoot() ? s.parseOperator() : s.parseOperand() type next = diff --git a/ark/type/parser/tuple.ts b/ark/type/parser/tuple.ts index 433083d3bf..74ffbbbc96 100644 --- a/ark/type/parser/tuple.ts +++ b/ark/type/parser/tuple.ts @@ -3,14 +3,14 @@ import { makeRootAndArrayPropertiesMutable, tsKeywords, type BaseMeta, + type BaseRoot, type Morph, type MutableInner, type Node, type Out, type Predicate, - type RawSchema, type UnionChildKind, - type UnknownSchema, + type UnknownRoot, type distillConstrainableIn, type distillConstrainableOut, type inferIntersection, @@ -35,10 +35,10 @@ import type { InfixOperator, PostfixExpression } from "./semantic/infer.js" import { writeMissingRightOperandMessage } from "./string/shift/operand/unenclosed.js" import type { BaseCompletions } from "./string/string.js" -export const parseTuple = (def: array, ctx: ParseContext): RawSchema => +export const parseTuple = (def: array, ctx: ParseContext): BaseRoot => maybeParseTupleExpression(def, ctx) ?? parseTupleLiteral(def, ctx) -export const parseTupleLiteral = (def: array, ctx: ParseContext): RawSchema => { +export const parseTupleLiteral = (def: array, ctx: ParseContext): BaseRoot => { let sequences: MutableInner<"sequence">[] = [{}] let i = 0 while (i < def.length) { @@ -93,11 +93,11 @@ type ElementKind = "optional" | "required" | "variadic" const appendElement = ( base: MutableInner<"sequence">, kind: ElementKind, - element: UnknownSchema + element: UnknownRoot ): MutableInner<"sequence"> => { switch (kind) { case "required": - if (base.optional) + if (base.optionals) // e.g. [string?, number] return throwParseError(requiredPostOptionalMessage) if (base.variadic) { @@ -113,7 +113,7 @@ const appendElement = ( // e.g. [...string[], number?] return throwParseError(optionalPostVariadicMessage) // e.g. [string, number?] - base.optional = append(base.optional, element) + base.optionals = append(base.optionals, element) return base case "variadic": // e.g. [...string[], number, ...string[]] @@ -143,7 +143,7 @@ const appendSpreadBranch = ( return appendElement(base, "variadic", tsKeywords.unknown) } spread.prefix.forEach(node => appendElement(base, "required", node)) - spread.optional.forEach(node => appendElement(base, "optional", node)) + spread.optionals.forEach(node => appendElement(base, "optional", node)) spread.variadic && appendElement(base, "variadic", spread.variadic) spread.postfix.forEach(node => appendElement(base, "required", node)) return base @@ -152,25 +152,12 @@ const appendSpreadBranch = ( const maybeParseTupleExpression = ( def: array, ctx: ParseContext -): RawSchema | undefined => { +): BaseRoot | undefined => { const tupleExpressionResult = isIndexZeroExpression(def) ? prefixParsers[def[0]](def as never, ctx) : isIndexOneExpression(def) ? indexOneParsers[def[1]](def as never, ctx) : undefined return tupleExpressionResult - - // TODO: remove - // return tupleExpressionResult.isNever() - // ? throwParseError( - // writeUnsatisfiableExpressionError( - // def - // .map((def) => - // typeof def === "string" ? def : printable(def) - // ) - // .join(" ") - // ) - // ) - // : tupleExpressionResult } // It is *extremely* important we use readonly any time we check a tuple against @@ -317,7 +304,7 @@ export const writeNonArraySpreadMessage = ( `Spread element must be an array (was ${operand})` as never type writeNonArraySpreadMessage = - `Spread element must be an array${operand extends string ? `(was ${operand})` + `Spread element must be an array${operand extends string ? ` (was ${operand})` : ""}` export const multipleVariadicMesage = @@ -427,12 +414,12 @@ const parseArrayTuple: PostfixParser<"[]"> = (def, ctx) => export type PostfixParser = ( def: IndexOneExpression, ctx: ParseContext -) => RawSchema +) => BaseRoot export type PrefixParser = ( def: IndexZeroExpression, ctx: ParseContext -) => RawSchema +) => BaseRoot export type TupleExpression = IndexZeroExpression | IndexOneExpression @@ -464,10 +451,10 @@ export const parseMorphTuple: PostfixParser<"=>"> = (def, ctx) => { export const writeMalformedFunctionalExpressionMessage = ( operator: ":" | "=>", value: unknown -) => +): string => `${ operator === ":" ? "Narrow" : "Morph" - } expression requires a function following '${operator}' (was ${typeof value})` as const + } expression requires a function following '${operator}' (was ${typeof value})` export type parseMorph = morph extends Morph ? @@ -538,5 +525,5 @@ export const writeInvalidConstructorMessage = < actual extends Domain | BuiltinObjectKind >( actual: actual -) => - `Expected a constructor following 'instanceof' operator (was ${actual})` as const +): string => + `Expected a constructor following 'instanceof' operator (was ${actual})` diff --git a/ark/type/scope.ts b/ark/type/scope.ts index fac33a9082..1271cc39f9 100644 --- a/ark/type/scope.ts +++ b/ark/type/scope.ts @@ -1,53 +1,53 @@ import { + RawRootScope, + hasArkKind, type ArkConfig, + type BaseRoot, type GenericProps, type PreparsedNodeResolution, type PrivateDeclaration, - type RawSchema, - type RawSchemaResolutions, - RawSchemaScope, - type SchemaScope, - type UnknownSchema, + type RawRootResolutions, + type RootScope, + type UnknownRoot, type ambient, type arkKind, type destructuredExportContext, type destructuredImportContext, type exportedNameOf, - hasArkKind, type writeDuplicateAliasError } from "@arktype/schema" import { - type Dict, domainOf, hasDomain, - type isAnyOrNever, isThunk, + throwParseError, + type Dict, + type anyOrNever, type keyError, type nominal, - type show, - throwParseError + type show } from "@arktype/util" import type { type } from "./ark.js" import { Generic } from "./generic.js" -import { type MatchParser, createMatchParser } from "./match.js" +import { createMatchParser, type MatchParser } from "./match.js" import type { Module } from "./module.js" import { - type inferDefinition, parseObject, - type validateDefinition, - writeBadDefinitionTypeMessage + writeBadDefinitionTypeMessage, + type inferDefinition, + type validateDefinition } from "./parser/definition.js" import { + parseGenericParams, type GenericDeclaration, - type GenericParamsParseError, - parseGenericParams + type GenericParamsParseError } from "./parser/generic.js" import { DynamicState } from "./parser/string/reduce/dynamic.js" import { fullStringParse } from "./parser/string/string.js" import { + RawTypeParser, type DeclarationParser, type DefinitionParser, - RawTypeParser, type Type, type TypeParser } from "./type.js" @@ -133,7 +133,7 @@ export type resolve = args[reference] : $[reference & keyof $] ) extends infer resolution ? - isAnyOrNever extends true ? resolution + [resolution] extends [anyOrNever] ? resolution : resolution extends Def ? inferDefinition : resolution : never @@ -154,13 +154,13 @@ export type tryInferSubmoduleReference<$, token> = export interface ParseContext { $: RawScope - args?: Record + args?: Record } export const scope: ScopeParser = ((def: Dict, config: ArkConfig = {}) => new RawScope(def, config)) as never -export interface Scope<$ = any> extends SchemaScope<$> { +export interface Scope<$ = any> extends RootScope<$> { type: TypeParser<$> match: MatchParser<$> @@ -179,9 +179,9 @@ export interface Scope<$ = any> extends SchemaScope<$> { } export class RawScope< - $ extends RawSchemaResolutions = RawSchemaResolutions -> extends RawSchemaScope<$> { - private parseCache: Record = {} + $ extends RawRootResolutions = RawRootResolutions +> extends RawRootScope<$> { + private parseCache: Record = {} constructor(def: Record, config?: ArkConfig) { const aliases: Record = {} @@ -196,15 +196,15 @@ export class RawScope< super(aliases, config) } - type = new RawTypeParser(this as never) + type: RawTypeParser = new RawTypeParser(this as never) match: MatchParser<$> = createMatchParser(this as never) as never - declare = (() => ({ + declare: () => { type: RawTypeParser } = (() => ({ type: this.type })).bind(this) - define = ((def: unknown) => def).bind(this) + define: (def: unknown) => unknown = ((def: unknown) => def).bind(this) override preparseRoot(def: unknown): unknown { if (isThunk(def) && !hasArkKind(def, "generic")) return def() @@ -212,8 +212,8 @@ export class RawScope< return def } - override parseRoot(def: unknown): RawSchema { - // args: { this: {} as RawSchema }, + override parseRoot(def: unknown): BaseRoot { + // args: { this: {} as RawRoot }, return this.parse(def, { $: this as never, args: {} @@ -222,7 +222,7 @@ export class RawScope< }).bindScope(this) } - parse(def: unknown, ctx: ParseContext): RawSchema { + parse(def: unknown, ctx: ParseContext): BaseRoot { if (typeof def === "string") { if (ctx.args && Object.keys(ctx.args).every(k => !def.includes(k))) { // we can only rely on the cache if there are no contextual @@ -239,11 +239,11 @@ export class RawScope< : throwParseError(writeBadDefinitionTypeMessage(domainOf(def))) } - parseString(def: string, ctx: ParseContext): RawSchema { + parseString(def: string, ctx: ParseContext): BaseRoot { return ( - this.maybeResolveSchema(def) ?? + this.maybeResolveRoot(def) ?? ((def.endsWith("[]") && - this.maybeResolveSchema(def.slice(0, -2))?.array()) || + this.maybeResolveRoot(def.slice(0, -2))?.array()) || fullStringParse(new DynamicState(def, ctx))) ) } diff --git a/ark/type/tsconfig.build.json b/ark/type/tsconfig.build.json index 80a796b963..f74ef64d4c 120000 --- a/ark/type/tsconfig.build.json +++ b/ark/type/tsconfig.build.json @@ -1 +1 @@ -../repo/tsconfig.build.json \ No newline at end of file +../repo/tsconfig.esm.json \ No newline at end of file diff --git a/ark/type/type.ts b/ark/type/type.ts index fbd1cbaea5..7cb448c859 100644 --- a/ark/type/type.ts +++ b/ark/type/type.ts @@ -1,15 +1,15 @@ import { ArkErrors, + BaseRoot, type BaseMeta, - type BaseRoot, type Disjoint, + type InnerRoot, type Morph, - type NodeDef, + type NodeSchema, type Out, type Predicate, type PrimitiveConstraintKind, - RawSchema, - type Schema, + type Root, type ambient, type constrain, type constraintKindOf, @@ -86,7 +86,7 @@ const typeParserAttachments = Object.freeze({ } satisfies TypeParserAttachments) export class RawTypeParser extends Callable< - (...args: unknown[]) => RawSchema | Generic, + (...args: unknown[]) => BaseRoot | Generic, TypeParserAttachments > { constructor($: RawScope) { @@ -130,8 +130,8 @@ export type DeclarationParser<$> = () => { // methods of BaseRoot are overridden, but we end up exporting it as an interface // to ensure it is not accessed as a runtime value declare class _Type - extends BaseRoot - implements internalImplementationOf + extends InnerRoot + implements internalImplementationOf { $: Scope<$>; @@ -231,7 +231,7 @@ declare class _Type constrain< kind extends PrimitiveConstraintKind, - const def extends NodeDef + const def extends NodeSchema >( kind: conform>, def: def @@ -249,7 +249,7 @@ export type TypeConstructor = new ( $: Scope<$> ) => Type -export const Type: TypeConstructor = RawSchema as never +export const Type: TypeConstructor = BaseRoot as never export type DefinitionParser<$> = (def: validateTypeRoot) => def diff --git a/ark/util/__tests__/records.test.ts b/ark/util/__tests__/records.test.ts new file mode 100644 index 0000000000..255f9c7c3d --- /dev/null +++ b/ark/util/__tests__/records.test.ts @@ -0,0 +1,82 @@ +import { attest, contextualize } from "@arktype/attest" +import type { withJsDoc } from "../records.js" + +contextualize(() => { + it("identical keys", () => { + interface Source { + /** is a foo */ + foo?: string + } + + interface Target { + foo: "bar" + } + + type Result = withJsDoc + + const result: Result = { foo: "bar" } + // should have annotation "is a foo" + result.foo + + attest() + }) + + it("less keys", () => { + interface Source { + /** is a foo */ + foo?: string + bar: number + } + + interface Target { + foo: "foo" + } + + type Result = withJsDoc + + const result: Result = { foo: "foo" } + // should have annotation "is a foo" + result.foo + + attest, Target>() + }) + + it("more keys", () => { + interface Source { + /** is a foo */ + foo?: string + } + + interface Target { + foo: "foo" + baz: "baz" + } + + type Result = withJsDoc + + const result: Result = { foo: "foo", baz: "baz" } + // should have annotation "is a foo" + result.foo + + attest, Target>() + }) + + it("requires optional keys on target", () => { + interface Source { + /** is a foo */ + foo?: string + } + + interface Target { + foo?: "foo" + } + + type Result = withJsDoc + + const result: Result = { foo: "foo" } + // should have annotation "is a foo" + result.foo + + attest, { foo: "foo" }>() + }) +}) diff --git a/ark/util/__tests__/traits.example.ts b/ark/util/__tests__/traits.example.ts index d3ceb69455..c2d7f9966e 100644 --- a/ark/util/__tests__/traits.example.ts +++ b/ark/util/__tests__/traits.example.ts @@ -9,11 +9,11 @@ export class Rectangle extends Trait { super() } - area() { + area(): number { return this.length * this.width } - perimeter() { + perimeter(): number { return 2 * (this.length + this.width) } } @@ -31,7 +31,7 @@ export class Rhombus extends Trait<{ super() } - perimeter() { + perimeter(): number { return this.side * 4 } } diff --git a/ark/util/__tests__/traits.test.ts b/ark/util/__tests__/traits.test.ts index 5b6eee97e5..b27ad136b0 100644 --- a/ark/util/__tests__/traits.test.ts +++ b/ark/util/__tests__/traits.test.ts @@ -24,7 +24,7 @@ export class Boundable extends Trait<{ this.limit = rule.limit } - check(data: data) { + check(data: data): boolean { return this.limit === undefined || this.sizeOf(data) <= this.limit } } diff --git a/ark/util/main.ts b/ark/util/api.ts similarity index 100% rename from ark/util/main.ts rename to ark/util/api.ts diff --git a/ark/util/arrays.ts b/ark/util/arrays.ts index 0aaf29cbd4..cb7c0f0aba 100644 --- a/ark/util/arrays.ts +++ b/ark/util/arrays.ts @@ -144,8 +144,7 @@ export type AppendOptions = { } /** - * Adds a value to an array, returning the array - * (based on the implementation from TypeScript's codebase) + * Adds a value or array to an array, returning the concatenated result * * @param to The array to which `value` is to be added. If `to` is `undefined`, a new array * is created as `[value]` if value was not undefined, otherwise `[]`. @@ -156,7 +155,7 @@ export type AppendOptions = { export const append = < to extends element[] | undefined, element, - value extends element | undefined + value extends listable | undefined >( to: to, value: value, @@ -164,10 +163,17 @@ export const append = < ): Exclude | Extract => { if (value === undefined) return to ?? ([] as any) - if (to === undefined) return value === undefined ? [] : ([value] as any) + if (to === undefined) { + return ( + value === undefined ? [] + : Array.isArray(value) ? value + : ([value] as any) + ) + } - if (opts?.prepend) to.unshift(value) - else to.push(value) + if (opts?.prepend) + Array.isArray(value) ? to.unshift(...value) : to.unshift(value as never) + else Array.isArray(value) ? to.push(...value) : to.push(value as never) return to as never } @@ -224,11 +230,11 @@ export type groupableKeyOf = { }[keyof t] export type groupBy> = { - [k in element[discriminator] & PropertyKey]?: element extends unknown ? + [k in element[discriminator] & PropertyKey]?: (element extends unknown ? isDisjoint extends true ? never - : element[] - : never + : element + : never)[] } & unknown export const groupBy = >( diff --git a/ark/util/clone.ts b/ark/util/clone.ts index 0746e4ab77..e7cab175b2 100644 --- a/ark/util/clone.ts +++ b/ark/util/clone.ts @@ -4,7 +4,10 @@ export const shallowClone = (input: input): input => Object.getOwnPropertyDescriptors(input) ) -export const deepClone = (input: input, seen = new Map()): input => { +export const deepClone = (input: input): input => + _deepClone(input, new Map()) + +const _deepClone = (input: input, seen: Map): input => { if (typeof input !== "object" || input === null) return input if (seen.has(input)) return seen.get(input) @@ -21,7 +24,7 @@ export const deepClone = (input: input, seen = new Map()): input => { const propertyDescriptors = Object.getOwnPropertyDescriptors(input) for (const key of Object.keys(propertyDescriptors)) { - propertyDescriptors[key].value = deepClone( + propertyDescriptors[key].value = _deepClone( propertyDescriptors[key].value, seen ) diff --git a/ark/util/compilation.ts b/ark/util/compilation.ts index b0af4e586a..b7504209e4 100644 --- a/ark/util/compilation.ts +++ b/ark/util/compilation.ts @@ -40,8 +40,8 @@ export class CompiledFunction< return compileLiteralPropAccess(key, optional) } - index(key: string, optional = false): string { - return indexPropAccess(key, optional) + index(key: string | number, optional = false): string { + return indexPropAccess(`${key}`, optional) } line(statement: string): this { @@ -124,5 +124,5 @@ export const compileLiteralPropAccess = ( export const serializeLiteralKey = (key: PropertyKey): string => typeof key === "symbol" ? registeredReference(key) : JSON.stringify(key) -export const indexPropAccess = (key: string, optional = false) => - `${optional ? "?." : ""}[${key}]` as const +export const indexPropAccess = (key: string, optional = false): string => + `${optional ? "?." : ""}[${key}]` diff --git a/ark/util/functions.ts b/ark/util/functions.ts index ae5db23668..d17244c891 100644 --- a/ark/util/functions.ts +++ b/ark/util/functions.ts @@ -1,18 +1,36 @@ import { throwInternalError } from "./errors.js" import { NoopBase } from "./records.js" -export const cached = (thunk: () => T): (() => T) => { - let isCached = false - let result: T | undefined - return () => { - if (!isCached) { - result = thunk() - isCached = true - } - return result as T - } +export const bound = ( + target: Function, + ctx: ClassMemberDecoratorContext +): void => { + ctx.addInitializer(function (this: any) { + this[ctx.name] = this[ctx.name].bind(this) + }) } +export const cached = ( + target: (this: self) => any, + context: + | ClassGetterDecoratorContext + | ClassMethodDecoratorContext any> +) => + function (this: self): any { + const value = target.call(this) + Object.defineProperty( + this, + context.name, + context.kind === "getter" ? + { value } + : { + value: () => value, + enumerable: false + } + ) + return value + } + export const isThunk = ( value: value ): value is Extract extends never ? value & Thunk diff --git a/ark/util/generics.ts b/ark/util/generics.ts index cd86aa4f73..c44b582593 100644 --- a/ark/util/generics.ts +++ b/ark/util/generics.ts @@ -44,7 +44,9 @@ export type UnknownUnion = export type andPreserveUnknown = unknown extends l & r ? unknown : show -export type isAnyOrNever = [unknown, t] extends [t, {}] ? true : isNever +declare const anyOrNever: unique symbol + +export type anyOrNever = typeof anyOrNever export type isAny = [unknown, t] extends [t, {}] ? true : false @@ -59,11 +61,13 @@ export type isUnknown = export type conform = t extends base ? t : base -export type equals = - (<_>() => _ extends t ? 1 : 2) extends <_>() => _ extends u ? 1 : 2 ? true +export type equals = [l, r] extends [r, l] ? true : false + +export type exactEquals = + (<_>() => _ extends l ? 1 : 2) extends <_>() => _ extends r ? 1 : 2 ? true : false -export const id = Symbol("id") +export const id: unique symbol = Symbol("id") export type nominal = t & { readonly [id]: id diff --git a/ark/util/numericLiterals.ts b/ark/util/numericLiterals.ts index 42ea1499f7..874f08f858 100644 --- a/ark/util/numericLiterals.ts +++ b/ark/util/numericLiterals.ts @@ -20,12 +20,11 @@ export type BigintLiteral = `${value}n` * 3. If the value includes a decimal, its last digit may not be 0 * 4. The value may not be "-0" */ -export const wellFormedNumberMatcher = +export const wellFormedNumberMatcher: RegExp = /^(?!^-0$)-?(?:0|[1-9]\d*)(?:\.\d*[1-9])?$/ -export const isWellFormedNumber = wellFormedNumberMatcher.test.bind( - wellFormedNumberMatcher -) +export const isWellFormedNumber: RegExp["test"] = + wellFormedNumberMatcher.test.bind(wellFormedNumberMatcher) const numberLikeMatcher = /^-?\d*\.?\d*$/ const isNumberLike = (s: string) => s.length !== 0 && numberLikeMatcher.test(s) @@ -35,10 +34,9 @@ const isNumberLike = (s: string) => s.length !== 0 && numberLikeMatcher.test(s) * 1. must begin with an integer, the first digit of which cannot be 0 unless the entire value is 0 * 2. The value may not be "-0" */ -export const wellFormedIntegerMatcher = /^(?:0|(?:-?[1-9]\d*))$/ -export const isWellFormedInteger = wellFormedIntegerMatcher.test.bind( - wellFormedIntegerMatcher -) +export const wellFormedIntegerMatcher: RegExp = /^(?:0|(?:-?[1-9]\d*))$/ +export const isWellFormedInteger: RegExp["test"] = + wellFormedIntegerMatcher.test.bind(wellFormedIntegerMatcher) const integerLikeMatcher = /^-?\d+$/ const isIntegerLike = integerLikeMatcher.test.bind(integerLikeMatcher) diff --git a/ark/util/objectKinds.ts b/ark/util/objectKinds.ts index 2dee1c9f3d..9e261c2ec7 100644 --- a/ark/util/objectKinds.ts +++ b/ark/util/objectKinds.ts @@ -2,9 +2,25 @@ import type { array } from "./arrays.js" import { type Domain, type domainDescriptions, domainOf } from "./domain.js" import { type Key, isKeyOf } from "./records.js" +export type builtinConstructors = { + Array: ArrayConstructor + Date: DateConstructor + Error: ErrorConstructor + Function: FunctionConstructor + Map: MapConstructor + RegExp: RegExpConstructor + Set: SetConstructor + String: StringConstructor + Number: NumberConstructor + Boolean: BooleanConstructor + WeakMap: WeakMapConstructor + WeakSet: WeakSetConstructor + Promise: PromiseConstructor +} + // Built-in object constructors based on a subset of: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects -export const builtinObjectKinds = { +export const builtinConstructors: builtinConstructors = { Array, Date, Error, @@ -18,11 +34,9 @@ export const builtinObjectKinds = { WeakMap, WeakSet, Promise -} as const satisfies ObjectKindSet - -export type ObjectKindSet = Record +} -export type BuiltinObjectConstructors = typeof builtinObjectKinds +export type BuiltinObjectConstructors = typeof builtinConstructors export type BuiltinObjectKind = keyof BuiltinObjectConstructors @@ -30,39 +44,33 @@ export type BuiltinObjects = { [kind in BuiltinObjectKind]: InstanceType } -export type objectKindOf< - data extends object, - kinds extends ObjectKindSet = BuiltinObjectConstructors -> = - object extends data ? keyof kinds | undefined +export type objectKindOf = + object extends data ? keyof builtinConstructors | undefined : data extends (...args: never[]) => unknown ? "Function" - : instantiableObjectKind extends never ? keyof kinds | undefined - : instantiableObjectKind + : instantiableObjectKind extends never ? + keyof builtinConstructors | undefined + : instantiableObjectKind export type describeObject = objectKindOf extends string ? objectKindDescriptions[objectKindOf] : domainDescriptions["object"] -type instantiableObjectKind< - data extends object, - kinds extends ObjectKindSet -> = { - [kind in keyof kinds]: data extends InstanceType ? kind : never -}[keyof kinds] - -export const objectKindOf = < - data extends object, - kinds extends ObjectKindSet = BuiltinObjectConstructors ->( - data: data, - kinds?: kinds -): objectKindOf | undefined => { - const kindSet: ObjectKindSet = kinds ?? builtinObjectKinds +type instantiableObjectKind = { + [kind in keyof builtinConstructors]: data extends ( + InstanceType + ) ? + kind + : never +}[keyof builtinConstructors] + +export const objectKindOf = ( + data: data +): objectKindOf | undefined => { let prototype: Partial | null = Object.getPrototypeOf(data) while ( prototype?.constructor && - (!kindSet[prototype.constructor.name] || - !(data instanceof kindSet[prototype.constructor.name])) + (!isKeyOf(prototype.constructor.name, builtinConstructors) || + !(data instanceof builtinConstructors[prototype.constructor.name])) ) prototype = Object.getPrototypeOf(prototype) @@ -72,36 +80,25 @@ export const objectKindOf = < return name as never } -export const objectKindOrDomainOf = < - data, - kinds extends ObjectKindSet = BuiltinObjectConstructors ->( - data: data, - kinds?: kinds -): (objectKindOf & {}) | domainOf => +export const objectKindOrDomainOf = ( + data: data +): (objectKindOf & {}) | domainOf => (typeof data === "object" && data !== null ? - objectKindOf(data, kinds) ?? "object" + objectKindOf(data) ?? "object" : domainOf(data)) as never -export type objectKindOrDomainOf< - data, - kinds extends ObjectKindSet = BuiltinObjectConstructors -> = +export type objectKindOrDomainOf = data extends object ? - objectKindOf extends undefined ? + objectKindOf extends undefined ? "object" - : objectKindOf + : objectKindOf : domainOf -export const hasObjectKind = < - kind extends keyof kinds, - kinds extends ObjectKindSet = BuiltinObjectConstructors ->( +export const hasObjectKind = ( data: object, - kind: kind, - kinds?: kinds -): data is InstanceType => - objectKindOf(data, kinds) === (kind as never) + kind: kind +): data is InstanceType => + objectKindOf(data) === (kind as never) export const isArray = (data: unknown): data is array => Array.isArray(data) @@ -132,8 +129,8 @@ export const getExactBuiltinConstructorName = ( const constructorName: string | null = Object(ctor).name ?? null return ( constructorName && - isKeyOf(constructorName, builtinObjectKinds) && - builtinObjectKinds[constructorName] === ctor + isKeyOf(constructorName, builtinConstructors) && + builtinConstructors[constructorName] === ctor ) ? constructorName : null diff --git a/ark/util/package.json b/ark/util/package.json index d500211005..86ef03d93d 100644 --- a/ark/util/package.json +++ b/ark/util/package.json @@ -1,27 +1,20 @@ { "name": "@arktype/util", - "version": "0.0.3", + "version": "0.0.41", "author": { "name": "David Blass", "email": "david@arktype.io", "url": "https://arktype.io" }, "type": "module", - "main": "./out/main.js", - "types": "./out/main.d.ts", + "main": "./out/api.js", + "types": "./out/api.d.ts", "exports": { - ".": { - "types": "./out/main.d.ts", - "default": "./out/main.js" - }, - "./internal/*": { - "default": "./out/*" - } + ".": "./out/api.js", + "./internal/*": "./out/*" }, "files": [ - "out", - "!__tests__", - "**/*.ts" + "out" ], "scripts": { "build": "tsx ../repo/build.ts", diff --git a/ark/util/records.ts b/ark/util/records.ts index 7ed5f3899e..04adc8a50e 100644 --- a/ark/util/records.ts +++ b/ark/util/records.ts @@ -124,17 +124,7 @@ export type requiredKeyOf = { export type optionalKeyOf = Exclude> -export type optionalizeKeys = show< - { [k in Exclude, keys>]: o[k] } & { - [k in optionalKeyOf | keys]?: o[k] - } -> - -export type merge = show< - { - [k in Exclude]: base[k] - } & merged -> +export type merge = show & merged> export type override< base, @@ -216,3 +206,25 @@ export type invert> = { export const invert = >( t: t ): invert => flatMorph(t as any, (k, v) => [v, k]) as never + +export const unset = Symbol("represents an uninitialized value") + +export type unset = typeof unset + +/** + * For each keyof o that also exists on jsDocSource, add associated JsDoc annotations to o. + * Does not preserve modifiers on o like optionality. + */ +export type withJsDoc = show< + keyof o extends keyof jsDocSource ? + keyof jsDocSource extends keyof o ? + _withJsDoc + : Pick<_withJsDoc, keyof o & keyof jsDocSource> + : Pick<_withJsDoc, keyof o & keyof jsDocSource> & { + [k in Exclude]: o[k] + } +> + +type _withJsDoc = { + [k in keyof jsDocSource]-?: o[k & keyof o] +} diff --git a/ark/util/registry.ts b/ark/util/registry.ts index 5f0114c362..39299d5ff9 100644 --- a/ark/util/registry.ts +++ b/ark/util/registry.ts @@ -28,10 +28,13 @@ export const register = (value: object | symbol): string => { return name } -export const reference = (name: string): `$ark.${string}` => `$ark.${name}` +export const reference = (name: string): RegisteredReference => `$ark.${name}` -export const registeredReference = (value: object | symbol): `$ark.${string}` => - reference(register(value)) +export const registeredReference = ( + value: object | symbol +): RegisteredReference => reference(register(value)) + +export type RegisteredReference = `$ark.${to}` export const isDotAccessible = (keyName: string): boolean => /^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(keyName) diff --git a/ark/util/tsconfig.build.json b/ark/util/tsconfig.build.json index 80a796b963..f74ef64d4c 120000 --- a/ark/util/tsconfig.build.json +++ b/ark/util/tsconfig.build.json @@ -1 +1 @@ -../repo/tsconfig.build.json \ No newline at end of file +../repo/tsconfig.esm.json \ No newline at end of file diff --git a/package.json b/package.json index c12ea81959..5e933a52b3 100644 --- a/package.json +++ b/package.json @@ -15,17 +15,16 @@ "type": "module", "private": true, "scripts": { - "prChecks": "pnpm install && pnpm lint && pnpm tnt && pnpm build", + "prChecks": "pnpm lint && pnpm testRepo && pnpm testTsVersions", "build": "pnpm -r build", "buildCjs": "ARKTYPE_CJS=1 pnpm -r build", - "test": "mocha", - "tnt": "mocha --skipTypes --exclude 'ark/attest/**/*.test.*'", - "testRepo": "pnpm test && cd ./ark/test && pnpm test && pnpm testTsVersions", + "test": "pnpm testTyped --skipTypes", + "testTyped": "mocha --exclude 'ark/attest/**/*.test.*'", + "testRepo": "pnpm test && cd ./ark/attest && pnpm test", "testTsVersions": "tsx ./ark/repo/testTsVersions.ts", "typecheck": "tsc --noEmit", - "lint": "eslint --max-warnings=0 .", + "lint": "prettier --check . && eslint --max-warnings=0 .", "format": "prettier --write .", - "checkFormat": "prettier --check .", "knip": "knip", "ci:publish": "changeset publish", "ci:version": "tsx ./ark/repo/updateVersions.ts" @@ -36,7 +35,7 @@ "@arktype/repo": "workspace:*", "@changesets/changelog-github": "0.5.0", "@changesets/cli": "2.27.1", - "@types/node": "20.12.7", + "@types/node": "20.12.12", "prettier": "3.2.5", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", @@ -46,16 +45,22 @@ "eslint-plugin-mdx": "3.1.5", "eslint-plugin-only-warn": "1.1.0", "eslint-plugin-prefer-arrow-functions": "3.3.2", - "eslint-plugin-unused-imports": "3.1.0", - "@typescript-eslint/eslint-plugin": "7.6.0", - "@typescript-eslint/parser": "7.6.0", + "@typescript-eslint/eslint-plugin": "7.9.0", + "@typescript-eslint/parser": "7.9.0", "c8": "9.1.0", - "knip": "5.9.4", - "tsx": "4.7.2", + "knip": "5.16.0", + "tsx": "4.10.4", "typescript": "5.4.5", + "typescript-min": "npm:typescript@5.1.6", + "typescript-nightly": "npm:typescript@next", "mocha": "10.4.0", "@types/mocha": "10.0.6" }, + "pnpm": { + "overrides": { + "esbuild": "0.21.2" + } + }, "mocha": { "//": "IF YOU UPDATE THE MOCHA CONFIG HERE, PLEASE ALSO UPDATE ark/repo/mocha.jsonc AND .vscode/settings.json", "spec": [ diff --git a/tsconfig.json b/tsconfig.json index 05cc6df32b..7252662fa1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "NodeNext", - "target": "ESNext", + "target": "ES2022", "moduleResolution": "NodeNext", "lib": ["ESNext"], "noEmit": true, @@ -14,14 +14,19 @@ "exactOptionalPropertyTypes": true, "noFallthroughCasesInSwitch": true, "stripInternal": true, + // should be off by default, but here as a convenience to toggle + // "noErrorTruncation": true, + // "isolatedDeclarations": true, "paths": { - "arktype": ["./ark/type/main.ts"], - "@arktype/util": ["./ark/util/main.ts"], - "@arktype/fs": ["./ark/fs/main.ts"], - "@arktype/attest": ["./ark/attest/main.ts"], - "@arktype/schema": ["./ark/schema/main.ts"] + "arktype": ["./ark/type/api.ts"], + "arktype/config": ["./ark/type/config.ts"], + "@arktype/util": ["./ark/util/api.ts"], + "@arktype/fs": ["./ark/fs/api.ts"], + "@arktype/attest": ["./ark/attest/api.ts"], + "@arktype/schema": ["./ark/schema/api.ts"], + "@arktype/schema/config": ["./ark/schema/config.ts"] }, "types": ["mocha", "node"] }, - "exclude": ["**/out", "**/node_modules", "./ark/docs", "./ark/repo"] + "exclude": ["**/out", "**/node_modules", "./ark/docs"] }