diff --git a/ark/attest/__tests__/unwrap.test.ts b/ark/attest/__tests__/unwrap.test.ts new file mode 100644 index 000000000..679056f54 --- /dev/null +++ b/ark/attest/__tests__/unwrap.test.ts @@ -0,0 +1,15 @@ +import { attest, contextualize } from "@ark/attest" + +contextualize(() => { + it("unwraps unversioned", () => { + attest(attest({ foo: "bar" }).unwrap()).equals({ + foo: "bar" + }) + }) + + it("unwraps serialized", () => { + attest( + attest({ foo: Symbol("unwrappedSymbol") }).unwrap({ serialize: true }) + ).snap({ foo: "Symbol(unwrappedSymbol)" }) + }) +}) diff --git a/ark/attest/assert/attest.ts b/ark/attest/assert/attest.ts index ab26572f0..3ddc699be 100644 --- a/ark/attest/assert/attest.ts +++ b/ark/attest/assert/attest.ts @@ -8,7 +8,11 @@ import { type VersionedTypeAssertion } from "../cache/getCachedAssertions.ts" import { getConfig, type AttestConfig } from "../config.ts" -import { assertEquals, typeEqualityMapping } from "./assertions.ts" +import { + assertEquals, + typeEqualityMapping, + type TypeAssertionMapping +} from "./assertions.ts" import { ChainableAssertions, type AssertionKind, @@ -29,8 +33,10 @@ export type AttestFn = { instantiations: (count?: Measure<"instantiations"> | undefined) => void } +export type VersionableActual = {} | null | undefined | TypeAssertionMapping + export type AssertionContext = { - actual: unknown + versionableActual: VersionableActual originalAssertedValue: unknown cfg: AttestConfig allowRegex: boolean @@ -54,7 +60,7 @@ export const attestInternal = ( const position = caller() const cfg = { ...getConfig(), ...cfgHooks } const ctx: AssertionContext = { - actual: value, + versionableActual: value, allowRegex: false, originalAssertedValue: value, position, diff --git a/ark/attest/assert/chainableAssertions.ts b/ark/attest/assert/chainableAssertions.ts index 1c9f1e0ad..20c4cf56c 100644 --- a/ark/attest/assert/chainableAssertions.ts +++ b/ark/attest/assert/chainableAssertions.ts @@ -22,7 +22,7 @@ import { getThrownMessage, throwAssertionError } from "./assertions.ts" -import type { AssertionContext } from "./attest.ts" +import type { AssertionContext, VersionableActual } from "./attest.ts" export type ChainableAssertionOptions = { allowRegex?: boolean @@ -31,6 +31,11 @@ export type ChainableAssertionOptions = { type AssertionRecord = Record, unknown> +export type UnwrapOptions = { + versionable?: boolean + serialize?: boolean +} + export class ChainableAssertions implements AssertionRecord { private ctx: AssertionContext @@ -38,22 +43,28 @@ export class ChainableAssertions implements AssertionRecord { this.ctx = ctx } - private serialize(value: unknown) { - return snapshot(value) - } - - private get unversionedActual() { - if (this.ctx.actual instanceof TypeAssertionMapping) { - return this.ctx.actual.fn( + private get unversionedActual(): unknown { + if (this.versionableActual instanceof TypeAssertionMapping) { + return this.versionableActual.fn( this.ctx.typeRelationshipAssertionEntries![0][1], this.ctx )!.actual } - return this.ctx.actual + return this.versionableActual + } + + private get versionableActual(): VersionableActual { + return this.ctx.versionableActual + } + + private get serializedActual(): unknown { + return snapshot(this.unversionedActual) } - private get serializedActual() { - return this.serialize(this.unversionedActual) + unwrap(opts?: UnwrapOptions): unknown { + const value = + opts?.versionable ? this.versionableActual : this.unversionedActual + return opts?.serialize ? snapshot(value) : value } private snapRequiresUpdate(expectedSerialized: unknown) { @@ -75,22 +86,25 @@ export class ChainableAssertions implements AssertionRecord { } equals(expected: unknown): this { - assertEquals(expected, this.ctx.actual, this.ctx) + assertEquals(expected, this.versionableActual, this.ctx) return this } satisfies(def: unknown): this { - assertSatisfies(type.raw(def), this.ctx.actual, this.ctx) + assertSatisfies(type.raw(def), this.versionableActual, this.ctx) return this } instanceOf(expected: Constructor): this { - if (!(this.ctx.actual instanceof expected)) { + if (!(this.versionableActual instanceof expected)) { throwAssertionError({ stack: this.ctx.assertionStack, message: `Expected an instance of ${expected.name} (was ${ - typeof this.ctx.actual === "object" && this.ctx.actual !== null ? - this.ctx.actual.constructor.name + ( + typeof this.versionableActual === "object" && + this.versionableActual !== null + ) ? + this.versionableActual.constructor.name : this.serializedActual })` }) @@ -102,7 +116,7 @@ export class ChainableAssertions implements AssertionRecord { // Use variadic args to distinguish undefined being passed explicitly from no args const inline = (...args: unknown[]) => { const snapName = this.ctx.lastSnapName ?? "snap" - const expectedSerialized = this.serialize(args[0]) + const expectedSerialized = snapshot(args[0]) if (!args.length || this.ctx.cfg.updateSnapshots) { if (this.snapRequiresUpdate(expectedSerialized)) { const snapshotArgs: SnapshotArgs = { @@ -157,8 +171,8 @@ export class ChainableAssertions implements AssertionRecord { } } if (this.ctx.allowRegex) - assertEqualOrMatching(expected, this.ctx.actual, this.ctx) - else assertEquals(expected, this.ctx.actual, this.ctx) + assertEqualOrMatching(expected, this.versionableActual, this.ctx) + else assertEquals(expected, this.versionableActual, this.ctx) return this } @@ -169,7 +183,7 @@ export class ChainableAssertions implements AssertionRecord { get throws(): unknown { const result = callAssertedFunction(this.unversionedActual as Function) - this.ctx.actual = getThrownMessage(result, this.ctx) + this.ctx.versionableActual = getThrownMessage(result, this.ctx) this.ctx.allowRegex = true this.ctx.defaultExpected = "" return this.immediateOrChained() @@ -198,7 +212,7 @@ export class ChainableAssertions implements AssertionRecord { get completions(): any { if (this.ctx.cfg.skipTypes) return chainableNoOpProxy - this.ctx.actual = new TypeAssertionMapping(data => { + this.ctx.versionableActual = new TypeAssertionMapping(data => { if (typeof data.completions === "string") { // if the completions were ambiguously defined, e.g. two string // literals with the same value, they are writen as an error @@ -218,14 +232,14 @@ export class ChainableAssertions implements AssertionRecord { const self = this return { get toString() { - self.ctx.actual = new TypeAssertionMapping(data => ({ + self.ctx.versionableActual = new TypeAssertionMapping(data => ({ actual: formatTypeString(data.args[0].type) })) self.ctx.allowRegex = true return self.immediateOrChained() }, get errors() { - self.ctx.actual = new TypeAssertionMapping(data => ({ + self.ctx.versionableActual = new TypeAssertionMapping(data => ({ actual: data.errors.join("\n") })) self.ctx.allowRegex = true @@ -311,6 +325,7 @@ export type comparableValueAssertion = { satisfies: (def: type.validate) => nextAssertions // This can be used to assert values without type constraints unknown: Omit, "unknown"> + unwrap: (opts?: UnwrapOptions) => unknown } export type TypeAssertionsRoot = { diff --git a/ark/attest/package.json b/ark/attest/package.json index 4126b864f..b43c669f9 100644 --- a/ark/attest/package.json +++ b/ark/attest/package.json @@ -1,6 +1,6 @@ { "name": "@ark/attest", - "version": "0.26.0", + "version": "0.27.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/fs/package.json b/ark/fs/package.json index 56b5ce60b..70b091117 100644 --- a/ark/fs/package.json +++ b/ark/fs/package.json @@ -1,6 +1,6 @@ { "name": "@ark/fs", - "version": "0.22.0", + "version": "0.23.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/repo/ts.js b/ark/repo/ts.js index 056823690..48d3b6a1a 100755 --- a/ark/repo/ts.js +++ b/ark/repo/ts.js @@ -13,7 +13,6 @@ const versionedFlags = ), "--import tsx") -execSync( - `node --conditions ark-ts ${versionedFlags} ${process.argv.slice(2).join(" ")}`, - { stdio: "inherit" } -) +process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ""} --conditions ark-ts ${versionedFlags}` + +execSync(`node ${process.argv.slice(2).join(" ")}`, { stdio: "inherit" }) diff --git a/ark/schema/package.json b/ark/schema/package.json index 2a6591afd..b2be760bf 100644 --- a/ark/schema/package.json +++ b/ark/schema/package.json @@ -1,6 +1,6 @@ { "name": "@ark/schema", - "version": "0.22.0", + "version": "0.23.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/schema/roots/root.ts b/ark/schema/roots/root.ts index 4426151ee..5511ac903 100644 --- a/ark/schema/roots/root.ts +++ b/ark/schema/roots/root.ts @@ -51,7 +51,10 @@ import { import { intersectNodesRoot, pipeNodesRoot } from "../shared/intersections.ts" import type { JsonSchema } from "../shared/jsonSchema.ts" import { $ark } from "../shared/registry.ts" -import type { StandardSchema } from "../shared/standardSchema.ts" +import type { + ArkTypeStandardSchemaProps, + StandardSchema +} from "../shared/standardSchema.ts" import { arkKind, hasArkKind } from "../shared/utils.ts" import { assertDefaultValueAssignability } from "../structure/optional.ts" import type { Prop } from "../structure/prop.ts" @@ -92,22 +95,18 @@ export abstract class BaseRoot< return this } - get "~standard"(): 1 { - return 1 - } - - get "~vendor"(): "arktype" { - return "arktype" - } - - "~validate"(input: StandardSchema.Input): StandardSchema.Result { - const out = this(input.value) - if (out instanceof ArkErrors) return out - return { value: out } + get "~standard"(): ArkTypeStandardSchemaProps { + return { + vendor: "arktype", + version: 1, + validate: input => { + const out = this(input) + if (out instanceof ArkErrors) return out + return { value: out } + } + } } - declare "~types": StandardSchema.Types - get optionalMeta(): boolean { return this.cacheGetter( "optionalMeta", diff --git a/ark/schema/shared/errors.ts b/ark/schema/shared/errors.ts index 16404a191..a8a0864d7 100644 --- a/ark/schema/shared/errors.ts +++ b/ark/schema/shared/errors.ts @@ -12,7 +12,7 @@ import { import type { ResolvedArkConfig } from "../config.ts" import type { Prerequisite, errorContext } from "../kinds.ts" import type { NodeKind } from "./implement.ts" -import type { StandardSchema } from "./standardSchema.ts" +import type { StandardFailureResult } from "./standardSchema.ts" import type { TraversalContext } from "./traversal.ts" import { arkKind } from "./utils.ts" @@ -84,7 +84,7 @@ export class ArkError< export class ArkErrors extends ReadonlyArray - implements StandardSchema.Failure + implements StandardFailureResult { protected ctx: TraversalContext diff --git a/ark/schema/shared/standardSchema.ts b/ark/schema/shared/standardSchema.ts index 1742dc4f7..706fb6f86 100644 --- a/ark/schema/shared/standardSchema.ts +++ b/ark/schema/shared/standardSchema.ts @@ -1,39 +1,109 @@ /** Subset of types from https://github.com/standard-schema/standard-schema */ -export interface StandardSchema extends StandardSchema.ConstantProps { - readonly "~types": StandardSchema.Types - "~validate": StandardSchema.Validator + +/** + * The Standard Schema interface. + */ +export interface StandardSchema { + /** + * The Standard Schema properties. + */ + readonly "~standard": StandardSchemaProps } -export declare namespace StandardSchema { - export interface ConstantProps { - readonly "~standard": 1 - readonly "~vendor": "arktype" - } +/** + * The Standard Schema properties interface. + */ +export interface StandardSchemaProps { + /** + * The version number of the standard. + */ + readonly version: 1 + /** + * The vendor name of the schema library. + */ + readonly vendor: string + /** + * Validates unknown input values. + */ + readonly validate: ( + value: unknown + ) => StandardResult | Promise> + /** + * Inferred types associated with the schema. + */ + readonly types?: StandardTypes | undefined +} - export interface Types { - input: In - output: Out - } +export interface ArkTypeStandardSchemaProps + extends StandardSchemaProps { + readonly vendor: "arktype" +} - export type Validator = (input: Input) => Result +/** + * The result interface of the validate function. + */ +type StandardResult = + | StandardSuccessResult + | StandardFailureResult - export interface Input { - value: unknown - } +/** + * The result interface if validation succeeds. + */ +interface StandardSuccessResult { + /** + * The typed output value. + */ + readonly value: Output + /** + * The non-existent issues. + */ + readonly issues?: undefined +} - export type Result = Success | Failure +/** + * The result interface if validation fails. + */ +export interface StandardFailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray +} - export interface Success { - value: Out - issues?: undefined - } +/** + * The issue interface of the failure output. + */ +export interface StandardIssue { + /** + * The error message of the issue. + */ + readonly message: string + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined +} - export interface Failure { - readonly issues: readonly Issue[] - } +/** + * The path segment interface of the issue. + */ +interface StandardPathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey +} - export interface Issue { - readonly message: string - readonly path: readonly PropertyKey[] - } +/** + * The base types interface of Standard Schema. + */ +interface StandardTypes { + /** + * The input type of the schema. + */ + readonly input: Input + /** + * The output type of the schema. + */ + readonly output: Output } diff --git a/ark/type/__tests__/standardSchema.test.ts b/ark/type/__tests__/standardSchema.test.ts index d1ed54edc..3863a91fa 100644 --- a/ark/type/__tests__/standardSchema.test.ts +++ b/ark/type/__tests__/standardSchema.test.ts @@ -7,14 +7,16 @@ contextualize(() => { it("validation conforms to spec", () => { const t = type({ foo: "string" }) const standard: v1.StandardSchema<{ foo: string }> = t - const standardOut = standard["~validate"]({ value: { foo: "bar" } }) - attest>>(standardOut).equals({ + const standardOut = standard["~standard"].validate({ + foo: "bar" + }) + attest>>(standardOut).equals({ value: { foo: "bar" } }) - const badStandardOut = standard["~validate"]({ - value: { foo: 5 } - }) as v1.StandardFailureOutput + const badStandardOut = standard["~standard"].validate({ + foo: 5 + }) as v1.StandardFailureResult attest(badStandardOut.issues).instanceOf(type.errors) attest(badStandardOut.issues.toString()).snap( diff --git a/ark/type/methods/base.ts b/ark/type/methods/base.ts index ae455c5de..ee641060b 100644 --- a/ark/type/methods/base.ts +++ b/ark/type/methods/base.ts @@ -1,5 +1,6 @@ import type { ArkErrors, + ArkTypeStandardSchemaProps, BaseRoot, Disjoint, JsonSchema, @@ -7,7 +8,6 @@ import type { Morph, Predicate, PredicateCast, - StandardSchema, UndeclaredKeyBehavior } from "@ark/schema" import type { @@ -45,7 +45,7 @@ import type { instantiateType } from "./instantiate.ts" interface Type extends Callable< (data: unknown) => distill.Out | ArkErrors, - StandardSchema.ConstantProps + ArkTypeStandardSchemaProps > { [inferred]: t @@ -285,13 +285,7 @@ interface Type ): instantiateType // Standard Schema Compatibility (https://github.com/standard-schema/standard-schema) - // Static properties are attached via Callable, so this is just those that depend on `t`. - // Important for type performance that we declare these directly as props rather than - // extend them so TS can be lazy about evaluating them (as is the case for props like `.inferIn`) - - "~validate": StandardSchema.Validator - - "~types": StandardSchema.Types + "~standard": ArkTypeStandardSchemaProps // deprecate Function methods so they are deprioritized as suggestions diff --git a/ark/type/package.json b/ark/type/package.json index 9903ca066..f7b3f1e79 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-rc.22", + "version": "2.0.0-rc.23", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/util/package.json b/ark/util/package.json index f61c27b38..ace5135ac 100644 --- a/ark/util/package.json +++ b/ark/util/package.json @@ -1,6 +1,6 @@ { "name": "@ark/util", - "version": "0.22.0", + "version": "0.23.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/util/registry.ts b/ark/util/registry.ts index 973bcbd59..4cc94cbec 100644 --- a/ark/util/registry.ts +++ b/ark/util/registry.ts @@ -7,7 +7,7 @@ import { FileConstructor, objectKindOf } from "./objectKinds.ts" // recent node versions (https://nodejs.org/api/esm.html#json-modules). // For now, we assert this matches the package.json version via a unit test. -export const arkUtilVersion = "0.22.0" +export const arkUtilVersion = "0.23.0" export const initialRegistryContents = { version: arkUtilVersion, diff --git a/package.json b/package.json index d520a9954..7b0f87fa6 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@ark/repo": "workspace:*", "@ark/util": "workspace:*", "@eslint/js": "9.12.0", - "@standard-schema/spec": "1.0.0-beta.1", + "@standard-schema/spec": "1.0.0-beta.3", "@types/mocha": "10.0.9", "@types/node": "22.7.5", "arktype": "workspace:*",