From de5bce880a529cf94d3c107479a208105db6694d Mon Sep 17 00:00:00 2001 From: David Blass Date: Wed, 18 Dec 2024 13:12:49 -0500 Subject: [PATCH] feat: tuple defaults, remove optional/default meta, remove inferred attributes (#1228) --- ark/attest/__tests__/satisfies.test.ts | 6 +- ark/attest/assert/chainableAssertions.ts | 30 +- ark/attest/package.json | 2 +- ark/dark/package.json | 11 +- ark/docs/package.json | 16 +- .../content/docs/intro/adding-constraints.mdx | 5 - ark/docs/src/content/docs/objects.mdx | 36 +- .../__tests__/arktypeFastCheck.test.ts | 12 +- ark/fast-check/package.json | 2 +- ark/fs/package.json | 2 +- ark/{docs => }/fuma/README.md | 0 ark/{docs => }/fuma/app/(home)/layout.tsx | 4 +- ark/{docs => }/fuma/app/(home)/page.tsx | 12 +- ark/{docs => }/fuma/app/api/search/route.ts | 2 +- .../fuma/app/docs-og/[...slug]/route.ts | 2 +- .../fuma/app/docs/[[...slug]]/page.tsx | 2 +- ark/{docs => }/fuma/app/docs/layout.tsx | 4 +- ark/{docs => }/fuma/app/global.css | 0 ark/{docs => }/fuma/app/layout.config.tsx | 2 +- ark/{docs => }/fuma/app/layout.tsx | 0 ark/{docs => }/fuma/components/ArkCard.tsx | 0 .../fuma/components/AutoplayDemo.tsx | 0 ark/{docs => }/fuma/components/CodeBlock.tsx | 2 +- .../fuma/components/FloatYourBoat.tsx | 2 +- ark/{docs => }/fuma/components/Hero.tsx | 2 +- ark/{docs => }/fuma/components/LinkCard.tsx | 0 .../fuma/components/PlatformCloud.tsx | 18 +- .../components/RuntimeBenchmarksGraph.tsx | 0 ark/{docs => }/fuma/components/SyntaxTabs.tsx | 0 .../fuma/components/icons/arktype-logo.tsx | 0 ark/{docs => }/fuma/components/icons/boat.tsx | 0 ark/{docs => }/fuma/components/icons/bun.tsx | 0 .../fuma/components/icons/chromium.tsx | 0 ark/{docs => }/fuma/components/icons/deno.tsx | 0 .../fuma/components/icons/intellij.tsx | 0 ark/{docs => }/fuma/components/icons/js.tsx | 0 .../fuma/components/icons/neovim.tsx | 0 ark/{docs => }/fuma/components/icons/node.tsx | 0 ark/{docs => }/fuma/components/icons/npm.tsx | 0 ark/{docs => }/fuma/components/icons/ts.tsx | 0 .../fuma/components/icons/vscode.tsx | 0 .../snippets/betterErrors.twoslash.ts | 0 .../snippets/clarityAndConcision.twoslash.js | 0 .../deepIntrospectability.twoslash.js | 0 .../intrinsicOptimization.twoslash.js | 0 .../snippets/unparalleledDx.twoslash.js | 0 ark/{docs => }/fuma/content/docs/about.mdx | 0 .../fuma/content/docs/configuration/index.mdx | 0 .../fuma/content/docs/configuration/meta.json | 0 .../fuma/content/docs/definitions.mdx | 0 .../fuma/content/docs/expressions/index.mdx | 0 .../fuma/content/docs/expressions/meta.json | 0 ark/{docs => }/fuma/content/docs/faq.mdx | 0 .../fuma/content/docs/generics/index.mdx | 0 .../fuma/content/docs/generics/meta.json | 0 .../fuma/content/docs/integrations/index.mdx | 0 .../fuma/content/docs/integrations/meta.json | 0 .../content/docs/intro/adding-constraints.mdx | 6 +- .../content/docs/intro/morphs-and-more.mdx | 0 .../fuma/content/docs/intro/setup.mdx | 0 .../content/docs/intro/your-first-type.mdx | 0 ark/{docs => }/fuma/content/docs/keywords.mdx | 0 ark/{docs => }/fuma/content/docs/meta.json | 0 .../fuma/content/docs/objects/index.mdx | 46 +- .../fuma/content/docs/objects/meta.json | 0 .../fuma/content/docs/primitives/index.mdx | 0 .../fuma/content/docs/primitives/meta.json | 0 .../content/docs/primitives/number/index.mdx | 0 .../content/docs/primitives/number/meta.json | 0 .../content/docs/primitives/string/index.mdx | 0 .../content/docs/primitives/string/meta.json | 0 .../fuma/content/docs/scopes/index.mdx | 0 .../fuma/content/docs/scopes/meta.json | 0 .../fuma/content/docs/types/index.mdx | 0 .../fuma/content/docs/types/meta.json | 0 ark/{docs => }/fuma/lib/ambient.d.ts | 0 ark/{docs => }/fuma/lib/metadata.ts | 2 +- ark/{docs => }/fuma/lib/shiki.ts | 0 ark/{docs => }/fuma/lib/source.ts | 0 ark/{docs => }/fuma/next-env.d.ts | 0 ark/{docs => }/fuma/next.config.ts | 0 ark/{docs => }/fuma/package.json | 9 +- ark/{docs => }/fuma/pnpm-lock.yaml | 0 ark/{docs => }/fuma/postcss.config.cjs | 0 ark/{docs => }/fuma/public/CNAME | 0 ark/{docs => }/fuma/public/image/check.svg | 0 ark/{docs => }/fuma/public/image/copy.svg | 0 .../fuma/public/image/errorSquiggle.svg | 0 ark/{docs => }/fuma/public/image/favicon.svg | 0 ark/{docs => }/fuma/public/image/logo.png | Bin .../fuma/public/image/logoTransparent.png | Bin .../fuma/public/image/openGraphBackground.png | Bin ark/{docs => }/fuma/public/image/splash.png | Bin ark/{docs => }/fuma/source.config.ts | 2 +- ark/{docs => }/fuma/tailwind.config.ts | 0 ark/{docs => }/fuma/tsconfig.json | 2 +- ark/repo/scratch.ts | 15 +- ark/schema/generic.ts | 4 +- ark/schema/node.ts | 9 +- ark/schema/package.json | 2 +- ark/schema/parse.ts | 20 +- ark/schema/roots/root.ts | 69 +- ark/schema/roots/union.ts | 4 +- ark/schema/scope.ts | 6 +- ark/schema/shared/declare.ts | 1 - ark/schema/shared/implement.ts | 20 +- ark/schema/shared/intersections.ts | 3 - ark/schema/shared/utils.ts | 6 +- ark/schema/structure/optional.ts | 165 ++-- ark/schema/structure/prop.ts | 10 +- ark/schema/structure/required.ts | 12 - ark/schema/structure/sequence.ts | 267 ++++-- ark/schema/structure/structure.ts | 19 +- ark/type/__tests__/arrays/array.test.ts | 2 +- ark/type/__tests__/arrays/defaults.test.ts | 95 ++ .../__tests__/arrays/variadicTuple.test.ts | 20 +- ark/type/__tests__/brand.test.ts | 78 +- ark/type/__tests__/cyclic.bench.ts | 8 +- ark/type/__tests__/declared.test.ts | 5 +- ark/type/__tests__/defaults.test.ts | 554 +++++------- ark/type/__tests__/generic.test.ts | 15 +- ark/type/__tests__/get.test.ts | 38 +- ark/type/__tests__/imports.test.ts | 2 +- ark/type/__tests__/instanceof.test.ts | 2 +- ark/type/__tests__/keywords/formData.test.ts | 2 +- ark/type/__tests__/keywords/json.test.ts | 7 +- ark/type/__tests__/keywords/object.test.ts | 4 +- ark/type/__tests__/keywords/url.test.ts | 4 +- ark/type/__tests__/narrow.test.ts | 31 +- ark/type/__tests__/object.bench.ts | 14 +- ark/type/__tests__/objects/mapped.test.ts | 10 +- ark/type/__tests__/objects/namedKeys.test.ts | 22 +- ark/type/__tests__/operand.bench.ts | 16 +- ark/type/__tests__/operator.bench.ts | 42 +- ark/type/__tests__/optional.test.ts | 29 + ark/type/__tests__/pipe.test.ts | 21 +- ark/type/__tests__/range.test.ts | 32 +- ark/type/__tests__/realWorld.test.ts | 59 +- ark/type/__tests__/scope.test.ts | 10 +- ark/type/__tests__/submodule.test.ts | 2 +- ark/type/__tests__/traverse.test.ts | 5 +- ark/type/attributes.ts | 638 ++++---------- ark/type/generic.ts | 11 +- ark/type/keywords/{constructors => }/Array.ts | 6 +- .../keywords/{constructors => }/FormData.ts | 6 +- .../keywords/{constructors => }/TypedArray.ts | 4 +- .../{constructors => }/constructors.ts | 4 +- ark/type/keywords/constructors/Date.ts | 97 --- ark/type/keywords/keywords.ts | 27 +- ark/type/keywords/number.ts | 69 ++ ark/type/keywords/number/epoch.ts | 34 - ark/type/keywords/number/integer.ts | 9 - ark/type/keywords/number/number.ts | 146 ---- ark/type/keywords/string.ts | 820 ++++++++++++++++++ ark/type/keywords/string/alpha.ts | 10 - ark/type/keywords/string/alphanumeric.ts | 13 - ark/type/keywords/string/base64.ts | 34 - ark/type/keywords/string/capitalize.ts | 31 - ark/type/keywords/string/creditCard.ts | 45 - ark/type/keywords/string/date.ts | 214 ----- ark/type/keywords/string/digits.ts | 10 - ark/type/keywords/string/email.ts | 14 - ark/type/keywords/string/integer.ts | 43 - ark/type/keywords/string/ip.ts | 51 -- ark/type/keywords/string/json.ts | 78 -- ark/type/keywords/string/lower.ts | 31 - ark/type/keywords/string/normalize.ts | 124 --- ark/type/keywords/string/numeric.ts | 35 - ark/type/keywords/string/semver.ts | 17 - ark/type/keywords/string/string.ts | 177 ---- ark/type/keywords/string/trim.ts | 35 - ark/type/keywords/string/upper.ts | 31 - ark/type/keywords/string/url.ts | 54 -- ark/type/keywords/string/utils.ts | 16 - ark/type/keywords/string/uuid.ts | 80 -- ark/type/methods/array.ts | 36 +- ark/type/methods/base.ts | 53 +- ark/type/methods/date.ts | 32 +- ark/type/methods/number.ts | 36 +- ark/type/methods/object.ts | 52 +- ark/type/methods/string.ts | 40 +- ark/type/package.json | 2 +- ark/type/parser/ast/generic.ts | 74 ++ ark/type/parser/ast/infer.ts | 138 +-- ark/type/parser/ast/validate.ts | 76 +- ark/type/parser/definition.ts | 110 ++- ark/type/parser/objectLiteral.ts | 356 +++++--- ark/type/parser/property.ts | 127 +++ ark/type/parser/reduce/dynamic.ts | 2 +- ark/type/parser/shift/operand/enclosed.ts | 6 +- ark/type/parser/shift/operand/unenclosed.ts | 3 +- ark/type/parser/shift/operator/default.ts | 8 +- ark/type/parser/string.ts | 36 +- ark/type/parser/tuple.ts | 589 ------------- ark/type/parser/tupleExpressions.ts | 273 ++++++ ark/type/parser/tupleLiteral.ts | 356 ++++++++ ark/type/scope.ts | 77 +- ark/type/type.ts | 13 +- ark/util/__tests__/traits.test.ts | 2 +- ark/util/arrays.ts | 15 +- ark/util/generics.ts | 4 +- ark/util/package.json | 2 +- ark/util/registry.ts | 2 +- ark/util/serialize.ts | 8 +- eslint.config.js | 9 +- package.json | 3 +- tsconfig.json | 2 +- 207 files changed, 3426 insertions(+), 4132 deletions(-) rename ark/{docs => }/fuma/README.md (100%) rename ark/{docs => }/fuma/app/(home)/layout.tsx (74%) rename ark/{docs => }/fuma/app/(home)/page.tsx (88%) rename ark/{docs => }/fuma/app/api/search/route.ts (79%) rename ark/{docs => }/fuma/app/docs-og/[...slug]/route.ts (97%) rename ark/{docs => }/fuma/app/docs/[[...slug]]/page.tsx (96%) rename ark/{docs => }/fuma/app/docs/layout.tsx (71%) rename ark/{docs => }/fuma/app/global.css (100%) rename ark/{docs => }/fuma/app/layout.config.tsx (93%) rename ark/{docs => }/fuma/app/layout.tsx (100%) rename ark/{docs => }/fuma/components/ArkCard.tsx (100%) rename ark/{docs => }/fuma/components/AutoplayDemo.tsx (100%) rename ark/{docs => }/fuma/components/CodeBlock.tsx (98%) rename ark/{docs => }/fuma/components/FloatYourBoat.tsx (96%) rename ark/{docs => }/fuma/components/Hero.tsx (95%) rename ark/{docs => }/fuma/components/LinkCard.tsx (100%) rename ark/{docs => }/fuma/components/PlatformCloud.tsx (78%) rename ark/{docs => }/fuma/components/RuntimeBenchmarksGraph.tsx (100%) rename ark/{docs => }/fuma/components/SyntaxTabs.tsx (100%) rename ark/{docs => }/fuma/components/icons/arktype-logo.tsx (100%) rename ark/{docs => }/fuma/components/icons/boat.tsx (100%) rename ark/{docs => }/fuma/components/icons/bun.tsx (100%) rename ark/{docs => }/fuma/components/icons/chromium.tsx (100%) rename ark/{docs => }/fuma/components/icons/deno.tsx (100%) rename ark/{docs => }/fuma/components/icons/intellij.tsx (100%) rename ark/{docs => }/fuma/components/icons/js.tsx (100%) rename ark/{docs => }/fuma/components/icons/neovim.tsx (100%) rename ark/{docs => }/fuma/components/icons/node.tsx (100%) rename ark/{docs => }/fuma/components/icons/npm.tsx (100%) rename ark/{docs => }/fuma/components/icons/ts.tsx (100%) rename ark/{docs => }/fuma/components/icons/vscode.tsx (100%) rename ark/{docs => }/fuma/components/snippets/betterErrors.twoslash.ts (100%) rename ark/{docs => }/fuma/components/snippets/clarityAndConcision.twoslash.js (100%) rename ark/{docs => }/fuma/components/snippets/deepIntrospectability.twoslash.js (100%) rename ark/{docs => }/fuma/components/snippets/intrinsicOptimization.twoslash.js (100%) rename ark/{docs => }/fuma/components/snippets/unparalleledDx.twoslash.js (100%) rename ark/{docs => }/fuma/content/docs/about.mdx (100%) rename ark/{docs => }/fuma/content/docs/configuration/index.mdx (100%) rename ark/{docs => }/fuma/content/docs/configuration/meta.json (100%) rename ark/{docs => }/fuma/content/docs/definitions.mdx (100%) rename ark/{docs => }/fuma/content/docs/expressions/index.mdx (100%) rename ark/{docs => }/fuma/content/docs/expressions/meta.json (100%) rename ark/{docs => }/fuma/content/docs/faq.mdx (100%) rename ark/{docs => }/fuma/content/docs/generics/index.mdx (100%) rename ark/{docs => }/fuma/content/docs/generics/meta.json (100%) rename ark/{docs => }/fuma/content/docs/integrations/index.mdx (100%) rename ark/{docs => }/fuma/content/docs/integrations/meta.json (100%) rename ark/{docs => }/fuma/content/docs/intro/adding-constraints.mdx (94%) rename ark/{docs => }/fuma/content/docs/intro/morphs-and-more.mdx (100%) rename ark/{docs => }/fuma/content/docs/intro/setup.mdx (100%) rename ark/{docs => }/fuma/content/docs/intro/your-first-type.mdx (100%) rename ark/{docs => }/fuma/content/docs/keywords.mdx (100%) rename ark/{docs => }/fuma/content/docs/meta.json (100%) rename ark/{docs => }/fuma/content/docs/objects/index.mdx (92%) rename ark/{docs => }/fuma/content/docs/objects/meta.json (100%) rename ark/{docs => }/fuma/content/docs/primitives/index.mdx (100%) rename ark/{docs => }/fuma/content/docs/primitives/meta.json (100%) rename ark/{docs => }/fuma/content/docs/primitives/number/index.mdx (100%) rename ark/{docs => }/fuma/content/docs/primitives/number/meta.json (100%) rename ark/{docs => }/fuma/content/docs/primitives/string/index.mdx (100%) rename ark/{docs => }/fuma/content/docs/primitives/string/meta.json (100%) rename ark/{docs => }/fuma/content/docs/scopes/index.mdx (100%) rename ark/{docs => }/fuma/content/docs/scopes/meta.json (100%) rename ark/{docs => }/fuma/content/docs/types/index.mdx (100%) rename ark/{docs => }/fuma/content/docs/types/meta.json (100%) rename ark/{docs => }/fuma/lib/ambient.d.ts (100%) rename ark/{docs => }/fuma/lib/metadata.ts (79%) rename ark/{docs => }/fuma/lib/shiki.ts (100%) rename ark/{docs => }/fuma/lib/source.ts (100%) rename ark/{docs => }/fuma/next-env.d.ts (100%) rename ark/{docs => }/fuma/next.config.ts (100%) rename ark/{docs => }/fuma/package.json (83%) rename ark/{docs => }/fuma/pnpm-lock.yaml (100%) rename ark/{docs => }/fuma/postcss.config.cjs (100%) rename ark/{docs => }/fuma/public/CNAME (100%) rename ark/{docs => }/fuma/public/image/check.svg (100%) rename ark/{docs => }/fuma/public/image/copy.svg (100%) rename ark/{docs => }/fuma/public/image/errorSquiggle.svg (100%) rename ark/{docs => }/fuma/public/image/favicon.svg (100%) rename ark/{docs => }/fuma/public/image/logo.png (100%) rename ark/{docs => }/fuma/public/image/logoTransparent.png (100%) rename ark/{docs => }/fuma/public/image/openGraphBackground.png (100%) rename ark/{docs => }/fuma/public/image/splash.png (100%) rename ark/{docs => }/fuma/source.config.ts (82%) rename ark/{docs => }/fuma/tailwind.config.ts (100%) rename ark/{docs => }/fuma/tsconfig.json (94%) create mode 100644 ark/type/__tests__/arrays/defaults.test.ts create mode 100644 ark/type/__tests__/optional.test.ts rename ark/type/keywords/{constructors => }/Array.ts (88%) rename ark/type/keywords/{constructors => }/FormData.ts (91%) rename ark/type/keywords/{constructors => }/TypedArray.ts (91%) rename ark/type/keywords/{constructors => }/constructors.ts (93%) delete mode 100644 ark/type/keywords/constructors/Date.ts create mode 100644 ark/type/keywords/number.ts delete mode 100644 ark/type/keywords/number/epoch.ts delete mode 100644 ark/type/keywords/number/integer.ts delete mode 100644 ark/type/keywords/number/number.ts create mode 100644 ark/type/keywords/string.ts delete mode 100644 ark/type/keywords/string/alpha.ts delete mode 100644 ark/type/keywords/string/alphanumeric.ts delete mode 100644 ark/type/keywords/string/base64.ts delete mode 100644 ark/type/keywords/string/capitalize.ts delete mode 100644 ark/type/keywords/string/creditCard.ts delete mode 100644 ark/type/keywords/string/date.ts delete mode 100644 ark/type/keywords/string/digits.ts delete mode 100644 ark/type/keywords/string/email.ts delete mode 100644 ark/type/keywords/string/integer.ts delete mode 100644 ark/type/keywords/string/ip.ts delete mode 100644 ark/type/keywords/string/json.ts delete mode 100644 ark/type/keywords/string/lower.ts delete mode 100644 ark/type/keywords/string/normalize.ts delete mode 100644 ark/type/keywords/string/numeric.ts delete mode 100644 ark/type/keywords/string/semver.ts delete mode 100644 ark/type/keywords/string/string.ts delete mode 100644 ark/type/keywords/string/trim.ts delete mode 100644 ark/type/keywords/string/upper.ts delete mode 100644 ark/type/keywords/string/url.ts delete mode 100644 ark/type/keywords/string/utils.ts delete mode 100644 ark/type/keywords/string/uuid.ts create mode 100644 ark/type/parser/ast/generic.ts create mode 100644 ark/type/parser/property.ts delete mode 100644 ark/type/parser/tuple.ts create mode 100644 ark/type/parser/tupleExpressions.ts create mode 100644 ark/type/parser/tupleLiteral.ts diff --git a/ark/attest/__tests__/satisfies.test.ts b/ark/attest/__tests__/satisfies.test.ts index 50618a32e0..6f4bc7d54f 100644 --- a/ark/attest/__tests__/satisfies.test.ts +++ b/ark/attest/__tests__/satisfies.test.ts @@ -1,11 +1,15 @@ import { attest, contextualize } from "@ark/attest" +import { nonOverlappingSatisfiesMessage } from "@ark/attest/internal/assert/chainableAssertions.js" contextualize(() => { it("can assert types", () => { attest({ foo: "bar" }).satisfies({ foo: "string" }) attest(() => { + // @ts-expect-error attest({ foo: "bar" }).satisfies({ foo: "number" }) - }).throws("foo must be a number (was a string)") + }) + .throws("foo must be a number (was a string)") + .type.errors(nonOverlappingSatisfiesMessage) }) }) diff --git a/ark/attest/assert/chainableAssertions.ts b/ark/attest/assert/chainableAssertions.ts index 0f6e0a0516..711b7e68e9 100644 --- a/ark/attest/assert/chainableAssertions.ts +++ b/ark/attest/assert/chainableAssertions.ts @@ -1,5 +1,11 @@ import { caller } from "@ark/fs" -import { printable, snapshot, type Constructor } from "@ark/util" +import { + printable, + snapshot, + type Constructor, + type ErrorType, + type isDisjoint +} from "@ark/util" import prettier from "@prettier/sync" import { type } from "arktype" import * as assert from "node:assert/strict" @@ -322,13 +328,33 @@ type snapProperty = { export type Unwrapper = (opts?: UnwrapOptions) => expected +export const nonOverlappingSatisfiesMessage = + "The type of your actual value and expected satisfies constraint have no overlap" + +export type nonOverlappingSatisfiesMessage = + typeof nonOverlappingSatisfiesMessage + +type validateExpectedOverlaps = + isDisjoint extends true ? + ErrorType< + nonOverlappingSatisfiesMessage, + { + actual: expected + satisfies: satisfies + } + > + : unknown + export type comparableValueAssertion = { snap: snapProperty equals: (value: expected) => nextAssertions instanceOf: (constructor: Constructor) => nextAssertions is: (value: expected) => nextAssertions completions: CompletionsSnap - satisfies: (def: type.validate) => nextAssertions + satisfies: ( + def: type.validate & + validateExpectedOverlaps> + ) => nextAssertions // This can be used to assert values without type constraints unknown: Omit, "unknown"> unwrap: Unwrapper diff --git a/ark/attest/package.json b/ark/attest/package.json index 9b9eb21e48..934a1852cc 100644 --- a/ark/attest/package.json +++ b/ark/attest/package.json @@ -1,6 +1,6 @@ { "name": "@ark/attest", - "version": "0.30.0", + "version": "0.31.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/dark/package.json b/ark/dark/package.json index 840a3eb616..39df47823c 100644 --- a/ark/dark/package.json +++ b/ark/dark/package.json @@ -2,12 +2,19 @@ "name": "arkdark", "displayName": "ArkDark", "description": "Syntax highlighting, inline errors and theme for ArkType⛵", - "version": "5.12.2", + "version": "5.13.0", "publisher": "arktypeio", "type": "module", "license": "MIT", "scripts": { - "publishExtension": "pnpx vsce publish" + "publishExtension": "pnpm packageExtension && pnpm publishVsce && pnpm publishOvsx", + "packageExtension": "vsce package --out arkdark.vsix", + "publishVsce": "vsce publish -i arkdark.vsix", + "publishOvsx": "ovsx publish -i arkdark.vsix" + }, + "devDependencies": { + "vsce": "2.15.0", + "ovsx": "0.10.1" }, "files": [ "*.json" diff --git a/ark/docs/package.json b/ark/docs/package.json index a63400c228..77f5fd5492 100644 --- a/ark/docs/package.json +++ b/ark/docs/package.json @@ -16,24 +16,24 @@ "@ark/util": "workspace:*", "@astrojs/check": "0.9.4", "@astrojs/react": "3.6.2", - "@astrojs/starlight": "0.28.3", - "@astrojs/ts-plugin": "1.10.3", - "@shikijs/transformers": "1.22.0", - "@shikijs/twoslash": "1.22.0", + "@astrojs/starlight": "0.29.0", + "@astrojs/ts-plugin": "1.10.4", + "@shikijs/transformers": "1.22.2", + "@shikijs/twoslash": "1.22.2", "arkdark": "workspace:*", "arktype": "workspace:*", - "astro": "4.16.6", + "astro": "4.16.17", "astro-og-canvas": "0.5.4", "canvaskit-wasm": "0.39.1", - "framer-motion": "11.11.9", + "framer-motion": "11.11.17", "react": "18.3.1", "react-dom": "18.3.1", "sharp": "0.33.5", - "shiki": "1.22.0", + "shiki": "1.22.2", "twoslash": "0.2.12" }, "devDependencies": { - "@types/react": "18.3.11", + "@types/react": "18.3.12", "@types/react-dom": "18.3.1", "typescript": "catalog:" } diff --git a/ark/docs/src/content/docs/intro/adding-constraints.mdx b/ark/docs/src/content/docs/intro/adding-constraints.mdx index 46de939c4c..3ff357a7df 100644 --- a/ark/docs/src/content/docs/intro/adding-constraints.mdx +++ b/ark/docs/src/content/docs/intro/adding-constraints.mdx @@ -17,7 +17,6 @@ In other words, **they just work**. Let's create a new `contact` Type that enforces our example constraints. ```ts -// hover to see the type-level representation const contact = type({ // many common constraints are available as built-in keywords email: "string.email", @@ -27,10 +26,6 @@ const contact = type({ // if you need the TS type, just infer it out as normal type Contact = typeof contact.infer -// ---cut-start--- -// this empty line prevents the source syntax highlighting from breaking - -// ---cut-end--- ``` ## Compose diff --git a/ark/docs/src/content/docs/objects.mdx b/ark/docs/src/content/docs/objects.mdx index 90b2f41953..b90fbd2dc6 100644 --- a/ark/docs/src/content/docs/objects.mdx +++ b/ark/docs/src/content/docs/objects.mdx @@ -159,47 +159,25 @@ const myObject = type({ const myObject = type({ defaultableKey: ["boolean", "=", false] }) -``` - - - - - -```ts -const myObject = type({ - defaultableKey: type("boolean", "=", false) -}) ``` -:::caution[Optional and default only work within objects!] -Adding a `optional` or `default` to a `Type` doesn't alter its standalone behavior. - -Rather, it adds metadata that changes how it works when referenced from an object or tuple. +:::caution[Optional and default only work within objects and tuples!] +Unlike e.g. `number.array()`, `number.optional()` and `number.default(0)` don't return a new `Type`, but rather a tuple definition like `[Type, "?"]` or `[Type, "=", 0]`. -
- See an example +This reflects the fact that in ArkType's type system, optionality and defaultability are only meaningful in reference to a property. Attempting to create an optional or defaultable value outside an object like `type("string?")` will result in a `ParseError`. -```ts -const optionalString = type.string.optional() +To create a `Type` accepting `string` or `undefined`, use a union like `type("string | undefined")`. -optionalString.allows(undefined) // false - -const objectWithOptionalKey = type({ - foo: optionalString -}) +To have it transform `undefined` to an empty string, use an explicit morph like: -objectWithOptionalKey.allows({}) // true +```ts +const fallbackString = type("string | undefined").pipe(v => v ?? "") ``` -
- -Prefer the key-embedded syntax (`"optionalKey?":`) where possible. -::: - ##### index diff --git a/ark/fast-check/__tests__/arktypeFastCheck.test.ts b/ark/fast-check/__tests__/arktypeFastCheck.test.ts index 457d784070..d3f6ac62a1 100644 --- a/ark/fast-check/__tests__/arktypeFastCheck.test.ts +++ b/ark/fast-check/__tests__/arktypeFastCheck.test.ts @@ -1,10 +1,10 @@ -import { attest } from "@ark/attest" +import { attest, contextualize } from "@ark/attest" import { arkToArbitrary } from "@ark/fast-check/internal/arktypeFastCheck.ts" import { scope, type } from "arktype" import { type Arbitrary, assert, property } from "fast-check" import { describe } from "mocha" -describe("Arbitrary Generation", () => { +contextualize(() => { describe("union", () => { it("boolean", () => { const t = type("boolean") @@ -17,6 +17,7 @@ describe("Arbitrary Generation", () => { assertProperty(arbitrary, t) }) }) + describe("number", () => { it("number", () => { const t = type("number") @@ -66,6 +67,7 @@ describe("Arbitrary Generation", () => { ) }) }) + describe("string", () => { it("string", () => { const t = type("string") @@ -98,6 +100,7 @@ describe("Arbitrary Generation", () => { attest(() => arkToArbitrary(t)).throws("Bounded regex is not supported.") }) }) + describe("misc", () => { it("unknown", () => { const t = type("unknown") @@ -178,6 +181,7 @@ describe("Arbitrary Generation", () => { assertProperty(arbitrary, t) }) }) + describe("tuple", () => { it("empty tuple", () => { const t = type([]) @@ -216,6 +220,7 @@ describe("Arbitrary Generation", () => { assertProperty(arbitrary, t) }) }) + describe("object", () => { it("{}", () => { const t = type({}) @@ -263,7 +268,7 @@ describe("Arbitrary Generation", () => { }) it("multiple index signatures", () => { const t = type({ - "[string?]": "number|string", + "[string]": "number|string", "[symbol]": "string" }) const arbitrary = arkToArbitrary(t) @@ -314,6 +319,7 @@ describe("Arbitrary Generation", () => { assertProperty(arbitrary, t) }) }) + describe("proto", () => { it("Set", () => { const t = type("Set") diff --git a/ark/fast-check/package.json b/ark/fast-check/package.json index eea3b53c5d..8d5a204159 100644 --- a/ark/fast-check/package.json +++ b/ark/fast-check/package.json @@ -1,6 +1,6 @@ { "name": "@ark/fast-check", - "version": "0.0.2", + "version": "0.0.3", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/fs/package.json b/ark/fs/package.json index c1a76bec47..fed2fc30b9 100644 --- a/ark/fs/package.json +++ b/ark/fs/package.json @@ -1,6 +1,6 @@ { "name": "@ark/fs", - "version": "0.26.0", + "version": "0.27.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/docs/fuma/README.md b/ark/fuma/README.md similarity index 100% rename from ark/docs/fuma/README.md rename to ark/fuma/README.md diff --git a/ark/docs/fuma/app/(home)/layout.tsx b/ark/fuma/app/(home)/layout.tsx similarity index 74% rename from ark/docs/fuma/app/(home)/layout.tsx rename to ark/fuma/app/(home)/layout.tsx index 60af62044c..673b9b57d2 100644 --- a/ark/docs/fuma/app/(home)/layout.tsx +++ b/ark/fuma/app/(home)/layout.tsx @@ -1,7 +1,7 @@ import { HomeLayout } from "fumadocs-ui/layouts/home" import type { ReactNode } from "react" -import { FloatYourBoat } from "../../components/FloatYourBoat.jsx" -import { baseOptions } from "../layout.config.jsx" +import { FloatYourBoat } from "../../components/FloatYourBoat.tsx" +import { baseOptions } from "../layout.config.tsx" export type LayoutProps = { children: ReactNode diff --git a/ark/docs/fuma/app/(home)/page.tsx b/ark/fuma/app/(home)/page.tsx similarity index 88% rename from ark/docs/fuma/app/(home)/page.tsx rename to ark/fuma/app/(home)/page.tsx index 402c2f1952..bb9a56b7d9 100644 --- a/ark/docs/fuma/app/(home)/page.tsx +++ b/ark/fuma/app/(home)/page.tsx @@ -1,9 +1,9 @@ -import { ArkCard, ArkCards } from "../../components/ArkCard" -import { CodeBlock } from "../../components/CodeBlock" -import { Hero } from "../../components/Hero" -import { TsIcon } from "../../components/icons/ts" -import { LinkCard } from "../../components/LinkCard" -import { RuntimeBenchmarksGraph } from "../../components/RuntimeBenchmarksGraph" +import { ArkCard, ArkCards } from "../../components/ArkCard.tsx" +import { CodeBlock } from "../../components/CodeBlock.tsx" +import { Hero } from "../../components/Hero.tsx" +import { TsIcon } from "../../components/icons/ts.tsx" +import { LinkCard } from "../../components/LinkCard.tsx" +import { RuntimeBenchmarksGraph } from "../../components/RuntimeBenchmarksGraph.tsx" import { LightbulbIcon, diff --git a/ark/docs/fuma/app/api/search/route.ts b/ark/fuma/app/api/search/route.ts similarity index 79% rename from ark/docs/fuma/app/api/search/route.ts rename to ark/fuma/app/api/search/route.ts index cb15ea2c89..3d5c2e7a04 100644 --- a/ark/docs/fuma/app/api/search/route.ts +++ b/ark/fuma/app/api/search/route.ts @@ -1,5 +1,5 @@ import { createFromSource } from "fumadocs-core/search/server" -import { source } from "../../../lib/source.js" +import { source } from "../../../lib/source.ts" // it should be cached forever export const revalidate = false export const { staticGET: GET } = createFromSource(source) diff --git a/ark/docs/fuma/app/docs-og/[...slug]/route.ts b/ark/fuma/app/docs-og/[...slug]/route.ts similarity index 97% rename from ark/docs/fuma/app/docs-og/[...slug]/route.ts rename to ark/fuma/app/docs-og/[...slug]/route.ts index bfdcebe641..b913fc078a 100644 --- a/ark/docs/fuma/app/docs-og/[...slug]/route.ts +++ b/ark/fuma/app/docs-og/[...slug]/route.ts @@ -1,4 +1,4 @@ -import { metadataImage } from "../../../lib/metadata.js" +import { metadataImage } from "../../../lib/metadata.ts" // import { fromPackageRoot } from "@ark/fs" import { generateOGImage } from "fumadocs-ui/og" diff --git a/ark/docs/fuma/app/docs/[[...slug]]/page.tsx b/ark/fuma/app/docs/[[...slug]]/page.tsx similarity index 96% rename from ark/docs/fuma/app/docs/[[...slug]]/page.tsx rename to ark/fuma/app/docs/[[...slug]]/page.tsx index 1fc180bc68..4aa3a11ccf 100644 --- a/ark/docs/fuma/app/docs/[[...slug]]/page.tsx +++ b/ark/fuma/app/docs/[[...slug]]/page.tsx @@ -8,7 +8,7 @@ import { DocsTitle } from "fumadocs-ui/page" import { notFound } from "next/navigation" -import { source } from "../../../lib/source.js" +import { source } from "../../../lib/source.ts" export default async (props: { params: Promise<{ slug?: string[] }> }) => { const params = await props.params diff --git a/ark/docs/fuma/app/docs/layout.tsx b/ark/fuma/app/docs/layout.tsx similarity index 71% rename from ark/docs/fuma/app/docs/layout.tsx rename to ark/fuma/app/docs/layout.tsx index f6b2330fc2..ecdd398f7a 100644 --- a/ark/docs/fuma/app/docs/layout.tsx +++ b/ark/fuma/app/docs/layout.tsx @@ -1,7 +1,7 @@ import { DocsLayout } from "fumadocs-ui/layouts/docs" import type { ReactNode } from "react" -import { source } from "../../lib/source.js" -import { baseOptions } from "../layout.config.jsx" +import { source } from "../../lib/source.ts" +import { baseOptions } from "../layout.config.tsx" export default ({ children }: { children: ReactNode }) => ( diff --git a/ark/docs/fuma/app/global.css b/ark/fuma/app/global.css similarity index 100% rename from ark/docs/fuma/app/global.css rename to ark/fuma/app/global.css diff --git a/ark/docs/fuma/app/layout.config.tsx b/ark/fuma/app/layout.config.tsx similarity index 93% rename from ark/docs/fuma/app/layout.config.tsx rename to ark/fuma/app/layout.config.tsx index be2b754368..af491e3186 100644 --- a/ark/docs/fuma/app/layout.config.tsx +++ b/ark/fuma/app/layout.config.tsx @@ -5,7 +5,7 @@ import { SiX } from "@icons-pack/react-simple-icons" import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared" -import { ArkTypeLogo } from "../components/icons/arktype-logo" +import { ArkTypeLogo } from "../components/icons/arktype-logo.tsx" /** * Shared layout configurations diff --git a/ark/docs/fuma/app/layout.tsx b/ark/fuma/app/layout.tsx similarity index 100% rename from ark/docs/fuma/app/layout.tsx rename to ark/fuma/app/layout.tsx diff --git a/ark/docs/fuma/components/ArkCard.tsx b/ark/fuma/components/ArkCard.tsx similarity index 100% rename from ark/docs/fuma/components/ArkCard.tsx rename to ark/fuma/components/ArkCard.tsx diff --git a/ark/docs/fuma/components/AutoplayDemo.tsx b/ark/fuma/components/AutoplayDemo.tsx similarity index 100% rename from ark/docs/fuma/components/AutoplayDemo.tsx rename to ark/fuma/components/AutoplayDemo.tsx diff --git a/ark/docs/fuma/components/CodeBlock.tsx b/ark/fuma/components/CodeBlock.tsx similarity index 98% rename from ark/docs/fuma/components/CodeBlock.tsx rename to ark/fuma/components/CodeBlock.tsx index bbbb3bbdd4..6b2a862c4d 100644 --- a/ark/docs/fuma/components/CodeBlock.tsx +++ b/ark/fuma/components/CodeBlock.tsx @@ -15,7 +15,7 @@ import { Pre } from "fumadocs-ui/components/codeblock" import { getSingletonHighlighter } from "shiki" -import { shikiConfig } from "../lib/shiki.js" +import { shikiConfig } from "../lib/shiki.ts" const snippetContentsById = { betterErrors, diff --git a/ark/docs/fuma/components/FloatYourBoat.tsx b/ark/fuma/components/FloatYourBoat.tsx similarity index 96% rename from ark/docs/fuma/components/FloatYourBoat.tsx rename to ark/fuma/components/FloatYourBoat.tsx index 0de2ee090f..28729d4abf 100644 --- a/ark/docs/fuma/components/FloatYourBoat.tsx +++ b/ark/fuma/components/FloatYourBoat.tsx @@ -2,7 +2,7 @@ import { motion } from "framer-motion" import React from "react" -import { BoatIcon } from "./icons/boat.jsx" +import { BoatIcon } from "./icons/boat.tsx" const BOB_HEIGHT_PX = 2 const BOB_WIDTH_PX = 16 diff --git a/ark/docs/fuma/components/Hero.tsx b/ark/fuma/components/Hero.tsx similarity index 95% rename from ark/docs/fuma/components/Hero.tsx rename to ark/fuma/components/Hero.tsx index 6f86dbd35a..9c667409c6 100644 --- a/ark/docs/fuma/components/Hero.tsx +++ b/ark/fuma/components/Hero.tsx @@ -1,6 +1,6 @@ import { ArrowRightIcon } from "lucide-react" import Link from "next/link" -import { PlatformCloud } from "./PlatformCloud.jsx" +import { PlatformCloud } from "./PlatformCloud.tsx" export const Hero = () => (
diff --git a/ark/docs/fuma/components/LinkCard.tsx b/ark/fuma/components/LinkCard.tsx similarity index 100% rename from ark/docs/fuma/components/LinkCard.tsx rename to ark/fuma/components/LinkCard.tsx diff --git a/ark/docs/fuma/components/PlatformCloud.tsx b/ark/fuma/components/PlatformCloud.tsx similarity index 78% rename from ark/docs/fuma/components/PlatformCloud.tsx rename to ark/fuma/components/PlatformCloud.tsx index 9982dd009f..88a6bc3b29 100644 --- a/ark/docs/fuma/components/PlatformCloud.tsx +++ b/ark/fuma/components/PlatformCloud.tsx @@ -1,15 +1,15 @@ "use client" import { motion } from "framer-motion" -import { BunIcon } from "./icons/bun.jsx" -import { ChromiumIcon } from "./icons/chromium.jsx" -import { DenoIcon } from "./icons/deno.jsx" -import { IntellijIcon } from "./icons/intellij.jsx" -import { JsIcon } from "./icons/js.jsx" -import { NeovimIcon } from "./icons/neovim.jsx" -import { NodeIcon } from "./icons/node.jsx" -import { TsIcon } from "./icons/ts.jsx" -import { VscodeIcon } from "./icons/vscode.jsx" +import { BunIcon } from "./icons/bun.tsx" +import { ChromiumIcon } from "./icons/chromium.tsx" +import { DenoIcon } from "./icons/deno.tsx" +import { IntellijIcon } from "./icons/intellij.tsx" +import { JsIcon } from "./icons/js.tsx" +import { NeovimIcon } from "./icons/neovim.tsx" +import { NodeIcon } from "./icons/node.tsx" +import { TsIcon } from "./icons/ts.tsx" +import { VscodeIcon } from "./icons/vscode.tsx" export type SvgLogoProps = { name: PlatformName diff --git a/ark/docs/fuma/components/RuntimeBenchmarksGraph.tsx b/ark/fuma/components/RuntimeBenchmarksGraph.tsx similarity index 100% rename from ark/docs/fuma/components/RuntimeBenchmarksGraph.tsx rename to ark/fuma/components/RuntimeBenchmarksGraph.tsx diff --git a/ark/docs/fuma/components/SyntaxTabs.tsx b/ark/fuma/components/SyntaxTabs.tsx similarity index 100% rename from ark/docs/fuma/components/SyntaxTabs.tsx rename to ark/fuma/components/SyntaxTabs.tsx diff --git a/ark/docs/fuma/components/icons/arktype-logo.tsx b/ark/fuma/components/icons/arktype-logo.tsx similarity index 100% rename from ark/docs/fuma/components/icons/arktype-logo.tsx rename to ark/fuma/components/icons/arktype-logo.tsx diff --git a/ark/docs/fuma/components/icons/boat.tsx b/ark/fuma/components/icons/boat.tsx similarity index 100% rename from ark/docs/fuma/components/icons/boat.tsx rename to ark/fuma/components/icons/boat.tsx diff --git a/ark/docs/fuma/components/icons/bun.tsx b/ark/fuma/components/icons/bun.tsx similarity index 100% rename from ark/docs/fuma/components/icons/bun.tsx rename to ark/fuma/components/icons/bun.tsx diff --git a/ark/docs/fuma/components/icons/chromium.tsx b/ark/fuma/components/icons/chromium.tsx similarity index 100% rename from ark/docs/fuma/components/icons/chromium.tsx rename to ark/fuma/components/icons/chromium.tsx diff --git a/ark/docs/fuma/components/icons/deno.tsx b/ark/fuma/components/icons/deno.tsx similarity index 100% rename from ark/docs/fuma/components/icons/deno.tsx rename to ark/fuma/components/icons/deno.tsx diff --git a/ark/docs/fuma/components/icons/intellij.tsx b/ark/fuma/components/icons/intellij.tsx similarity index 100% rename from ark/docs/fuma/components/icons/intellij.tsx rename to ark/fuma/components/icons/intellij.tsx diff --git a/ark/docs/fuma/components/icons/js.tsx b/ark/fuma/components/icons/js.tsx similarity index 100% rename from ark/docs/fuma/components/icons/js.tsx rename to ark/fuma/components/icons/js.tsx diff --git a/ark/docs/fuma/components/icons/neovim.tsx b/ark/fuma/components/icons/neovim.tsx similarity index 100% rename from ark/docs/fuma/components/icons/neovim.tsx rename to ark/fuma/components/icons/neovim.tsx diff --git a/ark/docs/fuma/components/icons/node.tsx b/ark/fuma/components/icons/node.tsx similarity index 100% rename from ark/docs/fuma/components/icons/node.tsx rename to ark/fuma/components/icons/node.tsx diff --git a/ark/docs/fuma/components/icons/npm.tsx b/ark/fuma/components/icons/npm.tsx similarity index 100% rename from ark/docs/fuma/components/icons/npm.tsx rename to ark/fuma/components/icons/npm.tsx diff --git a/ark/docs/fuma/components/icons/ts.tsx b/ark/fuma/components/icons/ts.tsx similarity index 100% rename from ark/docs/fuma/components/icons/ts.tsx rename to ark/fuma/components/icons/ts.tsx diff --git a/ark/docs/fuma/components/icons/vscode.tsx b/ark/fuma/components/icons/vscode.tsx similarity index 100% rename from ark/docs/fuma/components/icons/vscode.tsx rename to ark/fuma/components/icons/vscode.tsx diff --git a/ark/docs/fuma/components/snippets/betterErrors.twoslash.ts b/ark/fuma/components/snippets/betterErrors.twoslash.ts similarity index 100% rename from ark/docs/fuma/components/snippets/betterErrors.twoslash.ts rename to ark/fuma/components/snippets/betterErrors.twoslash.ts diff --git a/ark/docs/fuma/components/snippets/clarityAndConcision.twoslash.js b/ark/fuma/components/snippets/clarityAndConcision.twoslash.js similarity index 100% rename from ark/docs/fuma/components/snippets/clarityAndConcision.twoslash.js rename to ark/fuma/components/snippets/clarityAndConcision.twoslash.js diff --git a/ark/docs/fuma/components/snippets/deepIntrospectability.twoslash.js b/ark/fuma/components/snippets/deepIntrospectability.twoslash.js similarity index 100% rename from ark/docs/fuma/components/snippets/deepIntrospectability.twoslash.js rename to ark/fuma/components/snippets/deepIntrospectability.twoslash.js diff --git a/ark/docs/fuma/components/snippets/intrinsicOptimization.twoslash.js b/ark/fuma/components/snippets/intrinsicOptimization.twoslash.js similarity index 100% rename from ark/docs/fuma/components/snippets/intrinsicOptimization.twoslash.js rename to ark/fuma/components/snippets/intrinsicOptimization.twoslash.js diff --git a/ark/docs/fuma/components/snippets/unparalleledDx.twoslash.js b/ark/fuma/components/snippets/unparalleledDx.twoslash.js similarity index 100% rename from ark/docs/fuma/components/snippets/unparalleledDx.twoslash.js rename to ark/fuma/components/snippets/unparalleledDx.twoslash.js diff --git a/ark/docs/fuma/content/docs/about.mdx b/ark/fuma/content/docs/about.mdx similarity index 100% rename from ark/docs/fuma/content/docs/about.mdx rename to ark/fuma/content/docs/about.mdx diff --git a/ark/docs/fuma/content/docs/configuration/index.mdx b/ark/fuma/content/docs/configuration/index.mdx similarity index 100% rename from ark/docs/fuma/content/docs/configuration/index.mdx rename to ark/fuma/content/docs/configuration/index.mdx diff --git a/ark/docs/fuma/content/docs/configuration/meta.json b/ark/fuma/content/docs/configuration/meta.json similarity index 100% rename from ark/docs/fuma/content/docs/configuration/meta.json rename to ark/fuma/content/docs/configuration/meta.json diff --git a/ark/docs/fuma/content/docs/definitions.mdx b/ark/fuma/content/docs/definitions.mdx similarity index 100% rename from ark/docs/fuma/content/docs/definitions.mdx rename to ark/fuma/content/docs/definitions.mdx diff --git a/ark/docs/fuma/content/docs/expressions/index.mdx b/ark/fuma/content/docs/expressions/index.mdx similarity index 100% rename from ark/docs/fuma/content/docs/expressions/index.mdx rename to ark/fuma/content/docs/expressions/index.mdx diff --git a/ark/docs/fuma/content/docs/expressions/meta.json b/ark/fuma/content/docs/expressions/meta.json similarity index 100% rename from ark/docs/fuma/content/docs/expressions/meta.json rename to ark/fuma/content/docs/expressions/meta.json diff --git a/ark/docs/fuma/content/docs/faq.mdx b/ark/fuma/content/docs/faq.mdx similarity index 100% rename from ark/docs/fuma/content/docs/faq.mdx rename to ark/fuma/content/docs/faq.mdx diff --git a/ark/docs/fuma/content/docs/generics/index.mdx b/ark/fuma/content/docs/generics/index.mdx similarity index 100% rename from ark/docs/fuma/content/docs/generics/index.mdx rename to ark/fuma/content/docs/generics/index.mdx diff --git a/ark/docs/fuma/content/docs/generics/meta.json b/ark/fuma/content/docs/generics/meta.json similarity index 100% rename from ark/docs/fuma/content/docs/generics/meta.json rename to ark/fuma/content/docs/generics/meta.json diff --git a/ark/docs/fuma/content/docs/integrations/index.mdx b/ark/fuma/content/docs/integrations/index.mdx similarity index 100% rename from ark/docs/fuma/content/docs/integrations/index.mdx rename to ark/fuma/content/docs/integrations/index.mdx diff --git a/ark/docs/fuma/content/docs/integrations/meta.json b/ark/fuma/content/docs/integrations/meta.json similarity index 100% rename from ark/docs/fuma/content/docs/integrations/meta.json rename to ark/fuma/content/docs/integrations/meta.json diff --git a/ark/docs/fuma/content/docs/intro/adding-constraints.mdx b/ark/fuma/content/docs/intro/adding-constraints.mdx similarity index 94% rename from ark/docs/fuma/content/docs/intro/adding-constraints.mdx rename to ark/fuma/content/docs/intro/adding-constraints.mdx index 4ba12d24cc..42eaecf379 100644 --- a/ark/docs/fuma/content/docs/intro/adding-constraints.mdx +++ b/ark/fuma/content/docs/intro/adding-constraints.mdx @@ -16,8 +16,7 @@ In other words, **they just work**. Let's create a new `contact` Type that enforces our example constraints. -```ts twoslash -// hover to see the type-level representation +```ts const contact = type({ // many common constraints are available as built-in keywords email: "string.email", @@ -27,9 +26,6 @@ const contact = type({ // if you need the TS type, just infer it out as normal type Contact = typeof contact.infer -// This should cut but it isn't working as expected - -// this empty line prevents the source syntax highlighting from breaking ``` ## Compose diff --git a/ark/docs/fuma/content/docs/intro/morphs-and-more.mdx b/ark/fuma/content/docs/intro/morphs-and-more.mdx similarity index 100% rename from ark/docs/fuma/content/docs/intro/morphs-and-more.mdx rename to ark/fuma/content/docs/intro/morphs-and-more.mdx diff --git a/ark/docs/fuma/content/docs/intro/setup.mdx b/ark/fuma/content/docs/intro/setup.mdx similarity index 100% rename from ark/docs/fuma/content/docs/intro/setup.mdx rename to ark/fuma/content/docs/intro/setup.mdx diff --git a/ark/docs/fuma/content/docs/intro/your-first-type.mdx b/ark/fuma/content/docs/intro/your-first-type.mdx similarity index 100% rename from ark/docs/fuma/content/docs/intro/your-first-type.mdx rename to ark/fuma/content/docs/intro/your-first-type.mdx diff --git a/ark/docs/fuma/content/docs/keywords.mdx b/ark/fuma/content/docs/keywords.mdx similarity index 100% rename from ark/docs/fuma/content/docs/keywords.mdx rename to ark/fuma/content/docs/keywords.mdx diff --git a/ark/docs/fuma/content/docs/meta.json b/ark/fuma/content/docs/meta.json similarity index 100% rename from ark/docs/fuma/content/docs/meta.json rename to ark/fuma/content/docs/meta.json diff --git a/ark/docs/fuma/content/docs/objects/index.mdx b/ark/fuma/content/docs/objects/index.mdx similarity index 92% rename from ark/docs/fuma/content/docs/objects/index.mdx rename to ark/fuma/content/docs/objects/index.mdx index bc50476996..2fc6425fa7 100644 --- a/ark/docs/fuma/content/docs/objects/index.mdx +++ b/ark/fuma/content/docs/objects/index.mdx @@ -11,7 +11,7 @@ Objects definitions can include any combination of required, optional, defaultab ##### required - + ```ts @@ -44,12 +44,12 @@ const myObject = type({ - + ##### optional - + ```ts @@ -102,7 +102,7 @@ const myObject = type({ - + :::caution[Optional properties cannot be present with the value undefined] @@ -131,7 +131,7 @@ const errorResult = myObj({ key: undefined }) ##### defaultable - + ```ts @@ -162,43 +162,21 @@ const myObject = type({ - - -```ts -const myObject = type({ - defaultableKey: type("boolean", "=", false) -}) -``` - - + - +:::caution[Optional and default only work within objects and tuples!] +Unlike e.g. `number.array()`, `number.optional()` and `number.default(0)` don't return a new `Type`, but rather a tuple definition like `[Type, "?"]` or `[Type, "=", 0]`. -:::caution[Optional and default only work within objects!] -Adding a `optional` or `default` to a `Type` doesn't alter its standalone behavior. +This reflects the fact that in ArkType's type system, optionality and defaultability are only meaningful in reference to a property. Attempting to create an optional or defaultable value outside an object like `type("string?")` will result in a `ParseError`. -Rather, it adds metadata that changes how it works when referenced from an object or tuple. +To create a `Type` accepting `string` or `undefined`, use a union like `type("string | undefined")`. -
- See an example +To have it transform `undefined` to an empty string, use an explicit morph like: ```ts -const optionalString = type.string.optional() - -optionalString.allows(undefined) // false - -const objectWithOptionalKey = type({ - foo: optionalString -}) - -objectWithOptionalKey.allows({}) // true +const fallbackString = type("string | undefined").pipe(v => v ?? "") ``` -
- -Prefer the key-embedded syntax (`"optionalKey?":`) where possible. -::: -
##### index diff --git a/ark/docs/fuma/content/docs/objects/meta.json b/ark/fuma/content/docs/objects/meta.json similarity index 100% rename from ark/docs/fuma/content/docs/objects/meta.json rename to ark/fuma/content/docs/objects/meta.json diff --git a/ark/docs/fuma/content/docs/primitives/index.mdx b/ark/fuma/content/docs/primitives/index.mdx similarity index 100% rename from ark/docs/fuma/content/docs/primitives/index.mdx rename to ark/fuma/content/docs/primitives/index.mdx diff --git a/ark/docs/fuma/content/docs/primitives/meta.json b/ark/fuma/content/docs/primitives/meta.json similarity index 100% rename from ark/docs/fuma/content/docs/primitives/meta.json rename to ark/fuma/content/docs/primitives/meta.json diff --git a/ark/docs/fuma/content/docs/primitives/number/index.mdx b/ark/fuma/content/docs/primitives/number/index.mdx similarity index 100% rename from ark/docs/fuma/content/docs/primitives/number/index.mdx rename to ark/fuma/content/docs/primitives/number/index.mdx diff --git a/ark/docs/fuma/content/docs/primitives/number/meta.json b/ark/fuma/content/docs/primitives/number/meta.json similarity index 100% rename from ark/docs/fuma/content/docs/primitives/number/meta.json rename to ark/fuma/content/docs/primitives/number/meta.json diff --git a/ark/docs/fuma/content/docs/primitives/string/index.mdx b/ark/fuma/content/docs/primitives/string/index.mdx similarity index 100% rename from ark/docs/fuma/content/docs/primitives/string/index.mdx rename to ark/fuma/content/docs/primitives/string/index.mdx diff --git a/ark/docs/fuma/content/docs/primitives/string/meta.json b/ark/fuma/content/docs/primitives/string/meta.json similarity index 100% rename from ark/docs/fuma/content/docs/primitives/string/meta.json rename to ark/fuma/content/docs/primitives/string/meta.json diff --git a/ark/docs/fuma/content/docs/scopes/index.mdx b/ark/fuma/content/docs/scopes/index.mdx similarity index 100% rename from ark/docs/fuma/content/docs/scopes/index.mdx rename to ark/fuma/content/docs/scopes/index.mdx diff --git a/ark/docs/fuma/content/docs/scopes/meta.json b/ark/fuma/content/docs/scopes/meta.json similarity index 100% rename from ark/docs/fuma/content/docs/scopes/meta.json rename to ark/fuma/content/docs/scopes/meta.json diff --git a/ark/docs/fuma/content/docs/types/index.mdx b/ark/fuma/content/docs/types/index.mdx similarity index 100% rename from ark/docs/fuma/content/docs/types/index.mdx rename to ark/fuma/content/docs/types/index.mdx diff --git a/ark/docs/fuma/content/docs/types/meta.json b/ark/fuma/content/docs/types/meta.json similarity index 100% rename from ark/docs/fuma/content/docs/types/meta.json rename to ark/fuma/content/docs/types/meta.json diff --git a/ark/docs/fuma/lib/ambient.d.ts b/ark/fuma/lib/ambient.d.ts similarity index 100% rename from ark/docs/fuma/lib/ambient.d.ts rename to ark/fuma/lib/ambient.d.ts diff --git a/ark/docs/fuma/lib/metadata.ts b/ark/fuma/lib/metadata.ts similarity index 79% rename from ark/docs/fuma/lib/metadata.ts rename to ark/fuma/lib/metadata.ts index 22ebf0c9aa..8661f672ac 100644 --- a/ark/docs/fuma/lib/metadata.ts +++ b/ark/fuma/lib/metadata.ts @@ -1,5 +1,5 @@ import { createMetadataImage } from "fumadocs-core/server" -import { source } from "./source.js" +import { source } from "./source.ts" export const metadataImage = createMetadataImage({ imageRoute: "/docs-og", diff --git a/ark/docs/fuma/lib/shiki.ts b/ark/fuma/lib/shiki.ts similarity index 100% rename from ark/docs/fuma/lib/shiki.ts rename to ark/fuma/lib/shiki.ts diff --git a/ark/docs/fuma/lib/source.ts b/ark/fuma/lib/source.ts similarity index 100% rename from ark/docs/fuma/lib/source.ts rename to ark/fuma/lib/source.ts diff --git a/ark/docs/fuma/next-env.d.ts b/ark/fuma/next-env.d.ts similarity index 100% rename from ark/docs/fuma/next-env.d.ts rename to ark/fuma/next-env.d.ts diff --git a/ark/docs/fuma/next.config.ts b/ark/fuma/next.config.ts similarity index 100% rename from ark/docs/fuma/next.config.ts rename to ark/fuma/next.config.ts diff --git a/ark/docs/fuma/package.json b/ark/fuma/package.json similarity index 83% rename from ark/docs/fuma/package.json rename to ark/fuma/package.json index 8b24c0b417..88fbbd2ea1 100644 --- a/ark/docs/fuma/package.json +++ b/ark/fuma/package.json @@ -1,10 +1,11 @@ { - "name": "@ark/docs", + "name": "@ark/fuma-docs", "version": "0.0.1", "private": true, "type": "module", "scripts": { - "build": "pnpm clean && NODE_OPTIONS=--no-warnings next build", + "build": "echo 'skipped in CI'", + "buildDev": "pnpm clean && NODE_OPTIONS=--no-warnings next build", "dev": "NODE_OPTIONS=--no-warnings next dev", "start": "next start", "clean": "rm -rf .next .source out", @@ -30,8 +31,8 @@ "next": "15.0.3", "postcss": "8.4.49", "prettier-plugin-tailwindcss": "0.6.9", - "react": "19.0.0-rc.1", - "react-dom": "19.0.0-rc.1", + "react": "19.0.0", + "react-dom": "19.0.0", "shiki": "1.24.0", "tailwindcss": "3.4.16", "twoslash": "0.2.12", diff --git a/ark/docs/fuma/pnpm-lock.yaml b/ark/fuma/pnpm-lock.yaml similarity index 100% rename from ark/docs/fuma/pnpm-lock.yaml rename to ark/fuma/pnpm-lock.yaml diff --git a/ark/docs/fuma/postcss.config.cjs b/ark/fuma/postcss.config.cjs similarity index 100% rename from ark/docs/fuma/postcss.config.cjs rename to ark/fuma/postcss.config.cjs diff --git a/ark/docs/fuma/public/CNAME b/ark/fuma/public/CNAME similarity index 100% rename from ark/docs/fuma/public/CNAME rename to ark/fuma/public/CNAME diff --git a/ark/docs/fuma/public/image/check.svg b/ark/fuma/public/image/check.svg similarity index 100% rename from ark/docs/fuma/public/image/check.svg rename to ark/fuma/public/image/check.svg diff --git a/ark/docs/fuma/public/image/copy.svg b/ark/fuma/public/image/copy.svg similarity index 100% rename from ark/docs/fuma/public/image/copy.svg rename to ark/fuma/public/image/copy.svg diff --git a/ark/docs/fuma/public/image/errorSquiggle.svg b/ark/fuma/public/image/errorSquiggle.svg similarity index 100% rename from ark/docs/fuma/public/image/errorSquiggle.svg rename to ark/fuma/public/image/errorSquiggle.svg diff --git a/ark/docs/fuma/public/image/favicon.svg b/ark/fuma/public/image/favicon.svg similarity index 100% rename from ark/docs/fuma/public/image/favicon.svg rename to ark/fuma/public/image/favicon.svg diff --git a/ark/docs/fuma/public/image/logo.png b/ark/fuma/public/image/logo.png similarity index 100% rename from ark/docs/fuma/public/image/logo.png rename to ark/fuma/public/image/logo.png diff --git a/ark/docs/fuma/public/image/logoTransparent.png b/ark/fuma/public/image/logoTransparent.png similarity index 100% rename from ark/docs/fuma/public/image/logoTransparent.png rename to ark/fuma/public/image/logoTransparent.png diff --git a/ark/docs/fuma/public/image/openGraphBackground.png b/ark/fuma/public/image/openGraphBackground.png similarity index 100% rename from ark/docs/fuma/public/image/openGraphBackground.png rename to ark/fuma/public/image/openGraphBackground.png diff --git a/ark/docs/fuma/public/image/splash.png b/ark/fuma/public/image/splash.png similarity index 100% rename from ark/docs/fuma/public/image/splash.png rename to ark/fuma/public/image/splash.png diff --git a/ark/docs/fuma/source.config.ts b/ark/fuma/source.config.ts similarity index 82% rename from ark/docs/fuma/source.config.ts rename to ark/fuma/source.config.ts index 42d8469628..02c6f1347f 100644 --- a/ark/docs/fuma/source.config.ts +++ b/ark/fuma/source.config.ts @@ -1,5 +1,5 @@ import { defineConfig, defineDocs } from "fumadocs-mdx/config" -import { shikiConfig } from "./lib/shiki.js" +import { shikiConfig } from "./lib/shiki.ts" export const { docs, meta } = defineDocs({ dir: "content/docs" diff --git a/ark/docs/fuma/tailwind.config.ts b/ark/fuma/tailwind.config.ts similarity index 100% rename from ark/docs/fuma/tailwind.config.ts rename to ark/fuma/tailwind.config.ts diff --git a/ark/docs/fuma/tsconfig.json b/ark/fuma/tsconfig.json similarity index 94% rename from ark/docs/fuma/tsconfig.json rename to ark/fuma/tsconfig.json index 2d2e68573e..1adc762b85 100644 --- a/ark/docs/fuma/tsconfig.json +++ b/ark/fuma/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "../../tsconfig.json", "compilerOptions": { // unfortunately, twoslash doesn't seem to respect customConditions, // so .d.ts will need to be rebuilt to see its static compilation updated diff --git a/ark/repo/scratch.ts b/ark/repo/scratch.ts index c2bd05135f..534dc11f86 100644 --- a/ark/repo/scratch.ts +++ b/ark/repo/scratch.ts @@ -1,2 +1,13 @@ -import { flatMorph } from "@ark/util" -import { ark, type } from "arktype" +import { type } from "arktype" + +// type stats on attribute removal merge 12/18/2024 +// { +// "checkTime": 10.98, +// "types": 409252, +// "instantiations": 5066185 +// } + +// false +const t = type({ foo: "string" }).extends("Record") + +const tt = type(["...", "string[]", "...", ["string?"]]) diff --git a/ark/schema/generic.ts b/ark/schema/generic.ts index d3c2140fbb..1c14ada26a 100644 --- a/ark/schema/generic.ts +++ b/ark/schema/generic.ts @@ -5,7 +5,7 @@ import { throwParseError, type array, type Hkt, - type Json + type JsonStructure } from "@ark/util" import type { RootSchema } from "./kinds.ts" import type { BaseNode } from "./node.ts" @@ -126,7 +126,7 @@ export class GenericRoot< return value } - get json(): Json { + get json(): JsonStructure { return this.cacheGetter("json", { params: this.params.map(param => param[1].isUnknown() ? param[0] : [param[0], param[1].json] diff --git a/ark/schema/node.ts b/ark/schema/node.ts index 41418d04fc..7ca6e1e518 100644 --- a/ark/schema/node.ts +++ b/ark/schema/node.ts @@ -9,7 +9,7 @@ import { throwError, type Dict, type GuardablePredicate, - type Json, + type JsonStructure, type Key, type array, type conform, @@ -82,7 +82,9 @@ export abstract class BaseNode< if (pipedFromCtx) { this.traverseApply(data, pipedFromCtx) - return pipedFromCtx.data + return pipedFromCtx.hasError() ? + pipedFromCtx.errors + : pipedFromCtx.data } const ctx = new TraversalContext(data, this.$.resolvedConfig) @@ -112,6 +114,7 @@ export abstract class BaseNode< readonly includesMorph: boolean = this.kind === "morph" || (this.hasKind("optional") && this.hasDefault()) || + (this.hasKind("sequence") && this.defaultablesLength !== 0) || (this.hasKind("structure") && this.undeclared === "delete") || this.children.some(child => child.includesMorph) @@ -244,7 +247,7 @@ export abstract class BaseNode< return this.$.node(this.kind, ioInner) } - toJSON(): Json { + toJSON(): JsonStructure { return this.json } diff --git a/ark/schema/package.json b/ark/schema/package.json index 1f0097f1ad..5f9d6d14c1 100644 --- a/ark/schema/package.json +++ b/ark/schema/package.json @@ -1,6 +1,6 @@ { "name": "@ark/schema", - "version": "0.26.0", + "version": "0.27.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/schema/parse.ts b/ark/schema/parse.ts index 0d6b336449..2bfbfedf20 100644 --- a/ark/schema/parse.ts +++ b/ark/schema/parse.ts @@ -8,11 +8,11 @@ import { throwInternalError, throwParseError, unset, - type JsonData, - type PartialRecord, + type Brand, type dict, + type Json, type listable, - type nominal + type PartialRecord } from "@ark/util" import { nodeClassesByKind, @@ -41,11 +41,6 @@ export type ContextualArgs = Record export type BaseParseOptions = { alias?: string prereduced?: prereduced - /** Instead of creating the node, compute the innerHash of the definition and - * point it to the specified resolution. - * - * Useful for defining reductions like number|string|bigint|symbol|object|true|false|null|undefined => unknown - **/ args?: ContextualArgs id?: NodeId } @@ -130,7 +125,7 @@ const serializeListableChild = (listableNode: listable) => listableNode.map(node => node.collapsibleJson) : listableNode.collapsibleJson -export type NodeId = nominal +export type NodeId = Brand export type NodeResolver = (id: NodeId) => BaseNode @@ -228,11 +223,12 @@ export const createNode = ( innerJson[k] = serialize(v as never) - if (keyImpl.child) { + if (keyImpl.child === true) { const listableNode = v as listable if (isArray(listableNode)) children.push(...listableNode) else children.push(listableNode) - } + } else if (typeof keyImpl.child === "function") + children.push(...keyImpl.child(v as never)) }) if (impl.finalizeInnerJson) @@ -272,7 +268,7 @@ export const createNode = ( metaJson, json, hash, - collapsibleJson: collapsibleJson as JsonData, + collapsibleJson: collapsibleJson as Json, children } diff --git a/ark/schema/roots/root.ts b/ark/schema/roots/root.ts index df7adbd4c8..4496c25707 100644 --- a/ark/schema/roots/root.ts +++ b/ark/schema/roots/root.ts @@ -4,17 +4,11 @@ import { omit, throwInternalError, throwParseError, - unset, type Fn, type array } from "@ark/util" import { throwInvalidOperandError, type Constraint } from "../constraint.ts" -import type { - NodeSchema, - mutableNormalizedRootOfKind, - nodeOfKind, - reducibleKindOf -} from "../kinds.ts" +import type { NodeSchema, nodeOfKind, reducibleKindOf } from "../kinds.ts" import { BaseNode, appendUniqueFlatRefs, @@ -104,43 +98,6 @@ export abstract class BaseRoot< } } - get optionalMeta(): boolean { - return this.cacheGetter( - "optionalMeta", - this.meta.optional === true || - (this.hasKind("morph") && this.in.meta.optional === true) - ) - } - - /** returns unset if there is no default */ - get defaultMeta(): unknown { - return this.cacheGetter( - "defaultMeta", - "default" in this.meta ? this.meta.default - : this.hasKind("morph") ? this.in.defaultMeta - : unset - ) - } - - withoutOptionalOrDefaultMeta(): this { - if (!this.optionalMeta && this.defaultMeta === unset) return this - const meta = { ...this.meta } - delete meta.default - delete meta.optional - - if ( - !this.hasKind("morph") || - (!this.in.optionalMeta && this.in.defaultMeta === unset) - ) - return this.withMeta(meta) - - return this.$.node("morph", { - ...this.inner, - in: this.in.withoutOptionalOrDefaultMeta(), - meta - }) as never - } - as(): this { return this } @@ -372,14 +329,18 @@ export abstract class BaseRoot< return this.configure({ description }) } - optional(): this { - return this.withMeta({ optional: true }) + // these should ideally be implemented in arktype since they use its syntax + // https://github.com/arktypeio/arktype/issues/1223 + optional(): [this, "?"] { + return [this, "?"] } - default(value: unknown): this { - assertDefaultValueAssignability(this, value) + // these should ideally be implemented in arktype since they use its syntax + // https://github.com/arktypeio/arktype/issues/1223 + default(thunkableValue: unknown): [this, "=", unknown] { + assertDefaultValueAssignability(this, thunkableValue, null) - return this.withMeta({ default: value }) + return [this, "=", thunkableValue] } from(input: unknown): unknown { @@ -442,15 +403,7 @@ export abstract class BaseRoot< in: branch, morphs: [morph] }), - branches => { - if (!this.hasKind("union")) return this.$.parseSchema(branches[0]) - const schema: mutableNormalizedRootOfKind<"union"> = { - branches - } - if ("default" in this.meta) schema.meta = { default: this.meta.default } - else if (this.meta.optional) schema.meta = { optional: true } - return this.$.parseSchema(schema) - } + this.$.parseSchema ) } diff --git a/ark/schema/roots/union.ts b/ark/schema/roots/union.ts index 8353cc47ba..c2b46791cc 100644 --- a/ark/schema/roots/union.ts +++ b/ark/schema/roots/union.ts @@ -9,7 +9,7 @@ import { printable, throwParseError, type JsTypeOf, - type Json, + type JsonStructure, type Key, type SerializedPrimitive, type array, @@ -519,7 +519,7 @@ export const Union = { Node: UnionNode } -const discriminantToJson = (discriminant: Discriminant): Json => ({ +const discriminantToJson = (discriminant: Discriminant): JsonStructure => ({ kind: discriminant.kind, path: discriminant.path.map(k => typeof k === "string" ? k : compileSerializedValue(k) diff --git a/ark/schema/scope.ts b/ark/schema/scope.ts index ad0505b93b..921234063e 100644 --- a/ark/schema/scope.ts +++ b/ark/schema/scope.ts @@ -10,7 +10,7 @@ import { type Constructor, type Dict, type Fn, - type Json, + type JsonStructure, type anyOrNever, type array, type conform, @@ -142,7 +142,7 @@ export abstract class BaseScope<$ extends {} = {}> { readonly resolutions: { [alias: string]: CachedResolution | undefined } = {} - readonly json: Json = {} + readonly json: JsonStructure = {} exportedNames: string[] = [] readonly aliases: Record = {} protected resolved = false @@ -612,7 +612,7 @@ const bootstrapAliasReferences = (resolution: BaseRoot | GenericRoot) => { return resolution } -const resolutionsToJson = (resolutions: InternalResolutions): Json => +const resolutionsToJson = (resolutions: InternalResolutions): JsonStructure => flatMorph(resolutions, (k, v) => [ k, hasArkKind(v, "root") || hasArkKind(v, "generic") ? v.json diff --git a/ark/schema/shared/declare.ts b/ark/schema/shared/declare.ts index 609a05b851..1fdd8c4613 100644 --- a/ark/schema/shared/declare.ts +++ b/ark/schema/shared/declare.ts @@ -10,7 +10,6 @@ type withMetaPrefixedKeys = { export interface BaseMeta extends JsonSchema.Meta { alias?: string - optional?: true } declare global { diff --git a/ark/schema/shared/implement.ts b/ark/schema/shared/implement.ts index 6ad613a371..922f0746cb 100644 --- a/ark/schema/shared/implement.ts +++ b/ark/schema/shared/implement.ts @@ -4,7 +4,7 @@ import { throwParseError, type Entry, type Json, - type JsonData, + type JsonStructure, type KeySet, type arrayIndexOf, type entryOf, @@ -254,7 +254,7 @@ type keyRequiringSchemaDefinition = Exclude< keyof BaseNormalizedSchema > -export const defaultValueSerializer = (v: unknown): JsonData => { +export const defaultValueSerializer = (v: unknown): Json => { if (typeof v === "string" || typeof v === "boolean" || v === null) return v if (typeof v === "number") { @@ -275,8 +275,8 @@ export type NodeKeyImplementation< > = requireKeys< { preserveUndefined?: true - child?: boolean - serialize?: (schema: instantiated) => JsonData + child?: boolean | ((value: instantiated) => BaseNode[]) + serialize?: (schema: instantiated) => Json reduceIo?: ( ioKind: "in" | "out", inner: makeRootAndArrayPropertiesMutable, @@ -300,7 +300,9 @@ interface CommonNodeImplementationInput { keys: keySchemaDefinitions normalize: (schema: d["schema"], $: BaseScope) => d["normalizedSchema"] hasAssociatedError: d["errorContext"] extends null ? false : true - finalizeInnerJson?: (json: { [k in keyof d["inner"]]: JsonData }) => Json + finalizeInnerJson?: (json: { + [k in keyof d["inner"]]: Json + }) => JsonStructure collapsibleKey?: keyof d["inner"] reduce?: ( inner: d["inner"], @@ -374,7 +376,7 @@ export interface UnknownAttachments { readonly json: object readonly hash: string - readonly collapsibleJson: JsonData + readonly collapsibleJson: Json readonly children: BaseNode[] } @@ -382,9 +384,9 @@ export interface NarrowedAttachments extends UnknownAttachments { kind: d["kind"] inner: d["inner"] - json: Json - innerJson: Json - collapsibleJson: JsonData + json: JsonStructure + innerJson: JsonStructure + collapsibleJson: Json children: nodeOfKind[] } diff --git a/ark/schema/shared/intersections.ts b/ark/schema/shared/intersections.ts index afd915bab0..957ad6fa07 100644 --- a/ark/schema/shared/intersections.ts +++ b/ark/schema/shared/intersections.ts @@ -151,9 +151,6 @@ const pipeMorphed = ( let meta: BaseMeta | undefined - if ("default" in from.meta) meta = { default: from.meta.default } - else if (from.meta.optional) meta = { optional: true } - if (viableBranches.length === 1) { const onlyBranch = viableBranches[0] if (!meta) return onlyBranch diff --git a/ark/schema/shared/utils.ts b/ark/schema/shared/utils.ts index 567a3744e7..95fb6b9fa1 100644 --- a/ark/schema/shared/utils.ts +++ b/ark/schema/shared/utils.ts @@ -4,7 +4,8 @@ import { noSuggest, type array, type mutable, - type show + type show, + type Thunk } from "@ark/util" import type { BaseConstraint } from "../constraint.ts" import type { GenericRoot } from "../generic.ts" @@ -63,3 +64,6 @@ export const hasArkKind = ( export const isNode = (value: unknown): value is BaseNode => hasArkKind(value, "root") || hasArkKind(value, "constraint") + +export type unwrapDefault = + thunkableValue extends Thunk ? returnValue : thunkableValue diff --git a/ark/schema/structure/optional.ts b/ark/schema/structure/optional.ts index 0de38ed66c..0ae42b34cf 100644 --- a/ark/schema/structure/optional.ts +++ b/ark/schema/structure/optional.ts @@ -1,14 +1,8 @@ -import { - hasDomain, - omit, - printable, - throwParseError, - unset, - type keySetOf -} from "@ark/util" +import { hasDomain, isThunk, printable, throwParseError } from "@ark/util" import type { Morph } from "../roots/morph.ts" import type { BaseRoot } from "../roots/root.ts" -import type { BaseMeta, declareNode } from "../shared/declare.ts" +import { compileSerializedValue } from "../shared/compile.ts" +import type { declareNode } from "../shared/declare.ts" import { ArkErrors } from "../shared/errors.ts" import { implementNode, @@ -53,13 +47,7 @@ const implementation: nodeImplementationOf = preserveUndefined: true } }, - // safe to spread here as a node will never be passed to normalize - normalize: ({ ...schema }, $) => { - const value = $.parseSchema(schema.value) - schema.value = value - if (value.defaultMeta !== unset) schema.default ??= value.defaultMeta - return schema - }, + normalize: schema => schema, defaults: { description: node => `${node.compiledKey}?: ${node.value.description}` }, @@ -71,120 +59,119 @@ const implementation: nodeImplementationOf = export class OptionalNode extends BaseProp<"optional"> { constructor(...args: ConstructorParameters) { super(...args) - if ("default" in this.inner) { - assertDefaultValueAssignability( - this.value, - this.inner.default, - this.serializedKey - ) - } + if ("default" in this.inner) + assertDefaultValueAssignability(this.value, this.inner.default, this.key) } get outProp(): Prop.Node { if (!this.hasDefault()) return this const { default: defaultValue, ...requiredInner } = this.inner - requiredInner.value = requiredInner.value.withMeta(meta => - omit(meta, optionalValueMetaKeys) - ) - return this.cacheGetter( "outProp", this.$.node("required", requiredInner, { prereduced: true }) as never ) } - expression: string = `${this.compiledKey}?: ${this.value.expression}${this.hasDefault() ? ` = ${printable(this.inner.default)}` : ""}` + expression: string = + this.hasDefault() ? + `${this.compiledKey}: ${this.value.expression} = ${printable(this.inner.default)}` + : `${this.compiledKey}?: ${this.value.expression}` - defaultValueMorphs: Morph[] = this.computeDefaultValueMorphs() + defaultValueMorphs: Morph[] = + this.hasDefault() ? + computeDefaultValueMorphs(this.key, this.value, this.default) + : [] defaultValueMorphsReference = registeredReference(this.defaultValueMorphs) +} - private computeDefaultValueMorphs(): Morph[] { - if (!this.hasDefault()) return [] - - const defaultInput = this.default - - if (typeof defaultInput === "function") { - return [ - // if the value has a morph, pipe context through it - this.value.includesMorph ? - (data, ctx) => { - traverseKey( - this.key, - () => this.value((data[this.key] = defaultInput()), ctx), - ctx - ) - return data - } - : data => { - data[this.key] = defaultInput() - return data - } - ] - } - - // non-functional defaults can be safely cached as long as the morph is - // guaranteed to be pure and the output is primitive - const precomputedMorphedDefault = - this.value.includesMorph ? this.value.assert(defaultInput) : defaultInput +export const Optional = { + implementation, + Node: OptionalNode +} +export const computeDefaultValueMorphs = ( + key: PropertyKey, + value: BaseRoot, + defaultInput: unknown +): Morph[] => { + if (typeof defaultInput === "function") { return [ - hasDomain(precomputedMorphedDefault, "object") ? - // the type signature only allows this if the value was morphed + // if the value has a morph, pipe context through it + value.includesMorph ? (data, ctx) => { - traverseKey( - this.key, - () => this.value((data[this.key] = defaultInput), ctx), - ctx - ) + traverseKey(key, () => value((data[key] = defaultInput()), ctx), ctx) return data } : data => { - data[this.key] = precomputedMorphedDefault + data[key] = defaultInput() return data } ] } -} - -export const Optional = { - implementation, - Node: OptionalNode -} -const optionalValueMetaKeys: keySetOf = { - default: 1, - optional: 1 + // non-functional defaults can be safely cached as long as the morph is + // guaranteed to be pure and the output is primitive + const precomputedMorphedDefault = + value.includesMorph ? value.assert(defaultInput) : defaultInput + + return [ + hasDomain(precomputedMorphedDefault, "object") ? + // the type signature only allows this if the value was morphed + (data, ctx) => { + traverseKey(key, () => value((data[key] = defaultInput), ctx), ctx) + return data + } + : data => { + data[key] = precomputedMorphedDefault + return data + } + ] } export const assertDefaultValueAssignability = ( node: BaseRoot, value: unknown, - key = "" + key: PropertyKey | null ): unknown => { - if (hasDomain(value, "object") && typeof value !== "function") + const wrapped = isThunk(value) + + if (hasDomain(value, "object") && !wrapped) throwParseError(writeNonPrimitiveNonFunctionDefaultValueMessage(key)) - const out = node.in(typeof value === "function" ? value() : value) - if (out instanceof ArkErrors) - throwParseError(writeUnassignableDefaultValueMessage(out.message, key)) + const out = node.in(wrapped ? value() : value) + + if (out instanceof ArkErrors) { + // error summaries are computed via getters, so it is safe to + // mutate the paths to include key to ensure messages are + // generated the integrate the location with the reason + if (key !== null) out.forEach(e => (e.path as any).unshift(key)) + + const message = + key === null ? + // e.g. "Default must be assignable to number (was string)" + `Default ${out.message}` + // e.g. "Default for bar must be assignable to number (was string)" + // e.g. "Default for value at [0] must be assignable to number (was string)" + : `Default for ${out.message}` + throwParseError(message) + } return value } -export const writeUnassignableDefaultValueMessage = ( - message: string, - key = "" -): string => - `Default value${key && ` for key ${key}`} is not assignable: ${message}` - export type writeUnassignableDefaultValueMessage< baseDef extends string, defaultValue extends string -> = `Default value ${defaultValue} is not assignable to ${baseDef}` +> = `Default value ${defaultValue} must be assignable to ${baseDef}` export const writeNonPrimitiveNonFunctionDefaultValueMessage = ( - key: string -): string => - `Default value${key && ` for key ${key}`} is not primitive so it should be specified as a function like () => ({my: 'object'})` + key: PropertyKey | null +): string => { + const keyDescription = + key === null ? "" + : typeof key === "number" ? `for value at [${key}] ` + : `for ${compileSerializedValue(key)} ` + return `Non-primitive default ${keyDescription}must be specified as a function like () => ({my: 'object'})` +} diff --git a/ark/schema/structure/prop.ts b/ark/schema/structure/prop.ts index ffef0d9de3..666b1a8a6f 100644 --- a/ark/schema/structure/prop.ts +++ b/ark/schema/structure/prop.ts @@ -79,9 +79,7 @@ export const intersectProps = ( r.hasDefault() ? l.default === r.default ? l.default - : throwParseError( - `Invalid intersection of default values ${printable(l.default)} & ${printable(r.default)}` - ) + : throwParseError(writeDefaultIntersectionMessage(l.default, r.default)) : l.default : r.hasDefault() ? r.default : unset @@ -172,3 +170,9 @@ export abstract class BaseProp< if (js.traversalKind === "Allows") js.return(true) } } + +export const writeDefaultIntersectionMessage = ( + lValue: unknown, + rValue: unknown +): string => + `Invalid intersection of default values ${printable(lValue)} & ${printable(rValue)}` diff --git a/ark/schema/structure/required.ts b/ark/schema/structure/required.ts index 628a4f75db..9aad9f17d3 100644 --- a/ark/schema/structure/required.ts +++ b/ark/schema/structure/required.ts @@ -1,4 +1,3 @@ -import { unset } from "@ark/util" import type { BaseErrorContext, declareNode } from "../shared/declare.ts" import type { ArkErrorContextInput } from "../shared/errors.ts" import { @@ -7,7 +6,6 @@ import { type nodeImplementationOf } from "../shared/implement.ts" import { BaseProp, intersectProps, type Prop } from "./prop.ts" - export declare namespace Required { export interface ErrorContext extends BaseErrorContext<"required"> { missingValueDescription: string @@ -23,7 +21,6 @@ export declare namespace Required { normalizedSchema: Schema inner: Inner errorContext: ErrorContext - reducibleTo: "optional" } > @@ -43,15 +40,6 @@ const implementation: nodeImplementationOf = } }, normalize: schema => schema, - reduce: (inner, $) => { - if (inner.value.defaultMeta !== unset) { - return $.node("optional", { - ...inner, - default: inner.value.defaultMeta - }) - } - if (inner.value.optionalMeta) return $.node("optional", inner) - }, defaults: { description: node => `${node.compiledKey}: ${node.value.description}`, expected: ctx => ctx.missingValueDescription, diff --git a/ark/schema/structure/sequence.ts b/ark/schema/structure/sequence.ts index fb3505693b..cfb4d74ebb 100644 --- a/ark/schema/structure/sequence.ts +++ b/ark/schema/structure/sequence.ts @@ -1,6 +1,7 @@ import { append, conflatenate, + printable, throwInternalError, throwParseError, type array, @@ -20,14 +21,15 @@ import { import type { ExactLengthNode } from "../refinements/exactLength.ts" import type { MaxLengthNode } from "../refinements/maxLength.ts" import type { MinLengthNode } from "../refinements/minLength.ts" +import type { Morph } from "../roots/morph.ts" import type { BaseRoot } from "../roots/root.ts" import type { NodeCompiler } from "../shared/compile.ts" import type { BaseNormalizedSchema, declareNode } from "../shared/declare.ts" import { Disjoint } from "../shared/disjoint.ts" import { + defaultValueSerializer, implementNode, type IntersectionContext, - type NodeKeyImplementation, type RootKind, type nodeImplementationOf } from "../shared/implement.ts" @@ -36,16 +38,21 @@ import { writeUnsupportedJsonSchemaTypeMessage, type JsonSchema } from "../shared/jsonSchema.ts" -import { $ark } from "../shared/registry.ts" +import { $ark, registeredReference } from "../shared/registry.ts" import { traverseKey, type TraverseAllows, type TraverseApply } from "../shared/traversal.ts" - +import { + assertDefaultValueAssignability, + computeDefaultValueMorphs +} from "./optional.ts" +import { writeDefaultIntersectionMessage } from "./prop.ts" export declare namespace Sequence { export interface NormalizedSchema extends BaseNormalizedSchema { readonly prefix?: array + readonly defaultables?: array readonly optionals?: array readonly variadic?: RootSchema readonly minVariadicLength?: number @@ -54,10 +61,16 @@ export declare namespace Sequence { export type Schema = NormalizedSchema | RootSchema + export type DefaultableSchema = [schema: RootSchema, defaultValue: unknown] + + export type DefaultableElement = [node: BaseRoot, defaultValue: unknown] + export interface Inner { // a list of fixed position elements starting at index 0 readonly prefix?: array - // a list of optional elements following prefix + // a list of optional elements with default values following prefix + readonly defaultables?: array + // a list of optional elements without default values following defaultables readonly optionals?: array // the variadic element (only checked if all optional elements are present) readonly variadic?: BaseRoot @@ -80,27 +93,47 @@ export declare namespace Sequence { export type Node = SequenceNode } -const fixedSequenceKeySchemaDefinition: NodeKeyImplementation< - Sequence.Declaration, - "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.$.parseSchema(element)) -} - const implementation: nodeImplementationOf = implementNode({ kind: "sequence", hasAssociatedError: false, collapsibleKey: "variadic", keys: { - prefix: fixedSequenceKeySchemaDefinition, - optionals: fixedSequenceKeySchemaDefinition, + prefix: { + child: true, + parse: (schema, ctx) => { + // empty affixes are omitted. an empty array should therefore + // be specified as `{ proto: Array, length: 0 }` + if (schema.length === 0) return undefined + + return schema.map(element => ctx.$.parseSchema(element)) + } + }, + optionals: { + child: true, + parse: (schema, ctx) => { + if (schema.length === 0) return undefined + + return schema.map(element => ctx.$.parseSchema(element)) + } + }, + defaultables: { + child: defaultables => defaultables.map(element => element[0]), + parse: (defaultables, ctx) => { + if (defaultables.length === 0) return undefined + + return defaultables.map(element => { + const node = ctx.$.parseSchema(element[0]) + assertDefaultValueAssignability(node, element[1], null) + return [node, element[1]] + }) + }, + serialize: defaults => + defaults.map(element => [ + element[0].collapsibleJson, + defaultValueSerializer(element[1]) + ]) + }, variadic: { child: true, parse: (schema, ctx) => ctx.$.parseSchema(schema, ctx) @@ -111,7 +144,14 @@ const implementation: nodeImplementationOf = // node it implies parse: min => (min === 0 ? undefined : min) }, - postfix: fixedSequenceKeySchemaDefinition + postfix: { + child: true, + parse: (schema, ctx) => { + if (schema.length === 0) return undefined + + return schema.map(element => ctx.$.parseSchema(element)) + } + } }, normalize: schema => { if (typeof schema === "string") return { variadic: schema } @@ -119,6 +159,7 @@ const implementation: nodeImplementationOf = if ( "variadic" in schema || "prefix" in schema || + "defaultables" in schema || "optionals" in schema || "postfix" in schema || "minVariadicLength" in schema @@ -127,7 +168,7 @@ const implementation: nodeImplementationOf = if (!schema.variadic) return throwParseError(postfixWithoutVariadicMessage) - if (schema.optionals?.length) + if (schema.optionals?.length || schema.defaultables?.length) return throwParseError(postfixFollowingOptionalMessage) } if (schema.minVariadicLength && !schema.variadic) { @@ -142,13 +183,14 @@ const implementation: nodeImplementationOf = reduce: (raw, $) => { let minVariadicLength = raw.minVariadicLength ?? 0 const prefix = raw.prefix?.slice() ?? [] - const optional = raw.optionals?.slice() ?? [] + const defaultables = raw.defaultables?.slice() ?? [] + const optionals = 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() + while (optionals.at(-1)?.equals(raw.variadic)) optionals.pop() - if (optional.length === 0) { + if (optionals.length === 0 && defaultables.length === 0) { // If there are no optional, normalize prefix // elements adjacent and equivalent to variadic: // { variadic: number, prefix: [string, number] } @@ -167,8 +209,8 @@ const implementation: nodeImplementationOf = postfix.shift() minVariadicLength++ } - } else if (optional.length === 0) { - // if there's no variadic or optional parameters, + } else if (optionals.length === 0 && defaultables.length === 0) { + // if there's no variadic, optional or defaultable elements, // postfix can just be appended to prefix prefix.push(...postfix.splice(0)) } @@ -185,8 +227,9 @@ const implementation: nodeImplementationOf = ...raw, // empty lists will be omitted during parsing prefix, + defaultables, + optionals, postfix, - optionals: optional, minVariadicLength }, { prereduced: true } @@ -198,7 +241,10 @@ const implementation: nodeImplementationOf = if (node.isVariadicOnly) return `${node.variadic!.nestableExpression}[]` const innerDescription = node.tuple .map(element => - element.kind === "optionals" ? `${element.node.nestableExpression}?` + element.kind === "defaultables" ? + `${element.node.nestableExpression} = ${printable(element.default)}` + : element.kind === "optionals" ? + `${element.node.nestableExpression}?` : element.kind === "variadic" ? `...${element.node.nestableExpression}[]` : element.node.expression @@ -250,10 +296,18 @@ const implementation: nodeImplementationOf = export class SequenceNode extends BaseConstraint { impliedBasis: BaseRoot = $ark.intrinsic.Array.internal + tuple: SequenceTuple = sequenceInnerToTuple(this.inner) + prefixLength: number = this.prefix?.length ?? 0 + defaultablesLength: number = this.defaultables?.length ?? 0 optionalsLength: number = this.optionals?.length ?? 0 postfixLength: number = this.postfix?.length ?? 0 - prevariadic: array = conflatenate(this.prefix, this.optionals) + prevariadic: array = this.tuple.filter( + el => + el.kind === "prefix" || + el.kind === "defaultables" || + el.kind === "optionals" + ) variadicOrPostfix: array = conflatenate( this.variadic && [this.variadic], @@ -270,8 +324,7 @@ export class SequenceNode extends BaseConstraint { // cast is safe here as the only time this would not be a // MinLengthNode would be when minLength is 0 : (this.$.node("minLength", this.minLength) as never) - maxLength: number | null = - this.variadic ? null : this.minLength + this.optionalsLength + maxLength: number | null = this.variadic ? null : this.tuple.length maxLengthNode: MaxLengthNode | ExactLengthNode | null = this.maxLength === null ? null : this.$.node("maxLength", this.maxLength) impliedSiblings: array = @@ -282,35 +335,50 @@ export class SequenceNode extends BaseConstraint { : this.maxLengthNode ? [this.maxLengthNode] : [] - protected childAtIndex(data: array, index: number): BaseRoot { - if (index < this.prevariadic.length) return this.prevariadic[index] + defaultValueMorphs: Morph[][] = + this.defaultables?.map(([node, defaultValue], i) => + computeDefaultValueMorphs(this.prefixLength + i, node, defaultValue) + ) ?? [] + + defaultValueMorphsReference = registeredReference(this.defaultValueMorphs) + + protected elementAtIndex(data: array, index: number): SequenceElement { + if (index < this.prevariadic.length) return this.tuple[index] const firstPostfixIndex = data.length - this.postfixLength if (index >= firstPostfixIndex) - return this.postfix![index - firstPostfixIndex] - return ( - this.variadic ?? - throwInternalError( - `Unexpected attempt to access index ${index} on ${this}` - ) - ) + return { kind: "postfix", node: this.postfix![index - firstPostfixIndex] } + return { + kind: "variadic", + node: + 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 + for (let i = 0; i < data.length; i++) { + if (!this.elementAtIndex(data, i).node.traverseAllows(data[i], ctx)) + return false + } return true } traverseApply: TraverseApply = (data, ctx) => { - for (let i = 0; i < data.length; i++) { + let i = 0 + for (; i < data.length; i++) { traverseKey( i, - () => this.childAtIndex(data, i).traverseApply(data[i], ctx), + () => this.elementAtIndex(data, i).node.traverseApply(data[i], ctx), ctx ) } + + for (; i < this.prefixLength + this.defaultablesLength; i++) + ctx.queueMorphs(this.defaultValueMorphs[i - this.prefixLength]) } override get flatRefs(): FlatRef[] { @@ -320,8 +388,10 @@ export class SequenceNode extends BaseConstraint { refs, this.prevariadic.flatMap((element, i) => append( - element.flatRefs.map(ref => flatRef([`${i}`, ...ref.path], ref.node)), - flatRef([`${i}`], element) + element.node.flatRefs.map(ref => + flatRef([`${i}`, ...ref.path], ref.node) + ), + flatRef([`${i}`], element.node) ) ) ) @@ -355,8 +425,19 @@ export class SequenceNode extends BaseConstraint { this.prefix?.forEach((node, i) => js.traverseKey(`${i}`, `data[${i}]`, node) ) - this.optionals?.forEach((node, i) => { + + this.defaultables?.forEach((node, i) => { const dataIndex = `${i + this.prefixLength}` + js.if(`${dataIndex} >= ${js.data}.length`, () => + js.traversalKind === "Allows" ? + js.return(true) + : js.return(`ctx.queueMorphs(${this.defaultValueMorphsReference}[${i}])`) + ) + js.traverseKey(dataIndex, `data[${dataIndex}]`, node[0]) + }) + + this.optionals?.forEach((node, i) => { + const dataIndex = `${i + this.prefixLength + this.defaultablesLength}` js.if(`${dataIndex} >= ${js.data}.length`, () => js.traversalKind === "Allows" ? js.return(true) : js.return() ) @@ -394,7 +475,6 @@ export class SequenceNode extends BaseConstraint { return result } - tuple: SequenceTuple = sequenceInnerToTuple(this.inner) // this depends on tuple so needs to come after it expression: string = this.description @@ -444,6 +524,9 @@ export const Sequence = { const sequenceInnerToTuple = (inner: Sequence.Inner): SequenceTuple => { const tuple: mutable = [] inner.prefix?.forEach(node => tuple.push({ kind: "prefix", node })) + inner.defaultables?.forEach(([node, defaultValue]) => + tuple.push({ kind: "defaultables", node, default: defaultValue }) + ) 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 })) @@ -451,15 +534,19 @@ const sequenceInnerToTuple = (inner: Sequence.Inner): SequenceTuple => { } const sequenceTupleToInner = (tuple: SequenceTuple): Sequence.Inner => - tuple.reduce>((result, node) => { - if (node.kind === "variadic") result.variadic = node.node - else result[node.kind] = append(result[node.kind], node.node) + tuple.reduce>((result, element) => { + if (element.kind === "variadic") result.variadic = element.node + else if (element.kind === "defaultables") { + result.defaultables = append(result.defaultables, [ + [element.node, element.default] + ]) + } else result[element.kind] = append(result[element.kind], element.node) return result }, {}) export const postfixFollowingOptionalMessage = - "A postfix required element cannot follow an optional element" + "A postfix required element cannot follow an optional or defaultable element" export type postfixFollowingOptionalMessage = typeof postfixFollowingOptionalMessage @@ -469,15 +556,47 @@ export const postfixWithoutVariadicMessage = export type postfixWithoutVariadicMessage = typeof postfixWithoutVariadicMessage +export type SequenceElement = + | PrevariadicSequenceElement + | VariadicSequenceElement + | PostfixSequenceElement + export type SequenceElementKind = satisfy< keyof Sequence.Inner, - "prefix" | "optionals" | "variadic" | "postfix" + SequenceElement["kind"] > -export type SequenceElement = { - kind: SequenceElementKind +export type PrevariadicSequenceElement = + | PrefixSequenceElement + | DefaultableSequenceElement + | OptionalSequenceElement + +export type PrefixSequenceElement = { + kind: "prefix" + node: BaseRoot +} + +export type OptionalSequenceElement = { + kind: "optionals" + node: BaseRoot +} + +export type PostfixSequenceElement = { + kind: "postfix" + node: BaseRoot +} + +export type VariadicSequenceElement = { + kind: "variadic" node: BaseRoot } + +export type DefaultableSequenceElement = { + kind: "defaultables" + node: BaseRoot + default: unknown +} + export type SequenceTuple = array type SequenceIntersectionState = { @@ -502,15 +621,15 @@ const _intersectSequences = ( const kind: SequenceElementKind = lHead.kind === "prefix" || rHead.kind === "prefix" ? "prefix" - : lHead.kind === "optionals" || rHead.kind === "optionals" ? + : lHead.kind === "postfix" || rHead.kind === "postfix" ? "postfix" + : lHead.kind === "variadic" && rHead.kind === "variadic" ? "variadic" // 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" + : lHasPostfix || rHasPostfix ? "prefix" + : lHead.kind === "defaultables" || rHead.kind === "defaultables" ? + "defaultables" + : "optionals" if (lHead.kind === "prefix" && rHead.kind === "variadic" && rHasPostfix) { const postfixBranchResult = _intersectSequences({ @@ -546,7 +665,7 @@ const _intersectSequences = ( ) ) s.result = [...s.result, { kind, node: $ark.intrinsic.never.internal }] - } else if (kind === "optionals") { + } else if (kind === "optionals" || kind === "defaultables") { // 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 @@ -564,6 +683,30 @@ const _intersectSequences = ( r: lTail.map(element => ({ ...element, kind: "prefix" })) }) } + } else if (kind === "defaultables") { + if ( + lHead.kind === "defaultables" && + rHead.kind === "defaultables" && + lHead.default !== rHead.default + ) { + throwParseError( + writeDefaultIntersectionMessage(lHead.default, rHead.default) + ) + } + + s.result = [ + ...s.result, + { + kind, + node: result, + default: + lHead.kind === "defaultables" ? lHead.default + : rHead.kind === "defaultables" ? rHead.default + : throwInternalError( + `Unexpected defaultable intersection from ${lHead.kind} and ${rHead.kind} elements.` + ) + } + ] } else s.result = [...s.result, { kind, node: result }] const lRemaining = s.l.length diff --git a/ark/schema/structure/structure.ts b/ark/schema/structure/structure.ts index aab27458b6..9f4ad7f387 100644 --- a/ark/schema/structure/structure.ts +++ b/ark/schema/structure/structure.ts @@ -406,7 +406,7 @@ export class StructureNode extends BaseConstraint { } else { const index = Number.parseInt(key as string) if (index < this.sequence.prevariadic.length) { - const fixedElement = this.sequence.prevariadic[index] + const fixedElement = this.sequence.prevariadic[index].node value = value?.and(fixedElement) ?? fixedElement required ||= index < this.sequence.prefixLength } else if (this.sequence.variadic) { @@ -465,15 +465,14 @@ export class StructureNode extends BaseConstraint { const { optional, ...inner } = this.inner return this.$.node("structure", { ...inner, - required: this.props.map(prop => { - if (prop.hasKind("required")) return prop - // strip default/optional meta from the value so that it - // isn't reduced back to an optional prop - return this.$.node("required", { - key: prop.key, - value: prop.value.withoutOptionalOrDefaultMeta() - }) - }) + required: this.props.map(prop => + prop.hasKind("optional") ? + { + key: prop.key, + value: prop.value + } + : prop + ) }) } diff --git a/ark/type/__tests__/arrays/array.test.ts b/ark/type/__tests__/arrays/array.test.ts index 24ddc5ce0b..f349210f71 100644 --- a/ark/type/__tests__/arrays/array.test.ts +++ b/ark/type/__tests__/arrays/array.test.ts @@ -5,7 +5,7 @@ import { incompleteArrayTokenMessage } from "arktype/internal/parser/shift/opera import { multipleVariadicMesage, writeNonArraySpreadMessage -} from "arktype/internal/parser/tuple.ts" +} from "arktype/internal/parser/tupleLiteral.ts" contextualize(() => { describe("non-tuple", () => { diff --git a/ark/type/__tests__/arrays/defaults.test.ts b/ark/type/__tests__/arrays/defaults.test.ts new file mode 100644 index 0000000000..82601e610c --- /dev/null +++ b/ark/type/__tests__/arrays/defaults.test.ts @@ -0,0 +1,95 @@ +import { attest, contextualize } from "@ark/attest" +import { type } from "arktype" +import { defaultablePostOptionalMessage } from "arktype/internal/parser/tupleLiteral.ts" + +contextualize(() => { + it("single element tuple", () => { + const t = type([["number", "=", 5]]) + attest(t.t).type.toString.snap("[Default]") + attest(t.expression).snap("[number = 5]") + attest(t.json).snap({ + sequence: { defaultables: [["number", 5]] }, + proto: "Array", + maxLength: 1 + }) + attest(t([])).equals([5]) + attest(t([1])).equals([1]) + attest(t([null]).toString()).snap( + "value at [0] must be a number (was null)" + ) + attest(t([1, 2]).toString()).snap("must be at most length 1 (was 2)") + }) + + it("string", () => { + const t = type(["string = 'foo'"]) + attest(t.t).type.toString.snap(`[Default]`) + attest(t.expression).snap('[string = "foo"]') + attest(t.json).snap({ + sequence: { defaultables: [["string", "foo"]] }, + proto: "Array", + maxLength: 1 + }) + attest(t([])).equals(["foo"]) + attest(t(["bar"])).snap(["bar"]) + attest(t([false]).toString()).snap( + "value at [0] must be a string (was boolean)" + ) + attest(t(["foo", "bar"]).toString()).snap( + "must be at most length 1 (was 2)" + ) + }) + + it("defaults following prefix", () => { + const t = type(["string", "number = 5"]) + attest(t.t).type.toString.snap("[string, Default]") + attest(t.expression).snap("[string, number = 5]") + attest(t.json).snap({ + sequence: { defaultables: [["number", 5]], prefix: ["string"] }, + proto: "Array", + maxLength: 2, + minLength: 1 + }) + attest(t([""])).equals(["", 5]) + attest(t(["", 7])).equals(["", 7]) + + attest(t([]).toString()).snap("must be non-empty") + attest(t(["foo", "bar"]).toString()).snap( + "value at [1] must be a number (was a string)" + ) + }) + + it("defaults preceding variadic", () => { + const t = type(["number", "string = 'foo'", "...", "number[]"]) + attest(t.t).type.toString.snap( + '[number, Default, ...number[]]' + ) + attest(t.expression).snap('[number, string = "foo", ...number[]]') + attest(t.json).snap({ + sequence: { + defaultables: [["string", "foo"]], + prefix: ["number"], + variadic: "number" + }, + proto: "Array", + minLength: 1 + }) + + attest(t([5])).equals([5, "foo"]) + attest(t([7, "bar"])).equals([7, "bar"]) + attest(t([8, "bar", 5])).equals([8, "bar", 5]) + + attest(t([]).toString()).snap("must be non-empty") + // all positional elements including optionals + // must be specified before variadic elements + attest(t([5, 5]).toString()).snap( + "value at [1] must be a string (was a number)" + ) + }) + + it("default after undefaulted optional", () => { + // @ts-expect-error + attest(() => type(["number?", "number = 5"])).throwsAndHasTypeError( + defaultablePostOptionalMessage + ) + }) +}) diff --git a/ark/type/__tests__/arrays/variadicTuple.test.ts b/ark/type/__tests__/arrays/variadicTuple.test.ts index e8c5f17fb7..3d1733fc25 100644 --- a/ark/type/__tests__/arrays/variadicTuple.test.ts +++ b/ark/type/__tests__/arrays/variadicTuple.test.ts @@ -2,8 +2,9 @@ import { attest, contextualize } from "@ark/attest" import { scope, type } from "arktype" import { multipleVariadicMesage, + optionalPostVariadicMessage, writeNonArraySpreadMessage -} from "arktype/internal/parser/tuple.ts" +} from "arktype/internal/parser/tupleLiteral.ts" contextualize(() => { it("spreads simple arrays", () => { @@ -76,13 +77,16 @@ contextualize(() => { it("errors on multiple variadic", () => { attest(() => - type([ - "...", - "string[]", - // @ts-expect-error - "...", - "number[]" - ]) + // @ts-expect-error + type(["...", "string[]", "...", "number[]"]) ).throwsAndHasTypeError(multipleVariadicMesage) }) + + it("error on optional post-variadic in spread", () => { + // no type error yet, ideally would have one if tuple + // parsing were more precise for nested spread tuples + attest(() => type(["...", "string[]", "...", ["string?"]])).throws( + optionalPostVariadicMessage + ) + }) }) diff --git a/ark/type/__tests__/brand.test.ts b/ark/type/__tests__/brand.test.ts index 11dbbcf62d..baecaff3c5 100644 --- a/ark/type/__tests__/brand.test.ts +++ b/ark/type/__tests__/brand.test.ts @@ -1,65 +1,73 @@ import { attest, contextualize } from "@ark/attest" +import type { Brand, Json } from "@ark/util" import { type } from "arktype" -import type { number, string } from "arktype/internal/attributes.ts" contextualize(() => { - it("chained", () => { + it("fluent", () => { const t = type("string").brand("foo") - attest(t.t).type.toString.snap('branded<"foo">') + attest(t.t).type.toString.snap('Brand') + attest>(t.infer) + attest(t.inferIn) // no effect at runtime attest(t.expression).equals("string") const out = t("moo") - attest | type.errors>(out) + attest | type.errors>(out) }) - it("string-embedded", () => { + it("string", () => { const t = type("number#cool") - attest(t.t).type.toString.snap('branded<"cool">') + attest(t.t).type.toString.snap('Brand') + attest>(t.infer) + attest(t.inferIn) attest(t.expression).equals("number") const out = t(5) - attest | type.errors>(out) + attest | type.errors>(out) }) - it("brandAttributes", () => { - const unbranded = type({ - age: "number.integer >= 0" + it("in object", () => { + const t = type({ + foo: "string#foo", + bar: "string.json.parse#json" }) - attest(unbranded.t).type.toString.snap( - "{ age: is & AtLeast<0>> }" - ) + attest(t.t).type.toString.snap(`{ + foo: Brand + bar: (In: string) => To> +}`) + attest<{ + foo: Brand + bar: Brand + }>(t.infer) + attest<{ + foo: string + bar: string + }>(t.inferIn) + }) - const out = unbranded({ age: 5 }) + it("in union", () => { + const t = type("string#foo | boolean") - attest< - | type.errors - | { - age: number - } - >(out).equals({ age: 5 }) + attest(t.t).type.toString.snap(`boolean | Brand`) + attest>(t.infer) + attest(t.inferIn) + }) - const branded = unbranded.brandAttributes() + it("from morph", () => { + const fluent = type("string.numeric.parse").brand("num") - attest(branded.t).type.toString.snap( - "{ age: brand & AtLeast<0>> }" + attest(fluent.t).type.toString.snap( + '(In: string) => To>' ) + attest>(fluent.infer) + attest(fluent.inferIn) - const brandedOut = branded({ age: 5 }) - - attest(brandedOut).type.toString.snap(` | ArkErrors - | { age: brand & AtLeast<0>> }`) - - const reunbranded = branded.unbrandAttributes() - - attest(reunbranded.t).type.toString.snap( - "{ age: is & AtLeast<0>> }" - ) + const string = type("string.numeric.parse#num") - attest() - attest(unbranded.json).equals(reunbranded.json) + attest(string.json).equals(fluent.json) + attest(string.t) }) }) diff --git a/ark/type/__tests__/cyclic.bench.ts b/ark/type/__tests__/cyclic.bench.ts index cc103e4aef..5aaa47686f 100644 --- a/ark/type/__tests__/cyclic.bench.ts +++ b/ark/type/__tests__/cyclic.bench.ts @@ -7,19 +7,19 @@ bench.baseline(() => type("never")) bench( "cyclic 10 intersection", () => scope(cyclic10).type("user&user2").infer -).types([50275, "instantiations"]) +).types([65007, "instantiations"]) bench("cyclic(10)", () => scope(cyclic10).export()).types([ - 6932, + 8765, "instantiations" ]) bench("cyclic(100)", () => scope(cyclic100).export()).types([ - 39419, + 61220, "instantiations" ]) bench("cyclic(500)", () => scope(cyclic500).export()).types([ - 178460, + 288940, "instantiations" ]) diff --git a/ark/type/__tests__/declared.test.ts b/ark/type/__tests__/declared.test.ts index d2272f9531..450cd347ba 100644 --- a/ark/type/__tests__/declared.test.ts +++ b/ark/type/__tests__/declared.test.ts @@ -1,6 +1,5 @@ import { attest, contextualize } from "@ark/attest" import { declare, type } from "arktype" -import type { string } from "arktype/internal/attributes.ts" contextualize(() => { it("shallow", () => { @@ -53,8 +52,8 @@ contextualize(() => { }) it("regexp", () => { - const t = declare>().type(/.*/) - attest>(t.t) + const t = declare().type(/.*/) + attest(t.t) attest(t.infer) }) diff --git a/ark/type/__tests__/defaults.test.ts b/ark/type/__tests__/defaults.test.ts index 767703c9d0..18963c3d3b 100644 --- a/ark/type/__tests__/defaults.test.ts +++ b/ark/type/__tests__/defaults.test.ts @@ -1,59 +1,46 @@ import { attest, contextualize } from "@ark/attest" import { registeredReference, - writeNonPrimitiveNonFunctionDefaultValueMessage, - writeUnassignableDefaultValueMessage + writeNonPrimitiveNonFunctionDefaultValueMessage } from "@ark/schema" import { deepClone } from "@ark/util" import { scope, type } from "arktype" -import type { - InferredDefault, - Out, - string -} from "arktype/internal/attributes.ts" -import type { Date } from "arktype/internal/keywords/constructors/Date.ts" +import type { Default, Out, To } from "arktype/internal/attributes.ts" +import { shallowDefaultableMessage } from "arktype/internal/parser/ast/validate.ts" +import { invalidDefaultableKeyKindMessage } from "arktype/internal/parser/property.ts" import { writeNonLiteralDefaultMessage } from "arktype/internal/parser/shift/operator/default.ts" contextualize(() => { describe("parsing and traversal", () => { it("base", () => { - const fn5 = () => 5 as const + const fnDefaultTo5 = () => 5 as const const o = type({ a: "string", foo: "number = 5", bar: ["number", "=", 5], - baz: ["number", "=", fn5] + baz: ["number", "=", fnDefaultTo5] }) - const fn5reg = registeredReference(fn5) - // ensure type ast displays is exactly as expected attest(o.t).type.toString.snap(`{ a: string - foo: defaultsTo<5> - bar: defaultsTo<5> - baz: defaultsTo<5> + foo: Default + bar: Default + baz: Default }`) - attest<{ a: string; foo?: number; bar?: number; baz?: number }>(o.inferIn) + attest<{ + a: string + foo?: number + bar?: number + baz?: number + }>(o.inferIn) attest<{ a: string; foo: number; bar: number; baz: number }>(o.infer) attest(o.json).snap({ required: [{ key: "a", value: "string" }], optional: [ - { - default: fn5reg, - key: "baz", - value: { domain: "number", meta: { default: fn5reg } } - }, - { - default: 5, - key: "bar", - value: { domain: "number", meta: { default: 5 } } - }, - { - default: 5, - key: "foo", - value: { domain: "number", meta: { default: 5 } } - } + { default: "$ark.fnDefaultTo5", key: "baz", value: "number" }, + { default: 5, key: "bar", value: "number" }, + { default: 5, key: "foo", value: "number" } ], domain: "object" }) @@ -71,57 +58,18 @@ contextualize(() => { ) }) - it("defined with wrong type", () => { - attest(() => - // @ts-expect-error - type({ foo: "string", bar: ["number", "=", "5"] }) - ) - .throws( - writeUnassignableDefaultValueMessage( - "must be a number (was a string)" - ) - ) - .type.errors( - "Type 'string' is not assignable to type 'DefaultFor'." - ) - attest(() => - // @ts-expect-error - type({ foo: "string", bar: ["number", "=", () => "5"] }) - ) - .throws( - writeUnassignableDefaultValueMessage( - "must be a number (was a string)" - ) - ) - .type.errors("Type 'string' is not assignable to type 'number'.") - }) - it("unions are defaultable", () => { - const t = type("boolean = false") - - attest(t.t).type.toString.snap("of>") - - attest(t.json).snap({ - branches: [{ unit: false }, { unit: true }], - meta: { default: false } - }) - const o = type({ - boo: t + boo: "boolean = false" }) - - attest(o).type.toString.snap( - "Type<{ boo: of> }, {}>" - ) + // this should not distribute to Default | Default + attest(o).type.toString.snap("Type<{ boo: Default }, {}>") attest(o.json).snap({ optional: [ { default: false, key: "boo", - value: { - branches: [{ unit: false }, { unit: true }], - meta: { default: false } - } + value: [{ unit: false }, { unit: true }] } ], domain: "object" @@ -141,7 +89,7 @@ contextualize(() => { attest<{ foo: string - bar: InferredDefault + bar: Default }>(types.stringDefault.t) attest(types.tupleDefault.t) @@ -152,7 +100,7 @@ contextualize(() => { { default: 5, key: "bar", - value: { domain: "number", meta: { default: 5 } } + value: "number" } ], domain: "object" @@ -161,16 +109,36 @@ contextualize(() => { attest(types.tupleDefault.json).equals(types.stringDefault.json) }) + it("no shallow default in tuple expression", () => { + attest(() => + // @ts-expect-error + type(["string = 'foo'", "|", "number"]) + ).throwsAndHasTypeError(shallowDefaultableMessage) + + attest(() => + // @ts-expect-error + type(["string", "|", ["number", "=", 5]]) + ).throwsAndHasTypeError(shallowDefaultableMessage) + }) + + it("no shallow default in scope", () => { + // @ts-expect-error + attest(() => type.module({ foo: "string = ''" })).throwsAndHasTypeError( + shallowDefaultableMessage + ) + + attest(() => + // @ts-expect-error + type.module({ foo: ["string", "=", ""] }) + ).throwsAndHasTypeError(shallowDefaultableMessage) + }) + it("chained", () => { const defaultedString = type("string").default("") - attest(defaultedString.t).type.toString.snap('defaultsTo<"">') - attest(defaultedString.json).snap({ - domain: "string", - meta: { default: "" } - }) + attest(defaultedString).type.toString.snap('[Type, "=", ""]') const o = type({ a: defaultedString }) - attest(o.t).type.toString.snap('{ a: defaultsTo<""> }') + attest(o.t).type.toString.snap('{ a: Default }') attest<{ a?: string }>(o.inferIn) attest<{ a: string }>(o.infer) attest(o.json).snap({ @@ -178,41 +146,50 @@ contextualize(() => { { default: "", key: "a", - value: { domain: "string", meta: { default: "" } } + value: "string" } ], domain: "object" }) }) - it("invalid chained", () => { - // @ts-expect-error - attest(() => type("number").default(true)) - .throws( - writeUnassignableDefaultValueMessage("must be a number (was boolean)") + it("unassignable default tuple", () => { + attest(() => + // @ts-expect-error + type({ foo: "string", bar: ["number", "=", "5"] }) + ) + .throws.snap( + "ParseError: Default for bar must be a number (was a string)" ) - .type.errors.snap( - "Argument of type 'boolean' is not assignable to parameter of type 'DefaultFor'." + .type.errors( + "Type 'string' is not assignable to type 'defaultFor'." ) }) - it("spread", () => { - const t = type("number", "=", 5) - - const expected = type(["number", "=", 5]) - attest(t) - attest(t.json).equals(expected.json) + it("unassignable default thunk tuple", () => { + attest(() => + type({ + foo: [ + { foo: "true" }, + "=", + () => ({ + // @ts-expect-error + foo: false + }) + ] + }) + ) + .throws.snap("ParseError: Default for foo.foo must be true (was false)") + .type.errors.snap("Type 'false' is not assignable to type 'true'.") }) - it("invalid spread", () => { + it("unassignable default string", () => { // @ts-expect-error - attest(() => type("number", "=", true)) - .throws( - writeUnassignableDefaultValueMessage("must be a number (was boolean)") - ) - .type.errors.snap( - "Argument of type 'boolean' is not assignable to parameter of type 'DefaultFor'." + attest(() => type({ foo: "number = true" })) + .throws.snap( + "ParseError: Default for foo must be a number (was boolean)" ) + .type.errors("Default value true must be assignable to number ") }) it("morphed", () => { @@ -224,8 +201,9 @@ contextualize(() => { }) attest<{ - bool_value: (In: string.defaultsTo<"off">) => Out + bool_value: (In: Default) => Out }>(processForm.t) + attest<{ // key should still be distilled as optional even inside a morph bool_value?: string @@ -245,26 +223,6 @@ contextualize(() => { ) }) - it("morphed from defaulted", () => { - const processForm = type({ - bool_value: type("string='off'").pipe(v => (v === "on" ? true : false)) - }) - - attest<{ - bool_value: (In: string.defaultsTo<"off">) => Out - }>(processForm.t) - - const out = processForm({}) - - attest(out).snap({ bool_value: false }) - - attest(processForm({ bool_value: "on" })).snap({ bool_value: true }) - - attest(processForm({ bool_value: true }).toString()).snap( - "bool_value must be a string (was boolean)" - ) - }) - it("primitive morph precomputed", () => { let callCount = 0 @@ -275,25 +233,28 @@ contextualize(() => { const toggleRef = registeredReference(toggle) - const defaultablePipedBoolean = type("boolean = false").pipe(toggle) - - attest(defaultablePipedBoolean.t).type.toString.snap( - "(In: of>) => Out" - ) - attest(defaultablePipedBoolean.json).snap({ - in: [{ unit: false }, { unit: true }], - morphs: [toggleRef], - meta: { default: false } - }) - const t = type({ - blep: defaultablePipedBoolean + blep: type("boolean").pipe(toggle).default(false) }) attest(t.t).type.toString.snap(`{ - blep: (In: of>) => Out + blep: (In: Default) => Out }`) + attest(t.json).snap({ + optional: [ + { + default: false, + key: "blep", + value: { + in: [{ unit: false }, { unit: true }], + morphs: [toggleRef] + } + } + ], + domain: "object" + }) + const out = t({}) attest(out).snap({ blep: true }) @@ -313,31 +274,28 @@ contextualize(() => { const toggleRef = registeredReference(toggle) - const defaultablePipedBoolean = type("boolean = false") - .pipe(toggle) - .to("boolean") - - attest(defaultablePipedBoolean.t).type.toString - .snap(` | ((In: of>) => To) - | ((In: of>) => To)`) - attest(defaultablePipedBoolean.json).snap({ - in: { - branches: [{ unit: false }, { unit: true }], - meta: { default: false } - }, - morphs: [toggleRef, [{ unit: false }, { unit: true }]] - }) - const t = type({ - blep: defaultablePipedBoolean + blep: type("boolean").pipe(toggle).to("boolean").default(false) }) attest(t.t).type.toString.snap(`{ - blep: - | ((In: of>) => To) - | ((In: of>) => To) + blep: (In: Default) => To }`) + attest(t.json).snap({ + optional: [ + { + default: false, + key: "blep", + value: { + in: [{ unit: false }, { unit: true }], + morphs: [toggleRef, [{ unit: false }, { unit: true }]] + } + } + ], + domain: "object" + }) + const out = t({}) attest(out).snap({ blep: true }) @@ -348,13 +306,13 @@ contextualize(() => { }) it("primitive morphed to object not premorphed", () => { - const toNestedString = type("string") - .default("foo") - .pipe(s => ({ nest: s })) - - const t = type({ foo: toNestedString }) + const t = type({ + foo: type("string") + .pipe(s => ({ nest: s })) + .default("foo") + }) attest<{ - foo: (In: string.defaultsTo<"foo">) => Out<{ + foo: (In: Default) => Out<{ nest: string }> }>(t.t) @@ -406,7 +364,7 @@ contextualize(() => { // we can't check expected here since the Date instance will not // have a narrowed literal type attest<{ - key: InferredDefault> + key: Default }>(t.t) }) @@ -459,10 +417,10 @@ contextualize(() => { it("incorrect default type", () => { // @ts-expect-error attest(() => type({ foo: "string", bar: "number = true" })) - .throws( - writeUnassignableDefaultValueMessage("must be a number (was boolean)") + .throws.snap( + "ParseError: Default for bar must be a number (was boolean)" ) - .type.errors("true is not assignable to number") + .type.errors("Default value true must be assignable to number") }) it("non-literal", () => { @@ -481,14 +439,16 @@ contextualize(() => { $.export() attest($.json).snap({ - specialNumber: { domain: "number" }, + specialNumber: { + domain: "number" + }, obj: { required: [{ key: "foo", value: "string" }], optional: [ { default: 5, key: "bar", - value: { domain: "number", meta: { default: 5 } } + value: "number" } ], domain: "object" @@ -497,26 +457,39 @@ contextualize(() => { }) it("optional with default", () => { - const t = type({ foo: "string", "bar?": "number = 5" }) - attest<{ - foo: string - bar?: number - }>(t.inferIn) - attest<{ - foo: string - bar?: number - }>(t.infer) + attest(() => + // @ts-expect-error + type({ foo: "string", "bar?": "number = 5" }) + ).throwsAndHasTypeError(invalidDefaultableKeyKindMessage) - const fromTuple = type({ foo: "string", "bar?": ["number", "=", 5] }) - attest(fromTuple.t) - attest(fromTuple.json).equals(t.json) + attest(() => + // @ts-expect-error + type({ foo: "string", "bar?": ["number", "=", 5] }) + ).throwsAndHasTypeError(invalidDefaultableKeyKindMessage) + }) + + it("index with default", () => { + attest(() => + // @ts-expect-error + type({ foo: "string", "[string]": "number = 5" }) + ).throwsAndHasTypeError(invalidDefaultableKeyKindMessage) + + attest(() => + // @ts-expect-error + type({ foo: "string", "[string]": ["number", "=", 5] }) + ).throwsAndHasTypeError(invalidDefaultableKeyKindMessage) }) it("shallow default", () => { - const t = type("string='foo'") - const expected = type("string").default("foo") - attest(t.t) - attest(t.json).equals(expected.json) + // @ts-expect-error + attest(() => type("string='foo'")).throwsAndHasTypeError( + shallowDefaultableMessage + ) + + // @ts-expect-error + attest(() => type(["string", "=", "foo"])).throwsAndHasTypeError( + shallowDefaultableMessage + ) }) it("extracts output as required", () => { @@ -526,7 +499,7 @@ contextualize(() => { attest<{ foo?: string }>(t.in.infer) attest<{ foo: string }>(t.out.infer) - attest(t.in.expression).snap('{ foo?: string = "foo" }') + attest(t.in.expression).snap('{ foo: string = "foo" }') attest(t.out.expression).snap("{ foo: string }") }) }) @@ -557,17 +530,18 @@ contextualize(() => { // @ts-expect-error type({ foo: ["unknown", "=", { foo: "bar" }] }) }) - .throws("is not primitive") + .throws(writeNonPrimitiveNonFunctionDefaultValueMessage("foo")) .type.errors("'foo' does not exist in type '() => unknown'.") + attest(() => { // @ts-expect-error type({ foo: ["unknown.any", "=", { foo: "bar" }] }) }) - .throws("is not primitive") + .throws(writeNonPrimitiveNonFunctionDefaultValueMessage("foo")) .type.errors("'foo' does not exist in type '() => any'.") }) - it("allows string sybtyping", () => { + it("allows string subtyping", () => { type({ foo: [/^foo/ as type.cast<`foo${string}`>, "=", "foobar"], bar: [/bar$/ as type.cast<`${string}bar`>, "=", () => "foobar" as const] @@ -579,15 +553,17 @@ contextualize(() => { // @ts-expect-error () => type({ foo: ["number", "=", true] }) ) - .throws() + .throws.snap( + "ParseError: Default for foo must be a number (was boolean)" + ) .type.errors.snap( - "Type 'boolean' is not assignable to type 'DefaultFor'." + "Type 'boolean' is not assignable to type 'defaultFor'." ) attest( // @ts-expect-error () => type({ foo: ["number[]", "=", true] }) ) - .throws() + .throws.snap("AggregateError: must be an array (was boolean)") .type.errors.snap( "Type 'boolean' is not assignable to type '() => number[]'." ) @@ -595,7 +571,9 @@ contextualize(() => { // @ts-expect-error () => type({ foo: [{ bar: "false" }, "=", true] }) ) - .throws() + .throws.snap( + "ParseError: Default for foo must be an object (was boolean)" + ) .type.errors.snap( "Type 'boolean' is not assignable to type '() => { bar: false; }'." ) @@ -603,33 +581,39 @@ contextualize(() => { // @ts-expect-error () => type({ foo: [["number[]", "|", "string"], "=", true] }) ) - .throws() + .throws.snap( + "AggregateError: must be a string or an array (was boolean)" + ) .type.errors.snap( - "Type 'boolean' is not assignable to type 'DefaultFor'." + "Type 'boolean' is not assignable to type 'defaultFor'." + ) + // @ts-expect-error + attest(() => type({ foo: [["number[]", "|", "string"], "=", true] })) + .throws.snap( + "AggregateError: must be a string or an array (was boolean)" ) - attest( - // @ts-expect-error - () => type(["number[]", "|", "string"], "=", true) - ) - .throws() .type.errors.snap( - "Argument of type 'boolean' is not assignable to parameter of type 'DefaultFor'." + "Type 'boolean' is not assignable to type 'defaultFor'." ) // should not cause "instantiation is excessively deep" attest( // @ts-expect-error () => type("number[]", "|", "string").default(true) ) - .throws() + .throws.snap( + "ParseError: Default must be a string or an object (was boolean)" + ) .type.errors.snap( - "Argument of type 'boolean' is not assignable to parameter of type 'DefaultFor'." + "Argument of type 'boolean' is not assignable to parameter of type 'defaultFor'." ) // should not cause "instantiation is excessively deep" attest( // @ts-expect-error () => type("number[]", "|", "string").default(() => true) ) - .throws() + .throws.snap( + "ParseError: Default must be a string or an object (was boolean)" + ) .type.errors( "Type 'boolean' is not assignable to type 'string | number[]'." ) @@ -640,13 +624,13 @@ contextualize(() => { attest(() => type({ foo: ["string.numeric.parse = true"] })) .throws("must be a string (was boolean)") .type.errors( - "Default value true is not assignable to string.numeric.parse" + "Default value true must be assignable to string.numeric.parse" ) // @ts-expect-error attest(() => type({ foo: ["string.numeric.parse", "=", true] })) .throws("must be a string (was boolean)") .type.errors( - "Type 'boolean' is not assignable to type 'DefaultFor'." + "Type 'boolean' is not assignable to type 'defaultFor'." ) // @ts-expect-error attest(() => type({ foo: ["string.numeric.parse", "=", () => true] })) @@ -657,7 +641,7 @@ contextualize(() => { attest(() => type({ foo: [numtos, "=", true] })) .throws("must be a number (was boolean)") .type.errors( - "Type 'boolean' is not assignable to type 'DefaultFor'." + "Type 'boolean' is not assignable to type 'defaultFor'." ) // @ts-expect-error attest(() => type({ foo: [numtos, "=", () => true] })) @@ -670,9 +654,7 @@ contextualize(() => { foo3: ["string.numeric.parse", "=", () => "123"], bar1: [numtos, "=", 123], bar2: [numtos, "=", () => 123], - baz1: type(numtos, "=", 123), - baz2: type(numtos, "=", () => 123), - baz3: type(numtos).default(123) + baz1: type(numtos).default(123) }) attest(f.assert({})).snap({ foo1: 123, @@ -680,50 +662,9 @@ contextualize(() => { foo3: 123, bar1: "123", bar2: "123", - baz1: "123", - baz2: "123", - baz3: "123" + baz1: "123" }) }) - - it("boolean not distributed during inference", () => { - const t = type("boolean", "=", false) - - attest(t.json).snap({ - branches: [{ unit: false }, { unit: true }], - meta: { default: false } - }) - - attest(t.t).type.toString.snap("of>") - }) - - it("union not distributed during inference with morph", () => { - const parseDateToFuture = (s: string) => { - const d = new Date(s) - d.setFullYear(d.getFullYear() + 100) - return d - } - - const narrowFutureInput = () => true - - const t = type("boolean | number", "=", false) - .or(["string", "=>", parseDateToFuture]) - .satisfying(narrowFutureInput) - - attest(t.json).snap([ - { domain: "number", predicate: ["$ark.narrowFutureInput"] }, - { - in: { domain: "string", predicate: ["$ark.narrowFutureInput"] }, - morphs: ["$ark.parseDateToFuture"] - }, - { unit: false }, - { unit: true } - ]) - - attest(t.t).type.toString - .snap(` | of & Anonymous> - | ((In: nominal<"?">) => Out)`) - }) }) describe("intersection", () => { @@ -744,9 +685,7 @@ contextualize(() => { const result = l.and(r) attest(result.json).snap({ - optional: [ - { default: 5, key: "bar", value: { unit: 5, meta: { default: 5 } } } - ], + optional: [{ default: 5, key: "bar", value: { unit: 5 } }], domain: "object" }) }) @@ -773,19 +712,9 @@ contextualize(() => { describe("functions", () => { it("works in tuple", () => { - const t = type({ foo: ["string", "=", () => "bar"] }) - attest(t.assert({ foo: "bar" })).snap({ foo: "bar" }) - }) + const t = type({ foo: ["string", "=", () => "bar" as const] }) - it("works in type tuple", () => { - const foo = type(["string", "=", () => "bar"]) - const t = type({ foo }) - attest(t.assert({ foo: "bar" })).snap({ foo: "bar" }) - }) - - it("works in type args", () => { - const foo = type("string", "=", () => "bar") - const t = type({ foo }) + attest(t.t).type.toString.snap('{ foo: Default }') attest(t.assert({ foo: "bar" })).snap({ foo: "bar" }) }) @@ -793,25 +722,37 @@ contextualize(() => { attest(() => { // @ts-expect-error type({ foo: ["number", "=", () => "bar"] }) - }).throws.snap( - "ParseError: Default value is not assignable: must be a number (was a string)" - ) + }) + .throws.snap( + "ParseError: Default for foo must be a number (was a string)" + ) + .type.errors("Type 'string' is not assignable to type 'number'.") + attest(() => { // @ts-expect-error type({ foo: ["number[]", "=", () => "bar"] }) - }).throws.snap( - "ParseError: Default value is not assignable: must be an array (was string)" - ) + }) + .throws.snap( + "ParseError: Default for foo must be an array (was string)" + ) + .type.errors.snap("Type 'string' is not assignable to type 'number[]'.") + attest(() => { // @ts-expect-error type({ foo: [{ a: "number" }, "=", () => ({ a: "bar" })] }) - }).throws.snap( - "ParseError: Default value is not assignable: a must be a number (was a string)" - ) + }) + .throws.snap( + "ParseError: Default for foo.a must be a number (was a string)" + ) + .type.errors.snap("Type 'string' is not assignable to type 'number'.") }) it("morphs the returned value", () => { const t = type({ foo: ["string.numeric.parse", "=", () => "123"] }) + + attest<{ + foo: (In: Default) => To + }>(t.t) attest(t.assert({})).snap({ foo: 123 }) }) @@ -830,7 +771,7 @@ contextualize(() => { // @ts-expect-error type({ bar: ["number", "=", (a: number) => a] }) }).type.errors( - "Type '(a: number) => number' is not assignable to type 'DefaultFor'" + "Type '(a: number) => number' is not assignable to type 'defaultFor'" ) }) @@ -844,12 +785,15 @@ contextualize(() => { it("default function factory", () => { let i = 0 const t = type({ - // this requires explicit type argument - bar: type("Function").default<() => number>(() => { + bar: type("Function").default(() => { const j = ++i return () => j }) }) + + attest<{ + bar: Default number> + }>(t.t) attest(t.assert({}).bar()).snap(3) attest(t.assert({}).bar()).snap(4) }) @@ -859,8 +803,8 @@ contextualize(() => { const t = type({ foo: [["number", "|", "number[]"], "=", () => (i % 2 ? ++i : [++i])] }) + attest(t.assert({})).snap({ foo: 2 }) attest(t.assert({})).snap({ foo: [3] }) - attest(t.assert({})).snap({ foo: 4 }) }) it("default array", () => { @@ -875,15 +819,15 @@ contextualize(() => { attest(v1).snap({ foo: [1], bar: ["1"] }) attest(v1.foo !== v2.foo) }) + it("default array is checked", () => { attest(() => { // @ts-expect-error type({ bar: type("number[]").default(() => ["a"]) }) - }).throws( - writeUnassignableDefaultValueMessage( - "value at [0] must be a number (was a string)" - ) + }).throws.snap( + "ParseError: Default value at [0] must be a number (was a string)" ) + attest(() => { type({ baz: type("number[]") @@ -891,20 +835,21 @@ contextualize(() => { // @ts-expect-error .default(() => ["a"]) }) - }).throws( - writeUnassignableDefaultValueMessage( - "value at [0] must be a number (was a string)" - ) + }).throws.snap( + "ParseError: Default value at [0] must be a number (was a string)" ) }) + it("default object", () => { const t = type({ foo: type({ "foo?": "string" }).default(() => ({})), bar: type({ "foo?": "string" }).default(() => ({ foo: "foostr" })), baz: type({ foo: "string = 'foostr'" }).default(() => ({})) }) + const v1 = t.assert({}), v2 = t.assert({}) + attest(v1).snap({ foo: {}, bar: { foo: "foostr" }, @@ -917,43 +862,14 @@ contextualize(() => { attest(() => { // @ts-expect-error type({ foo: type({ foo: "string" }).default({}) }) - }).throws(writeNonPrimitiveNonFunctionDefaultValueMessage("")) + }).throws(writeNonPrimitiveNonFunctionDefaultValueMessage(null)) + attest(() => { type({ // @ts-expect-error bar: type({ foo: "number" }).default(() => ({ foo: "foostr" })) }) - }).throws( - writeUnassignableDefaultValueMessage( - "foo must be a number (was a string)" - ) - ) - }) - - it("default allows nested default keys", () => { - const a = type(["string.numeric.parse", "=", "1"]) - - attest(a).type.toString.snap(`Type< - (In: is & Default<"1">>) => To, - {} ->`) - - const defaulted = type({ a }).default(() => ({})) - - attest(defaulted.expression).snap( - '{ a?: (In: string /^(?:(?!^-0\\.?0*$)(?:-?(?:(?:0|[1-9]\\d*)(?:\\.\\d+)?)?))$/) => Out = "1" }' - ) - attest(defaulted).type.toString.snap(`Type< - of< - { - a: ( - In: is & Default<"1">> - ) => To - }, - Default<{}> - >, - {} ->`) + }).throws.snap("ParseError: Default foo must be a number (was a string)") }) }) }) diff --git a/ark/type/__tests__/generic.test.ts b/ark/type/__tests__/generic.test.ts index fc68beac3c..a16b4ce49e 100644 --- a/ark/type/__tests__/generic.test.ts +++ b/ark/type/__tests__/generic.test.ts @@ -152,18 +152,9 @@ contextualize(() => { attest(t.t) attest(t.expression).equals(expected.expression) - // @ts-expect-error - attest(() => positiveToInteger("number")) - .throws( - writeUnsatisfiedParameterConstraintMessage( - "n", - "number > 0", - "number" - ) - ) - .type.errors( - `ErrorType<"Invalid argument for n", [expected: moreThan<0>]>` - ) + attest(() => positiveToInteger("number")).throws( + writeUnsatisfiedParameterConstraintMessage("n", "number > 0", "number") + ) }) it("unsatisfied parameter string", () => { diff --git a/ark/type/__tests__/get.test.ts b/ark/type/__tests__/get.test.ts index 5e469db260..f736635d35 100644 --- a/ark/type/__tests__/get.test.ts +++ b/ark/type/__tests__/get.test.ts @@ -1,8 +1,6 @@ import { attest, contextualize } from "@ark/attest" import { writeInvalidKeysMessage, writeNumberIndexMessage } from "@ark/schema" import { keywords, type } from "arktype" -import type { of, string } from "arktype/internal/attributes.ts" -import type { Matching } from "arktype/internal/keywords/string/string.ts" contextualize(() => { it("can get shallow roots by path", () => { @@ -62,11 +60,10 @@ contextualize(() => { named: "1" }) - const a = t.get("foo" as string & string.matching<"^f">) + const a = t.get("foo" as string) attest<0>(a.t) attest(a.expression).snap("undefined | 0") - // @ts-expect-error attest(() => t.get("bar")).throws( writeInvalidKeysMessage(t.expression, ["bar"]) ) @@ -81,34 +78,29 @@ contextualize(() => { foof: { c: "1" } }) - const a = t.get("foo" as string.matching<"^f">) + const a = t.get("foo") - attest<{ a: 1 }>(a.infer) + attest<{ a: 1 } | { b: 1 }>(a.infer) attest(a.expression).snap("{ a: 1 } | undefined") - const b = t.get("oof" as string.matching<"f$">) - attest<{ b: 1 }>(b.infer) + const b = t.get("oof") + attest<{ a: 1 } | { b: 1 }>(b.infer) attest(b.expression).snap("{ b: 1 } | undefined") - const c = t.get("fof" as string.matching<"^f"> & string.matching<"f$">) - attest<{ - a: 1 - b: 1 - }>(c.infer) + const c = t.get("fof" as string) + attest< + | { + a: 1 + } + | { b: 1 } + >(c.infer) attest(c.expression).snap("{ a: 1, b: 1 } | undefined") - const d = t.get("foof" as of<"foof", Matching<"^f"> & Matching<"f$">>) - // should include { c: 1 } as well but it seems TS can't infer it for now - attest< - { - a: 1 - } & { - b: 1 - } - >(d.infer) + const d = t.get("foof") + + attest<{ c: 1 }>(d.infer) attest(d.expression).snap("{ a: 1, b: 1, c: 1 }") - // @ts-expect-error attest(() => t.get("goog").expression).throws( writeInvalidKeysMessage(t.expression, ["goog"]) ) diff --git a/ark/type/__tests__/imports.test.ts b/ark/type/__tests__/imports.test.ts index fb049c06c7..5692cc5bca 100644 --- a/ark/type/__tests__/imports.test.ts +++ b/ark/type/__tests__/imports.test.ts @@ -76,7 +76,7 @@ contextualize(() => { // have to snapshot the module since TypeScript treats it as bivariant attest(types).type.toString.snap(`Module<{ - public: true | 3 | uuid | "no" + public: string | true | 3 hasCrept: true }>`) }) diff --git a/ark/type/__tests__/instanceof.test.ts b/ark/type/__tests__/instanceof.test.ts index 3e34d25c1b..45f3937b19 100644 --- a/ark/type/__tests__/instanceof.test.ts +++ b/ark/type/__tests__/instanceof.test.ts @@ -1,7 +1,7 @@ import { attest, contextualize } from "@ark/attest" import { rootSchema } from "@ark/schema" import { type } from "arktype" -import { writeInvalidConstructorMessage } from "arktype/internal/parser/tuple.ts" +import { writeInvalidConstructorMessage } from "arktype/internal/parser/tupleExpressions.ts" contextualize(() => { describe("tuple expression", () => { diff --git a/ark/type/__tests__/keywords/formData.test.ts b/ark/type/__tests__/keywords/formData.test.ts index 1923fc7f8f..7622ea7f3b 100644 --- a/ark/type/__tests__/keywords/formData.test.ts +++ b/ark/type/__tests__/keywords/formData.test.ts @@ -16,7 +16,7 @@ contextualize(() => { ( In: FormData ) => To<{ - email: email + email: string file: File tags: (In: string | string[]) => To }>, diff --git a/ark/type/__tests__/keywords/json.test.ts b/ark/type/__tests__/keywords/json.test.ts index 2691dd8d84..5d6fc0308a 100644 --- a/ark/type/__tests__/keywords/json.test.ts +++ b/ark/type/__tests__/keywords/json.test.ts @@ -1,6 +1,6 @@ import { attest, contextualize } from "@ark/attest" import { type } from "arktype" -import { writeJsonSyntaxErrorProblem } from "arktype/internal/keywords/string/json.ts" +import { writeJsonSyntaxErrorProblem } from "arktype/internal/keywords/string.ts" contextualize(() => { let syntaxError: unknown @@ -24,9 +24,10 @@ contextualize(() => { it("string.json.parse", () => { const parseJson = type("string.json.parse") + attest(parseJson('{"a": "hello"}')).snap({ a: "hello" }) - attest(parseJson(123).toString()).snap("must be a string (was a number)") + attest(parseJson(123)?.toString()).snap("must be a string (was a number)") - attest(parseJson("{").toString()).equals(expectedSyntaxErrorProblem) + attest(parseJson("{")?.toString()).equals(expectedSyntaxErrorProblem) }) }) diff --git a/ark/type/__tests__/keywords/object.test.ts b/ark/type/__tests__/keywords/object.test.ts index 6f8d304057..d0af6d39c2 100644 --- a/ark/type/__tests__/keywords/object.test.ts +++ b/ark/type/__tests__/keywords/object.test.ts @@ -24,8 +24,8 @@ contextualize(() => { attest(json({})).equals({}) attest(json([])).equals([]) - attest(json(5).toString()).snap("must be an object or an array (was 5)") - attest(json({ foo: [5n] }).toString()).snap( + attest(json(5)?.toString()).snap("must be an object or an array (was 5)") + attest(json({ foo: [5n] })?.toString()).snap( "foo[0] must be an object (was a bigint) or must be an array (was object)" ) }) diff --git a/ark/type/__tests__/keywords/url.test.ts b/ark/type/__tests__/keywords/url.test.ts index e3b8181847..48df18d338 100644 --- a/ark/type/__tests__/keywords/url.test.ts +++ b/ark/type/__tests__/keywords/url.test.ts @@ -5,7 +5,7 @@ contextualize(() => { it("root", () => { const url = type("string.url") - attest(url).type.toString.snap("Type") + attest(url).type.toString.snap("Type") attest(url("https://arktype.io")).snap("https://arktype.io") attest(url("arktype").toString()).snap( @@ -16,7 +16,7 @@ contextualize(() => { it("parse", () => { const parseUrl = type("string.url.parse") - attest(parseUrl).type.toString.snap("Type<(In: url) => To, {}>") + attest(parseUrl).type.toString.snap("Type<(In: string) => To, {}>") attest(parseUrl("https://arktype.io")).instanceOf(URL) attest(parseUrl("arktype").toString()).snap( 'must be a URL string (was "arktype")' diff --git a/ark/type/__tests__/narrow.test.ts b/ark/type/__tests__/narrow.test.ts index 46acd145da..713eba68ee 100644 --- a/ark/type/__tests__/narrow.test.ts +++ b/ark/type/__tests__/narrow.test.ts @@ -2,13 +2,7 @@ import { attest, contextualize } from "@ark/attest" import { registeredReference } from "@ark/schema" import type { equals } from "@ark/util" import { type } from "arktype" -import type { - Anonymous, - Out, - number, - of, - string -} from "arktype/internal/attributes.ts" +import type { Out } from "arktype/internal/attributes.ts" contextualize(() => { it("implicit problem", () => { @@ -48,7 +42,7 @@ contextualize(() => { (n, ctx) => n % 5 === 0 || ctx.reject("divisible by 5") ) - attest(divisibleBy30.t) + attest(divisibleBy30.t) attest(divisibleBy30(1).toString()).snap("must be divisible by 2 (was 1)") attest(divisibleBy30(2).toString()).snap("must be divisible by 3 (was 2)") @@ -72,7 +66,7 @@ contextualize(() => { } ]) - attest>(abEqual.t) + attest<{ a: number; b: number }>(abEqual.t) attest<{ a: number b: number @@ -110,7 +104,7 @@ contextualize(() => { (s, ctx) => s === [...s].reverse().join("") ? true : ctx.reject("a palindrome") ]) - attest(palindrome.t) + attest(palindrome.t) attest(palindrome("dad")).snap("dad") attest(palindrome("david").toString()).snap( 'must be a palindrome (was "david")' @@ -164,7 +158,7 @@ contextualize(() => { const A = type("bigint").narrow(predicate).pipe(toString) - attest<(In: of) => Out>(A.t) + attest<(In: bigint) => Out>(A.t) attest(A.in.infer) attest(A.inferIn) attest(A.infer) @@ -214,13 +208,13 @@ contextualize(() => { attest<{ foo: number }>(object.in.infer) const nested = type({ foo: ["number.integer", "=>", n => n++] }) - attest(nested.t).type.toString.snap("{ foo: (In: integer) => Out }") + attest(nested.t).type.toString.snap("{ foo: (In: number) => Out }") attest<{ foo: number }>(nested.inferIn) attest<{ foo: number }>(nested.in.infer) const map = type.keywords.Map.narrow(() => true).pipe(m => m) attest(map.t).type.toString.snap(`( - In: of, Anonymous> + In: Map ) => Out>`) attest(map.infer).type.toString.snap("Map") attest(map.inferIn).type.toString("Map") @@ -251,7 +245,7 @@ contextualize(() => { it("can distill units", () => { const t = type("5").narrow(() => true) - attest>(t.t) + attest<5>(t.t) attest<5>(t.infer) attest<5>(t.inferIn) @@ -260,10 +254,9 @@ contextualize(() => { }) it("unknown is narrowable", () => { - const t = type("unknown").narrow(() => true) - attest(t.t).type.toString.snap(`{ - " attributes": { base: unknown; attributes: Anonymous } -}`) - attest(t.expression).snap("unknown") + const unknownPredicate854 = () => true + const t = type("unknown").narrow(unknownPredicate854) + attest(t.t).type.toString.snap("unknown") + attest(t.json).snap({ predicate: ["$ark.unknownPredicate854"] }) }) }) diff --git a/ark/type/__tests__/object.bench.ts b/ark/type/__tests__/object.bench.ts index 003b69f26d..054f39b02a 100644 --- a/ark/type/__tests__/object.bench.ts +++ b/ark/type/__tests__/object.bench.ts @@ -16,7 +16,7 @@ bench("object literal", () => b: "number[]", c: { nested: "boolean[]" } }) -).types([2305, "instantiations"]) +).types([2443, "instantiations"]) bench("object literal with optional keys", () => type({ @@ -24,10 +24,10 @@ bench("object literal with optional keys", () => "b?": "number[]", "c?": { "nested?": "boolean[]" } }) -).types([2220, "instantiations"]) +).types([2308, "instantiations"]) bench("tuple", () => type(["string[]", "number[]", ["boolean[]"]])).types([ - 3260, + 3028, "instantiations" ]) @@ -35,21 +35,21 @@ bench("inline definition", () => type({ a: "string" }) -).types([873, "instantiations"]) +).types([948, "instantiations"]) bench("referenced type", () => { const a = type("string") return type({ a }) -}).types([984, "instantiations"]) +}).types([1070, "instantiations"]) // https://github.com/arktypeio/arktype/issues/787 bench("inline reference", () => type({ a: type("string") }) -).types([1210, "instantiations"]) +).types([1296, "instantiations"]) bench("nested type invocations", () => type({ @@ -71,4 +71,4 @@ bench("nested type invocations", () => }) .array() }) -).types([21572, "instantiations"]) +).types([13449, "instantiations"]) diff --git a/ark/type/__tests__/objects/mapped.test.ts b/ark/type/__tests__/objects/mapped.test.ts index 43e87352ab..41325016f0 100644 --- a/ark/type/__tests__/objects/mapped.test.ts +++ b/ark/type/__tests__/objects/mapped.test.ts @@ -1,6 +1,6 @@ import { attest, contextualize } from "@ark/attest" import { type } from "arktype" -import type { InferredDefault } from "arktype/internal/attributes.ts" +import type { Default } from "arktype/internal/attributes.ts" contextualize(() => { it("identity", () => { @@ -132,10 +132,10 @@ contextualize(() => { }) attest<{ - foo: InferredDefault + foo: Default bar?: number }>(original.t) - attest(original.expression).snap('{ foo?: string = "foo", bar?: number }') + attest(original.expression).snap('{ foo: string = "foo", bar?: number }') const t = original.map(prop => { if (prop.key === "foo") { @@ -149,8 +149,8 @@ contextualize(() => { attest<{ bar?: number - foo: InferredDefault + foo: Default }>(t.t) - attest(t.expression).snap('{ foo?: string = "foot", bar?: number }') + attest(t.expression).snap('{ foo: string = "foot", bar?: number }') }) }) diff --git a/ark/type/__tests__/objects/namedKeys.test.ts b/ark/type/__tests__/objects/namedKeys.test.ts index deedbd780f..e097bf1da3 100644 --- a/ark/type/__tests__/objects/namedKeys.test.ts +++ b/ark/type/__tests__/objects/namedKeys.test.ts @@ -1,7 +1,7 @@ import { attest, contextualize } from "@ark/attest" import { registeredReference, writeUnresolvableMessage } from "@ark/schema" -import { type } from "arktype" -import type { Out, string } from "arktype/internal/attributes.ts" +import { type, type Type } from "arktype" +import type { Out } from "arktype/internal/attributes.ts" contextualize(() => { it("empty", () => { @@ -34,20 +34,19 @@ contextualize(() => { it("chained optional", () => { const optionalString = type("string").optional() - attest(optionalString.t) - attest(optionalString.infer) + attest<[Type, "?"]>(optionalString) const o = type({ a: optionalString }) // directly inferring the optional key causes recursive generics/intersections to fail, // so instead we just distill it out like defaults - attest(o.t).type.toString.snap("{ a: optional }") + attest(o.t).type.toString.snap("{ a?: string }") attest(o.infer).type.toString.snap("{ a?: string }") attest(o.inferIn).type.toString.snap("{ a?: string }") attest(o.json).snap({ optional: [ { key: "a", - value: { domain: "string", meta: { optional: true } } + value: "string" } ], domain: "object" @@ -60,7 +59,7 @@ contextualize(() => { const t = type({ [s]: "string?" }) attest<{ - [s]: string.optional + [s]?: string }>(t.t) attest<{ [s]?: string }>(t.infer) @@ -68,7 +67,7 @@ contextualize(() => { optional: [ { key: ref, - value: { domain: "string", meta: { optional: true } } + value: "string" } ], domain: "object" @@ -88,8 +87,7 @@ contextualize(() => { key: ref, value: { required: [{ key: "foo", value: "string" }], - domain: "object", - meta: { optional: true } + domain: "object" } } ], @@ -203,7 +201,7 @@ contextualize(() => { optional: [ { key: keyReference, - value: { domain: "number", meta: { optional: true } } + value: "number" } ], domain: "object" @@ -218,7 +216,7 @@ contextualize(() => { }) attest<{ - bool_value: (In: string.optional) => Out + bool_value?: (In: string) => Out }>(processForm.t) attest<{ // key should still be distilled as optional even inside a morph diff --git a/ark/type/__tests__/operand.bench.ts b/ark/type/__tests__/operand.bench.ts index 57009394ad..9ddf864787 100644 --- a/ark/type/__tests__/operand.bench.ts +++ b/ark/type/__tests__/operand.bench.ts @@ -4,27 +4,27 @@ import { type } from "arktype" bench.baseline(() => type("never")) bench("single-quoted", () => type("'nineteen characters'")).types([ - 708, + 709, "instantiations" ]) bench("double-quoted", () => type('"nineteen characters"')).types([ - 708, + 709, "instantiations" ]) bench("regex literal", () => type("/nineteen characters/")).types([ - 720, + 699, "instantiations" ]) -bench("keyword", () => type("string")).types([534, "instantiations"]) +bench("keyword", () => type("string")).types([547, "instantiations"]) -bench("number", () => type("-98765.4321")).types([485, "instantiations"]) +bench("number", () => type("-98765.4321")).types([498, "instantiations"]) -bench("bigint", () => type("-987654321n")).types([564, "instantiations"]) +bench("bigint", () => type("-987654321n")).types([577, "instantiations"]) -bench("object", () => type({ foo: "string" })).types([1226, "instantiations"]) +bench("object", () => type({ foo: "string" })).types([1242, "instantiations"]) bench("union", () => // Union is automatically discriminated using shallow or deep keys @@ -39,4 +39,4 @@ bench("union", () => .or({ kind: "'pleb'" }) -).types([5723, "instantiations"]) +).types([5400, "instantiations"]) diff --git a/ark/type/__tests__/operator.bench.ts b/ark/type/__tests__/operator.bench.ts index cf08130450..600e032086 100644 --- a/ark/type/__tests__/operator.bench.ts +++ b/ark/type/__tests__/operator.bench.ts @@ -9,50 +9,50 @@ bench.baseline(() => { type("symbol").narrow(() => true) }) -bench("array-string", () => type("number[]")).types([931, "instantiations"]) +bench("array-string", () => type("number[]")).types([939, "instantiations"]) bench("array-tuple", () => type(["number", "[]"])).types([ - 920, + 898, "instantiations" ]) bench("array-chain", () => type("number").array()).types([ - 489, + 502, "instantiations" ]) bench("union-string", () => type("number|string")).types([ - 1145, + 1157, "instantiations" ]) bench("union-tuple", () => type(["number", "|", "string"])).types([ - 1020, + 1096, "instantiations" ]) bench("union-chain", () => type("number").or("string")).types([ - 1716, + 1502, "instantiations" ]) bench("union-10-ary", () => type("0|1|2|3|4|5|6|7|8|9")).types([ - 4274, + 4258, "instantiations" ]) bench("intersection-string", () => type("number&0")).types([ - 1080, + 1328, "instantiations" ]) bench("intersection-tuple", () => type(["number", "&", "0"])).types([ - 954, + 1274, "instantiations" ]) bench("intersection-chain", () => type("number").and("0")).types([ - 1677, + 1728, "instantiations" ]) @@ -60,48 +60,48 @@ bench("intersection-10-ary", () => type( "unknown&unknown&unknown&unknown&unknown&unknown&unknown&unknown&unknown&unknown" ) -).types([5141, "instantiations"]) +).types([5153, "instantiations"]) bench("group-shallow", () => type("string|(number[])")).types([ - 1466, + 1473, "instantiations" ]) bench("group-nested", () => type("string|(number|(boolean))[][]")).types([ - 2216, + 2214, "instantiations" ]) bench("group-deep", () => type("(0|(1|(2|(3|(4|5)[])[])[])[])[]")).types([ - 7256, + 7383, "instantiations" ]) -bench("bound-single", () => type("string>5")).types([1679, "instantiations"]) +bench("bound-single", () => type("string>5")).types([1480, "instantiations"]) bench("bound-double", () => type("-7<=string.integer<99")).types([ - 2642, + 2253, "instantiations" ]) -bench("divisor", () => type("number%5")).types([1254, "instantiations"]) +bench("divisor", () => type("number%5")).types([1074, "instantiations"]) bench("filter-tuple", () => type(["boolean", ":", b => b])).types([ - 1432, + 1338, "instantiations" ]) bench("filter-chain", () => type("boolean").narrow(b => b)).types([ - 1015, + 738, "instantiations" ]) bench("morph-tuple", () => type(["boolean", "=>", b => b])).types([ - 1357, + 1440, "instantiations" ]) bench("morph-chain", () => type("boolean").pipe(b => b)).types([ - 1008, + 944, "instantiations" ]) diff --git a/ark/type/__tests__/optional.test.ts b/ark/type/__tests__/optional.test.ts new file mode 100644 index 0000000000..db860a648a --- /dev/null +++ b/ark/type/__tests__/optional.test.ts @@ -0,0 +1,29 @@ +import { attest, contextualize } from "@ark/attest" +import { type } from "arktype" +import { shallowOptionalMessage } from "arktype/internal/parser/ast/validate.ts" + +contextualize(() => { + it("no shallow default in tuple expression", () => { + attest(() => + // @ts-expect-error + type(["string?", "|", "number"]) + ).throwsAndHasTypeError(shallowOptionalMessage) + + attest(() => + // @ts-expect-error + type(["string", "|", ["number", "?"]]) + ).throwsAndHasTypeError(shallowOptionalMessage) + }) + + it("no shallow default in scope", () => { + // @ts-expect-error + attest(() => type.module({ foo: "string?" })).throwsAndHasTypeError( + shallowOptionalMessage + ) + + // @ts-expect-error + attest(() => type.module({ foo: ["string", "?"] })).throwsAndHasTypeError( + shallowOptionalMessage + ) + }) +}) diff --git a/ark/type/__tests__/pipe.test.ts b/ark/type/__tests__/pipe.test.ts index 3fe82e0429..9519df8021 100644 --- a/ark/type/__tests__/pipe.test.ts +++ b/ark/type/__tests__/pipe.test.ts @@ -8,7 +8,7 @@ import { type ArkErrors } from "@ark/schema" import { keywords, scope, type, type Type } from "arktype" -import type { MoreThan, Out, To, of } from "arktype/internal/attributes.ts" +import type { Out, To } from "arktype/internal/attributes.ts" contextualize(() => { it("base", () => { @@ -56,14 +56,7 @@ contextualize(() => { restringifyUser ]) - attest(t.t).type.toString.snap(`( - In: string & { - " attributes": { - base: string - attributes: Nominal<"json"> - } - } -) => Out`) + attest(t.t).type.toString.snap("(In: string) => Out") attest(t.infer) attest(t.json).snap({ @@ -378,9 +371,9 @@ contextualize(() => { attest(types.c).type.toString.snap(`Type< (In: { a: 1; b: 2 }) => Out, { + b: { b: 2 } a: (In: { a: 1 }) => Out c: (In: { a: 1; b: 2 }) => Out - b: { b: 2 } } >`) assertNodeKind(types.c.internal, "morph") @@ -613,7 +606,7 @@ contextualize(() => { b: { a: "1" }, c: "a&b" }).export() - attest<{ a: (In: of<1, MoreThan<0>>) => Out }>(types.c.t) + attest<{ a: (In: 1) => Out }>(types.c.t) const { serializedMorphs } = types.a.internal.firstReferenceOfKindOrThrow("morph") @@ -788,8 +781,8 @@ contextualize(() => { }) attest(t).type.toString.snap(`Type< - | { l: 1; n: (In: numeric) => To } - | { r: 1; n: (In: numeric) => To }, + | { l: 1; n: (In: string) => To } + | { r: 1; n: (In: string) => To }, {} >`) @@ -820,7 +813,7 @@ Right: { foo: (In: string) => Out<{ [string]: $jsonObject | number | string | $j it("multiple chained pipes", () => { const t = type("string.trim").to("string.lower") - attest(t.t).type.toString.snap("(In: string) => To") + attest(t.t).type.toString.snap("(In: string) => To") attest(t("Success")).equals("success") attest(t("success")).equals("success") diff --git a/ark/type/__tests__/range.test.ts b/ark/type/__tests__/range.test.ts index 3c2dcf11b5..d9e00aa5fd 100644 --- a/ark/type/__tests__/range.test.ts +++ b/ark/type/__tests__/range.test.ts @@ -21,7 +21,7 @@ contextualize(() => { it(">", () => { const t = type("number>0") attest(t.infer) - attest(t).type.toString.snap("Type, {}>") + attest(t).type.toString.snap("Type") attest(t.json).snap({ domain: "number", min: { exclusive: true, rule: 0 } @@ -31,7 +31,7 @@ contextualize(() => { it("<", () => { const t = type("number<10") attest(t.infer) - attest(t).type.toString.snap("Type, {}>") + attest(t).type.toString.snap("Type") const expected = rootSchema({ domain: "number", max: { rule: 10, exclusive: true } @@ -42,7 +42,7 @@ contextualize(() => { it("<=", () => { const t = type("number<=-49") attest(t.infer) - attest(t).type.toString.snap("Type, {}>") + attest(t).type.toString.snap("Type") const expected = rootSchema({ domain: "number", max: { rule: -49, exclusive: false } @@ -52,8 +52,8 @@ contextualize(() => { it("==", () => { const t = type("number==3211993") - attest<3211993>(t.infer) - attest(t).type.toString.snap("Type<3211993, {}>") + attest(t.infer) + attest(t).type.toString.snap("Type") const expected = rootSchema({ unit: 3211993 }) attest(t.json).equals(expected.json) }) @@ -69,7 +69,7 @@ contextualize(() => { it("<,<=", () => { const t = type("-5 & MoreThan<-5>>, {}>") + attest(t).type.toString.snap("Type") attest(t.infer) const expected = rootSchema({ domain: "number", @@ -81,9 +81,7 @@ contextualize(() => { it("<=,<", () => { const t = type("-3.23<=number<4.654") - attest(t).type.toString.snap( - "Type & AtLeast<-3.23>>, {}>" - ) + attest(t).type.toString.snap("Type") attest(t.infer) const expected = rootSchema({ domain: "number", @@ -95,7 +93,7 @@ contextualize(() => { it("whitespace following comparator", () => { const t = type("number > 3") - attest(t).type.toString.snap("Type, {}>") + attest(t).type.toString.snap("Type") attest(t.infer) const expected = rootSchema({ domain: "number", @@ -107,14 +105,14 @@ contextualize(() => { it("single Date", () => { const t = type("Date(t.infer) - attest(t).type.toString.snap('Type, {}>') + attest(t).type.toString.snap("Type") attest(t.json).snap({ proto: "Date", before: "2023-01-12T04:59:59.999Z" }) }) it("Date equality", () => { const t = type("Date==d'2020-1-1'") attest(t.infer) - attest(t).type.toString.snap('Type, {}>') + 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) @@ -123,9 +121,7 @@ contextualize(() => { it("double Date", () => { const t = type("d'2001/10/10'< Date < d'2005/10/10'") attest(t.infer) - attest(t.t).type.toString.snap( - 'is & After<"2001/10/10">>' - ) + attest(t.t).type.toString.snap("Date") attest(t.json).snap({ proto: "Date", before: "2005-10-10T03:59:59.999Z", @@ -140,9 +136,7 @@ contextualize(() => { const now = new Date() const t = type(`d'2000'< Date <=d'${now.toISOString()}'`) attest(t.infer) - attest(t).type.toString.snap( - 'Type & After<"2000">>, {}>' - ) + attest(t).type.toString.snap("Type") 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) @@ -238,7 +232,7 @@ contextualize(() => { }) it("number", () => { - attest<-3.14159>(type("number==-3.14159").infer) + attest(type("number==-3.14159").infer) }) it("string", () => { diff --git a/ark/type/__tests__/realWorld.test.ts b/ark/type/__tests__/realWorld.test.ts index 142eeefd55..1363ef84d3 100644 --- a/ark/type/__tests__/realWorld.test.ts +++ b/ark/type/__tests__/realWorld.test.ts @@ -5,16 +5,7 @@ import { type ArkErrors } from "@ark/schema" import { scope, type, type Module } from "arktype" -import type { - Anonymous, - AtLeastLength, - AtMostLength, - Out, - To, - number, - of, - string -} from "arktype/internal/attributes.ts" +import type { Out, To } from "arktype/internal/attributes.ts" declare class TimeStub { declare readonly isoString: string @@ -349,9 +340,7 @@ nospace must be matched by ^\\S*$ (was "One space")`) }) attest< - | ((In: string) => To & AtMostLength<3>>>) - | null - | undefined, + ((In: string) => To) | null | undefined, typeof CreatePatientInput.t.first_name >() @@ -464,9 +453,9 @@ nospace must be matched by ^\\S*$ (was "One space")`) attest< Module<{ svgMap: { - [x: string.matching]: string + [x: string]: string } - svgPath: string.matching + svgPath: string }> >(test) attest(test.svgMap({ "./f.svg": "123", bar: 5 })).unknown.snap({ @@ -587,7 +576,7 @@ nospace must be matched by ^\\S*$ (was "One space")`) .narrow(() => true) .describe('This will "fail"') - attest(t.t) + attest(t.t) const serializedPredicate = t.internal.firstReferenceOfKindOrThrow("predicate").serializedPredicate @@ -623,7 +612,7 @@ nospace must be matched by ^\\S*$ (was "One space")`) .pipe(s => parseInt(s)) .narrow(() => true) - attest<(In: string) => Out>(t.t) + attest<(In: string) => Out>(t.t) const u = t.pipe( n => `${n}`, @@ -703,7 +692,7 @@ nospace must be matched by ^\\S*$ (was "One space")`) .pipe(parseBigint) .narrow(validatePositiveBigint) - attest<(In: string | number) => Out>>(Amount.t) + attest<(In: string | number) => Out>(Amount.t) attest(Amount.json).snap({ in: ["number", "string"], morphs: [morphReference, { predicate: [predicateReference] }] @@ -728,11 +717,9 @@ nospace must be matched by ^\\S*$ (was "One space")`) attest(t.expression).snap( "{ first_name?: (In: string) => Out= 1> }" ) - attest(t.t).type.toString.snap(`{ - first_name?: ( - In: string - ) => To & AtLeastLength<1>>> -}`) + attest(t.t).type.toString.snap( + "{ first_name?: (In: string) => To }" + ) }) it("cyclic narrow in scope", () => { @@ -759,25 +746,11 @@ nospace must be matched by ^\\S*$ (was "One space")`) root: "file|directory" }).resolve("root") - attest(root.t).type.toString.snap(` | { - type: "file" - name: is & MoreThanLength<0>> - } + attest(root.t).type.toString.snap(` | { type: "file"; name: string } | { type: "directory" - name: is & MoreThanLength<0>> - children: of< - ( - | { - type: "file" - name: is< - LessThanLength<255> & MoreThanLength<0> - > - } - | cyclic - )[], - Anonymous - > + name: string + children: ({ type: "file"; name: string } | cyclic)[] }`) }) @@ -1039,7 +1012,7 @@ nospace must be matched by ^\\S*$ (was "One space")`) }) attest(t.expression).snap( - '{ storeA: { [string]: string }, ext?: string = ".txt" } | { storeB: { foo: { [string]: string } }, ext?: string = ".txt" }' + '{ storeA: { [string]: string }, ext: string = ".txt" } | { storeB: { foo: { [string]: string } }, ext: string = ".txt" }' ) }) @@ -1056,8 +1029,6 @@ nospace must be matched by ^\\S*$ (was "One space")`) attest(feedbackSchema.expression).snap( "{ contact: string == 0 | string /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/ }" ) - attest(feedbackSchema.t).type.toString.snap( - `{ contact: email | is> }` - ) + attest(feedbackSchema.t).type.toString.snap(`{ contact: string }`) }) }) diff --git a/ark/type/__tests__/scope.test.ts b/ark/type/__tests__/scope.test.ts index 403c8c8b8a..82cdb8e8c5 100644 --- a/ark/type/__tests__/scope.test.ts +++ b/ark/type/__tests__/scope.test.ts @@ -6,7 +6,7 @@ import { type ArkErrors } from "@ark/schema" import { define, scope, type, type Module } from "arktype" -import type { distill, string } from "arktype/internal/attributes.ts" +import type { distill } from "arktype/internal/attributes.ts" import { writeUnexpectedCharacterMessage } from "arktype/internal/parser/shift/operator/operator.ts" contextualize(() => { @@ -276,8 +276,8 @@ contextualize(() => { b: { a: "a&b" } }).export() attest(types).type.toString.snap(`Module<{ - a: { b: { a: { b: cyclic; a: cyclic }; b: cyclic } } b: { a: { b: { a: cyclic; b: cyclic }; a: cyclic } } + a: { b: { a: { b: cyclic; a: cyclic }; b: cyclic } } }>`) }) @@ -287,8 +287,8 @@ contextualize(() => { b: { a: "a|true" } }).export() attest(types).type.toString.snap(`Module<{ - a: { b: false | { a: true | cyclic } } b: { a: true | { b: false | cyclic } } + a: { b: false | { a: true | cyclic } } }>`) }) @@ -462,9 +462,9 @@ b.c.c must be an object (was missing)`) }).export() attest< Module<{ - string: string.atLeastLength<1> + string: string foo: { - bar: string.atLeastLength<1> + bar: string } }> >(types) diff --git a/ark/type/__tests__/submodule.test.ts b/ark/type/__tests__/submodule.test.ts index edad875f9f..66ed209b14 100644 --- a/ark/type/__tests__/submodule.test.ts +++ b/ark/type/__tests__/submodule.test.ts @@ -96,7 +96,7 @@ contextualize.each( it("can reference subaliases in expression", () => { const dateFrom = type("string.date.parse | Date") - attest(dateFrom.t).type.toString.snap("Date | ((In: date) => To)") + attest(dateFrom.t).type.toString.snap("Date | ((In: string) => To)") attest(dateFrom("05-21-1993")).instanceOf(Date) attest(dateFrom(new Date())).instanceOf(Date) diff --git a/ark/type/__tests__/traverse.test.ts b/ark/type/__tests__/traverse.test.ts index 3068e27032..e502c0c4b1 100644 --- a/ark/type/__tests__/traverse.test.ts +++ b/ark/type/__tests__/traverse.test.ts @@ -236,10 +236,7 @@ age must be more than 18 (was 2)`) foo: ["unknown", "=>", () => callCount++] }).satisfying((data, ctx) => ctx.mustBe("valid")) - attest(t.t).type.toString.snap(`of< - { foo: (In: unknown) => Out }, - Anonymous ->`) + attest(t.t).type.toString.snap("{ foo: (In: unknown) => Out }") const out = t({ foo: 1 }) attest(out.toString()).snap('must be valid (was {"foo":1})') diff --git a/ark/type/attributes.ts b/ark/type/attributes.ts index f9c8079444..203cbb7bf4 100644 --- a/ark/type/attributes.ts +++ b/ark/type/attributes.ts @@ -1,33 +1,19 @@ +import type { ArkError, ArkErrors, Morph } from "@ark/schema" import type { - ArkError, - ArkErrors, - Constraint, - constraintKindOf, - Morph, - NodeSchema -} from "@ark/schema" -import { - noSuggest, - type anyOrNever, - type array, - type conform, - type equals, - type Hkt, - type intersectArrays, - type isSafelyMappable, - type leftIfEqual, - type Primitive, - type show + anyOrNever, + array, + Brand, + equals, + Hkt, + intersectArrays, + isSafelyMappable, + Primitive, + show } from "@ark/util" -import type { arkPrototypes } from "./keywords/constructors/constructors.ts" -import type { Date } from "./keywords/constructors/Date.ts" +import type { arkPrototypes } from "./keywords/constructors.ts" import type { type } from "./keywords/keywords.ts" -import type { number } from "./keywords/number/number.ts" -import type { Matching, string } from "./keywords/string/string.ts" import type { Type } from "./type.ts" -export type { arkPrototypes as object } from "./keywords/constructors/constructors.ts" -export type { number } from "./keywords/number/number.ts" -export type { string } from "./keywords/string/string.ts" +export type { arkPrototypes as object } from "./keywords/constructors.ts" export type Comparator = "<" | "<=" | ">" | ">=" | "==" @@ -37,64 +23,6 @@ export type DateLiteral = | `d"${source}"` | `d'${source}'` -export const attributesKey = noSuggest("attributes") - -export type attributesKey = typeof attributesKey - -export type of = base & { - [attributesKey]: { - base: base - attributes: attributes - } -} - -export type brand = base & { - [attributesKey]: { - base: base - attributes: attributes - brand: true - } -} - -export interface ConstrainingAttributeValuesByKind { - divisibleBy: number - moreThan: number - atLeast: number - atMost: number - lessThan: number - matching: string - moreThanLength: number - atLeastLength: number - atMostLength: number - lessThanLength: number - atOrAfter: string - after: string - atOrBefore: string - before: string - nominal: string -} - -export type ConstrainingAttributeKind = keyof ConstrainingAttributeValuesByKind - -export type ConstrainingAttributesByKind = { - [k in ConstrainingAttributeKind]?: Record< - ConstrainingAttributeValuesByKind[k], - true - > -} - -export interface MetaAttributeValuesByKind extends Optional, Default {} - -export type MetaAttributeKind = keyof MetaAttributeValuesByKind - -export type MetaAttributesByKind = Partial - -export interface Attributes - extends ConstrainingAttributesByKind, - MetaAttributesByKind {} - -export type AttributeKind = keyof Attributes - export type LimitLiteral = number | DateLiteral export type normalizeLimit = @@ -102,397 +30,95 @@ export type normalizeLimit = : limit extends number | string ? limit : never -export type constraint = { [k in rule & PropertyKey]: true } - -export type Anonymous = Nominal<"?"> - -export type Nominal = { - nominal: constraint -} - -export type AtLeast = { - atLeast: constraint -} - -export type AtMost = { - atMost: constraint -} - -export type MoreThan = { - moreThan: constraint -} - -export type LessThan = { - lessThan: constraint -} - -export type DivisibleBy = { - divisibleBy: constraint -} - -export type AtOrAfter = { - atOrAfter: constraint -} - -export type AtOrBefore = { - atOrBefore: constraint -} - -export type After = { - after: constraint -} - -export type Before = { - before: constraint -} - -export type primitiveConstraintKindOf = Extract< - Constraint.PrimitiveKind, - constraintKindOf -> - -export type AtLeastLength = { - atLeastLength: constraint -} - -export type AtMostLength = { - atMostLength: constraint -} - -export type MoreThanLength = { - moreThanLength: constraint -} - -export type LessThanLength = { - lessThanLength: constraint -} - -export type ExactlyLength = { - atLeastLength: constraint - atMostLength: constraint -} - -export type AttributeInferenceBehavior = "brand" | "associate" - -export type associateAttributes< - t, - attributes extends Attributes -> = attachAttributes - -export type brandAttributes< - t, - attributes extends Attributes -> = attachAttributes - -type attachAttributes< - t, - attributes extends Attributes, - behavior extends AttributeInferenceBehavior = "associate", - unmorphedT = Exclude -> = - t extends InferredMorph ? - (In: leftIfEqual>) => o - : leftIfEqual> - -type _attachAttributes< - t, - attributes extends Attributes, - behavior extends AttributeInferenceBehavior, - distributed = t -> = - distributed extends null | undefined ? distributed - : distributed extends of ? - "brand" extends keyof distributed[attributesKey] | behavior ? - brandMultiple - : associateMultiple - : extractIfSingleAttributeEntry extends ( - AttributeEntry - ) ? - "brand" extends behavior ? - brandSingle - : associateSingle - : "brand" extends behavior ? brandMultiple - : associateMultiple - -type associateMultiple = - [t, string] extends [string, t] ? string.is - : [t, number] extends [number, t] ? number.is - : [t, Date] extends [Date, t] ? Date.is - : of - -type brandMultiple = - [t, string] extends [string, t] ? string.branded.is - : [t, number] extends [number, t] ? number.branded.is - : [t, Date] extends [Date, t] ? Date.branded.is - : brand - -type associateSingle< - t, - attributes extends Attributes, - kind extends AttributeKind, - value -> = - [t, string] extends [string, t] ? string.raw.withSingleAttribute - : [t, number] extends [number, t] ? - number.raw.withSingleAttribute - : [t, Date] extends [Date, t] ? Date.raw.withSingleAttribute - : of - -type brandSingle< - t, - attributes extends Attributes, - kind extends AttributeKind, - value -> = - [t, string] extends [string, t] ? - string.branded.raw.withSingleAttribute - : [t, number] extends [number, t] ? - number.branded.raw.withSingleAttribute - : [t, Date] extends [Date, t] ? - Date.branded.raw.withSingleAttribute - : brand - -type AttributeEntry = [kind, value] - -/** - * Check if attributes is a single attribute kind + value that can be collapsed - * for display purposes, e.g.: - * - * // has multiple attribute kinds - * { divisibleBy: { 2: true }, moreThan: { 3: true } } => null - * - * // has multiple attribute values of a single kind - * { divisibleBy: { 2: true, 3: true } } => null - * - * // has a single attribute kind + value, can be collapsed - * { divisibleBy: { 2: true } } => ["divisibleBy", 2] - */ -type extractIfSingleAttributeEntry = - extractIfSingleEntry extends ( - AttributeEntry - ) ? - extractIfSingleEntry extends [infer key, infer value] ? - // the relevant values for optional and default aren't - // stored in keys like constraining attributes - AttributeEntry - : null - : null - -type extractIfSingleEntry = { - [k in keyof o]: keyof o extends k ? [key: k, value: o[k]] : null -}[keyof o] - -export interface LengthAttributeValuesByKind { - moreThanLength: number - atLeastLength: number - atMostLength: number - lessThanLength: number -} - -export type LengthAttributeKind = keyof LengthAttributeValuesByKind - -export type normalizePrimitiveConstraintRoot< - schema extends NodeSchema -> = - "rule" extends keyof schema ? conform - : conform - -type minLengthSchemaToConstraint = - schema extends { exclusive: true } ? MoreThanLength - : AtLeastLength - -type maxLengthSchemaToConstraint = - schema extends { exclusive: true } ? LessThanLength : AtMostLength - -export type associateAttributesFromSchema< - t, - kind extends Constraint.PrimitiveKind, - schema extends NodeSchema -> = associateAttributes> - -// useful for helping TypeScript infer that adding attributes to a type -// like string will still be a string for methods like .matching -export type associateAttributesFromStringSchema< - t extends string, - kind extends Constraint.PrimitiveKind, - schema extends NodeSchema -> = conform>, string> - -export type associateAttributesFromNumberSchema< - t extends number, - kind extends Constraint.PrimitiveKind, - schema extends NodeSchema -> = conform>, number> - -export type associateAttributesFromDateSchema< - t extends Date, - kind extends Constraint.PrimitiveKind, - schema extends NodeSchema -> = conform>, Date> - -export type associateAttributesFromArraySchema< - t extends readonly unknown[], - kind extends Constraint.PrimitiveKind, - schema extends NodeSchema -> = conform< - associateAttributes>, - readonly unknown[] -> - -export type schemaToAttributes< - kind extends Constraint.PrimitiveKind, - schema extends NodeSchema -> = - normalizePrimitiveConstraintRoot extends infer rule ? - kind extends "pattern" ? Matching - : kind extends "divisor" ? DivisibleBy - : kind extends "min" ? number.minSchemaToConstraint - : kind extends "max" ? number.maxSchemaToConstraint - : kind extends "minLength" ? minLengthSchemaToConstraint - : kind extends "maxLength" ? maxLengthSchemaToConstraint - : kind extends "exactLength" ? ExactlyLength - : kind extends "after" ? Date.afterSchemaToConstraint - : kind extends "before" ? Date.beforeSchemaToConstraint - : Anonymous - : never - export type distill< t, - opts extends distill.Options = {} -> = finalizeDistillation> + endpoint extends distill.Endpoint +> = finalizeDistillation> export declare namespace distill { export type Endpoint = "in" | "out" | "out.introspectable" - export type Options = { - endpoint?: Endpoint - attributes?: "preserve" | "brand" | "unbrand" - } - - export type In = distill + export type In = distill - export type Out = distill - - export namespace withAttributes { - export type In = distill - - export type Out = distill - - export namespace introspectable { - export type Out = distill< - t, - { endpoint: "out.introspectable"; attributes: "preserve" } - > - } - } - - export type brand = distill - - export type unbrand = distill + export type Out = distill export namespace introspectable { - export type Out = distill + export type Out = distill } } type finalizeDistillation = equals extends true ? t : distilled -type _distill = +type _distill = // ensure optional keys don't prevent extracting defaults t extends undefined ? t - : [t] extends [anyOrNever] ? t - : t extends of ? - opts["attributes"] extends "preserve" ? - associateAttributes<_distill, attributes> - : opts["attributes"] extends "unbrand" ? - associateAttributes<_distill, attributes> - : opts["attributes"] extends "brand" ? - brand<_distill, attributes> - : "brand" extends keyof t[attributesKey] ? - brand<_distill, attributes> - : _distill + : [t] extends [anyOrNever | seen] ? t : unknown extends t ? unknown - : t extends TerminallyInferredObject | Primitive ? t - : t extends InferredMorph ? distillIo - : 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 - : isSafelyMappable extends true ? distillMappable + : t extends Brand ? + endpoint extends "in" ? + base + : t + : // Function is excluded from TerminallyInferredObjectKind so that + // those types could be inferred before checking for morphs + t extends TerminallyInferredObject | Primitive ? t + : t extends Function ? + t extends InferredMorph ? + distillIo + : t + : t extends Default ? _distill + : t extends array ? distillArray + : isSafelyMappable extends true ? distillMappable : t -type distillMappable = - opts["endpoint"] extends "in" ? +type distillMappable = + endpoint extends "in" ? show< { // this is homomorphic so includes parsed optional keys like "key?": "string" - [k in keyof o as k extends inferredOptionalOrDefaultKeyOf ? never - : k]: _distill + [k in keyof o as k extends inferredDefaultKeyOf ? never + : k]: _distill } & { - [k in inferredOptionalOrDefaultKeyOf]?: _distill + [k in inferredDefaultKeyOf]?: _distill } > - : show< - { - // this is homomorphic so includes parsed optional keys like "key?": "string" - [k in keyof o as k extends inferredOptionalKeyOf ? never - : k]: _distill - } & { - [k in keyof o as k extends inferredOptionalKeyOf ? k - : never]?: _distill - } - > - -type distillIo = - opts["endpoint"] extends "in" ? _distill - : opts["endpoint"] extends "out.introspectable" ? - o extends To ? - _distill - : unknown - : opts["endpoint"] extends "out" ? _distill - : _distill extends infer r ? - o extends To ? - (In: i) => To - : (In: i) => Out - : never - -export type inferredOptionalOrDefaultKeyOf = - | inferredDefaultKeyOf - | inferredOptionalKeyOf - -type inExtends = - [v] extends [anyOrNever] ? false - : [v] extends [t] ? true - : [v] extends [InferredMorph] ? inExtends - : false + : { [k in keyof o]: _distill } + +type distillIo = + endpoint extends "out" ? _distill + : endpoint extends "in" ? _distill + : // out.introspectable only respects To (schema-validated output) + o extends To ? _distill + : unknown + +type unwrapInput = + t extends InferredMorph ? + t extends anyOrNever ? + t + : i + : t type inferredDefaultKeyOf = keyof o extends infer k ? k extends keyof o ? - inExtends extends true ? - k + unwrapInput extends Default ? + [t] extends [anyOrNever] ? + never + : k : never : never : never -type inferredOptionalKeyOf = - keyof o extends infer k ? - k extends keyof o ? - inExtends extends true ? - k - : never - : never - : never - -type distillArray = +type distillArray = // fast path for non-tuple arrays with no extra props // this also allows TS to infer certain recursive arrays like JSON - t[number][] extends t ? alignReadonly<_distill[], t> + t[number][] extends t ? + alignReadonly<_distill[], t> : distillNonArraykeys< t, - alignReadonly<_distillArray<[...t], opts, []>, t>, - opts + alignReadonly, t>, + endpoint, + seen > type alignReadonly = @@ -502,40 +128,49 @@ type alignReadonly = type distillNonArraykeys< originalArray extends array, distilledArray, - opts extends distill.Options + endpoint extends distill.Endpoint, + seen > = - keyof originalArray extends keyof distilledArray | attributesKey ? - distilledArray + keyof originalArray extends keyof distilledArray ? distilledArray : distilledArray & _distill< { - [k in keyof originalArray as k extends ( - | keyof distilledArray - | (opts["attributes"] extends true ? never : attributesKey) - ) ? - never + [k in keyof originalArray as k extends keyof distilledArray ? never : k]: originalArray[k] }, - opts + endpoint, + seen > -type _distillArray< +type distillArrayFromPrefix< t extends array, - opts extends distill.Options, + endpoint extends distill.Endpoint, + seen, prefix extends array > = t extends readonly [infer head, ...infer tail] ? - _distillArray]> - : [...prefix, ...distillPostfix] + distillArrayFromPrefix< + tail, + endpoint, + seen, + [...prefix, _distill] + > + : [...prefix, ...distillArrayFromPostfix] -type distillPostfix< +type distillArrayFromPostfix< t extends array, - opts extends distill.Options, - postfix extends array = [] + endpoint extends distill.Endpoint, + seen, + postfix extends array > = t extends readonly [...infer init, infer last] ? - distillPostfix, ...postfix]> - : [...{ [i in keyof t]: _distill }, ...postfix] + distillArrayFromPostfix< + init, + endpoint, + seen, + [_distill, ...postfix] + > + : [...{ [i in keyof t]: _distill }, ...postfix] type BuiltinTerminalObjectKind = Exclude< keyof arkPrototypes.instances, @@ -549,20 +184,15 @@ type TerminallyInferredObject = export type inferPredicate = predicate extends (data: any, ...args: any[]) => data is infer narrowed ? - t extends of ? - "brand" extends keyof t[attributesKey] ? - brand - : of - : narrowed - : associateAttributes + narrowed + : t export type inferPipes = pipes extends [infer head extends Morph, ...infer tail extends Morph[]] ? inferPipes< - pipes[0] extends type.cast ? inferPipe - : inferMorphOut extends infer out ? - (In: distill.withAttributes.In) => Out - : never, + head extends type.cast ? inferPipe + : inferMorphOut extends infer out ? (In: distill.In) => Out + : never, tail > : t @@ -572,39 +202,75 @@ export type inferMorphOut = Exclude< ArkError | ArkErrors > -export type Out = ["=>", o, boolean] +declare const morphOutSymbol: unique symbol -export type To = ["=>", o, true] +export interface Out { + [morphOutSymbol]: true + t: o + introspectable: boolean +} + +export interface To extends Out { + introspectable: true +} export type InferredMorph = (In: i) => o -export type Optional = { - optional: { - "=": true - } -} +declare const defaultsTo: unique symbol -export type InferredOptional = of +export type Default = { [defaultsTo]: [t, v] } -export type Default = { - defaultsTo: { - value: v - } -} +// we have to distribute over morphs to preserve the i/o relationship +// this avoids stuff like: +// Default => Default | Default +export type withDefault = + t extends InferredMorph ? addDefaultToMorph + : Default, v> -export type DefaultFor = +type addDefaultToMorph = + [normalizeMorphDistribution] extends [InferredMorph] ? + (In: Default) => o + : never + +// will return `boolean` if some morphs are unequal +// so should be compared against `true` +type normalizeMorphDistribution< + t, + undistributedIn = t extends InferredMorph ? i : never, + undistributedOut extends Out = [t] extends [InferredMorph] ? + [o] extends [To] ? + To + : o + : never +> = + // using Extract here rather than distributing normally helps TS collapse the union + // was otherwise getting duplicated branches, e.g.: + // (In: boolean) => To | (In: boolean) => To + // revert to `t extends InferredMorph...` if it doesn't break the tests in the future + | (Extract extends anyOrNever ? never + : Extract extends InferredMorph ? + [undistributedOut] extends [o] ? (In: undistributedIn) => undistributedOut + : [undistributedIn] extends [i] ? + (In: undistributedIn) => undistributedOut + : t + : never) + | Exclude + +export type defaultFor = | (Primitive extends t ? Primitive : t extends Primitive ? t : never) | (() => t) -export type InferredDefault = of> - export type termOrType = t | Type -export type inferIntersection = _inferIntersection +export type inferIntersection = normalizeMorphDistribution< + _inferIntersection +> -export type inferPipe = _inferIntersection +export type inferPipe = normalizeMorphDistribution< + _inferIntersection +> type _inferIntersection = [l & r] extends [infer t extends anyOrNever] ? t @@ -618,12 +284,6 @@ type _inferIntersection = : (In: _inferIntersection) => lOut : r extends InferredMorph ? (In: _inferIntersection) => rOut - : l extends of ? - r extends of ? - of<_inferIntersection, lAttributes & rAttributes> - : of<_inferIntersection, lAttributes> - : r extends of ? - of<_inferIntersection, rAttributes> : [l, r] extends [object, object] ? // adding this intermediate infer result avoids extra instantiations intersectObjects extends infer result ? diff --git a/ark/type/generic.ts b/ark/type/generic.ts index 62e39c96f9..b863f24dc0 100644 --- a/ark/type/generic.ts +++ b/ark/type/generic.ts @@ -16,16 +16,13 @@ import { type ErrorMessage, type ErrorType, type Hkt, - type Json, + type JsonStructure, type WhitespaceChar } from "@ark/util" import type { type } from "./keywords/keywords.ts" import type { inferAstRoot } from "./parser/ast/infer.ts" import type { validateAst } from "./parser/ast/validate.ts" -import type { - inferDefinition, - validateDefinition -} from "./parser/definition.ts" +import type { inferDefinition } from "./parser/definition.ts" import { DynamicState } from "./parser/reduce/dynamic.ts" import type { state, StaticState } from "./parser/reduce/static.ts" import type { ArkTypeScanner } from "./parser/shift/scanner.ts" @@ -252,7 +249,7 @@ export interface Generic< arg$: Scope internal: GenericRoot - json: Json + json: JsonStructure } export type GenericConstructor< @@ -414,7 +411,7 @@ export type GenericParser<$ = {}> = < interface GenericBodyParser, $> { ( - body: validateDefinition> + body: type.validate> ): Generic ( diff --git a/ark/type/keywords/constructors/Array.ts b/ark/type/keywords/Array.ts similarity index 88% rename from ark/type/keywords/constructors/Array.ts rename to ark/type/keywords/Array.ts index f733b6ab68..5ce370873c 100644 --- a/ark/type/keywords/constructors/Array.ts +++ b/ark/type/keywords/Array.ts @@ -1,8 +1,8 @@ import { genericNode, intrinsic, rootSchema } from "@ark/schema" import { Hkt, liftArray, type Digit } from "@ark/util" -import type { To } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" +import type { To } from "../attributes.ts" +import type { Module, Submodule } from "../module.ts" +import { arkModule } from "./utils.ts" class liftFromHkt extends Hkt<[element: unknown]> { declare body: liftArray extends infer lifted ? diff --git a/ark/type/keywords/constructors/FormData.ts b/ark/type/keywords/FormData.ts similarity index 91% rename from ark/type/keywords/constructors/FormData.ts rename to ark/type/keywords/FormData.ts index 4642895d7d..15598e92ed 100644 --- a/ark/type/keywords/constructors/FormData.ts +++ b/ark/type/keywords/FormData.ts @@ -1,8 +1,8 @@ import { rootSchema } from "@ark/schema" import { registry } from "@ark/util" -import type { To } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" +import type { To } from "../attributes.ts" +import type { Module, Submodule } from "../module.ts" +import { arkModule } from "./utils.ts" export type FormDataValue = string | File diff --git a/ark/type/keywords/constructors/TypedArray.ts b/ark/type/keywords/TypedArray.ts similarity index 91% rename from ark/type/keywords/constructors/TypedArray.ts rename to ark/type/keywords/TypedArray.ts index 2f12dbe342..6937877b4b 100644 --- a/ark/type/keywords/constructors/TypedArray.ts +++ b/ark/type/keywords/TypedArray.ts @@ -1,5 +1,5 @@ -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" +import type { Module, Submodule } from "../module.ts" +import { arkModule } from "./utils.ts" export const TypedArray: TypedArray.module = arkModule({ Int8: ["instanceof", Int8Array], diff --git a/ark/type/keywords/constructors/constructors.ts b/ark/type/keywords/constructors.ts similarity index 93% rename from ark/type/keywords/constructors/constructors.ts rename to ark/type/keywords/constructors.ts index f22b35f53a..d235f54abe 100644 --- a/ark/type/keywords/constructors/constructors.ts +++ b/ark/type/keywords/constructors.ts @@ -6,11 +6,11 @@ import { type KeySet, type PlatformObjects } from "@ark/util" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" +import type { Module, Submodule } from "../module.ts" import { arkArray } from "./Array.ts" import { arkFormData } from "./FormData.ts" import { TypedArray } from "./TypedArray.ts" +import { arkModule } from "./utils.ts" const omittedPrototypes = { Boolean: 1, diff --git a/ark/type/keywords/constructors/Date.ts b/ark/type/keywords/constructors/Date.ts deleted file mode 100644 index 7927bddfc8..0000000000 --- a/ark/type/keywords/constructors/Date.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { satisfy } from "@ark/util" -import type { - After, - Anonymous, - AtOrAfter, - AtOrBefore, - AttributeKind, - Attributes, - Before, - brand, - Default, - Nominal, - normalizeLimit, - of, - Optional -} from "../../attributes.ts" - -export declare namespace Date { - export type atOrAfter = of> - - export type after = of> - - export type atOrBefore = of> - - export type before = of> - - export type anonymous = of - - export type nominal = of> - - export type optional = of - - export type defaultsTo = of> - - export type is = of - - export type afterSchemaToConstraint = - schema extends { exclusive: true } ? After> - : AtOrAfter> - - export type beforeSchemaToConstraint = - schema extends { exclusive: true } ? Before> - : AtOrBefore> - - export type AttributableKind = satisfy< - AttributeKind, - "after" | "atOrAfter" | "before" | "atOrBefore" - > - - export type withSingleAttribute< - kind extends AttributableKind, - value extends Attributes[kind] - > = raw.withSingleAttribute - - export namespace raw { - export type withSingleAttribute = - kind extends "nominal" ? nominal - : kind extends "after" ? after - : kind extends "atOrAfter" ? atOrAfter - : kind extends "before" ? before - : kind extends "atOrBefore" ? atOrBefore - : kind extends "optional" ? optional - : kind extends "defaultsTo" ? defaultsTo - : never - } - - export type branded = brand> - - export namespace branded { - export type atOrAfter = brand> - - export type after = brand> - - export type atOrBefore = brand> - - export type before = brand> - - export type anonymous = brand - - export type is = brand - - export type withSingleAttribute< - kind extends AttributableKind, - value extends Attributes[kind] - > = raw.withSingleAttribute - - export namespace raw { - export type withSingleAttribute = - kind extends "nominal" ? branded - : kind extends "after" ? after - : kind extends "atOrAfter" ? atOrAfter - : kind extends "before" ? before - : kind extends "atOrBefore" ? atOrBefore - : never - } - } -} diff --git a/ark/type/keywords/keywords.ts b/ark/type/keywords/keywords.ts index 48f7600896..7920c8d336 100644 --- a/ark/type/keywords/keywords.ts +++ b/ark/type/keywords/keywords.ts @@ -1,6 +1,6 @@ import type { ArkErrors, arkKind } from "@ark/schema" -import type { inferred } from "@ark/util" -import type { distill } from "../attributes.ts" +import type { Brand, inferred } from "@ark/util" +import type { distill, InferredMorph, Out, To } from "../attributes.ts" import type { GenericParser } from "../generic.ts" import type { BaseType } from "../methods/base.ts" import type { BoundModule, Module } from "../module.ts" @@ -17,9 +17,9 @@ import type { TypeParser } from "../type.ts" import { arkBuiltins } from "./builtins.ts" -import { arkPrototypes } from "./constructors/constructors.ts" -import { number } from "./number/number.ts" -import { string } from "./string/string.ts" +import { arkPrototypes } from "./constructors.ts" +import { number } from "./number.ts" +import { string } from "./string.ts" import { arkTsGenerics, arkTsKeywords, object, unknown } from "./ts.ts" export interface Ark @@ -103,16 +103,6 @@ export declare namespace type { inferDefinition > - export namespace withAttributes { - export type In = distill.withAttributes.In< - inferDefinition - > - - export type Out = distill.withAttributes.Out< - inferDefinition - > - } - export namespace introspectable { export type Out = distill.introspectable.Out< inferDefinition @@ -125,6 +115,13 @@ export declare namespace type { $, args > + + export type brand = + t extends InferredMorph ? + o["introspectable"] extends true ? + (In: i) => To> + : (In: i) => Out> + : Brand } export type type = Type diff --git a/ark/type/keywords/number.ts b/ark/type/keywords/number.ts new file mode 100644 index 0000000000..77232f3b75 --- /dev/null +++ b/ark/type/keywords/number.ts @@ -0,0 +1,69 @@ +import { intrinsic, rootSchema } from "@ark/schema" +import type { Module, Submodule } from "../module.ts" +import { arkModule } from "./utils.ts" + +/** + * As per the ECMA-262 specification: + * A time value supports a slightly smaller range of -8,640,000,000,000,000 to 8,640,000,000,000,000 milliseconds. + * + * @see https://262.ecma-international.org/15.0/index.html#sec-time-values-and-time-range + */ + +export const epoch = rootSchema({ + domain: { + domain: "number", + meta: "a number representing a Unix timestamp" + }, + divisor: { + rule: 1, + meta: `an integer representing a Unix timestamp` + }, + min: { + rule: -8640000000000000, + meta: `a Unix timestamp after -8640000000000000` + }, + max: { + rule: 8640000000000000, + meta: "a Unix timestamp before 8640000000000000" + }, + meta: "an integer representing a safe Unix timestamp" +}) + +export const integer = rootSchema({ + domain: "number", + divisor: 1 +}) + +export const number: number.module = arkModule({ + root: intrinsic.number, + integer, + epoch, + safe: rootSchema({ + domain: "number", + min: Number.MIN_SAFE_INTEGER, + max: Number.MAX_SAFE_INTEGER, + predicate: { + predicate: n => !Number.isNaN(n), + meta: "a safe number" + } + }), + NaN: ["===", Number.NaN], + Infinity: ["===", Number.POSITIVE_INFINITY], + NegativeInfinity: ["===", Number.NEGATIVE_INFINITY] +}) + +export declare namespace number { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: number + epoch: number + integer: number + safe: number + NaN: number + Infinity: number + NegativeInfinity: number + } +} diff --git a/ark/type/keywords/number/epoch.ts b/ark/type/keywords/number/epoch.ts deleted file mode 100644 index fd8a692ebf..0000000000 --- a/ark/type/keywords/number/epoch.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { rootSchema } from "@ark/schema" -import type { AtLeast, AtMost, DivisibleBy } from "../../attributes.ts" -import type { number } from "./number.ts" - -/** - * As per the ECMA-262 specification: - * A time value supports a slightly smaller range of -8,640,000,000,000,000 to 8,640,000,000,000,000 milliseconds. - * - * @see https://262.ecma-international.org/15.0/index.html#sec-time-values-and-time-range - */ - -export const epoch = rootSchema({ - domain: { - domain: "number", - meta: "a number representing a Unix timestamp" - }, - divisor: { - rule: 1, - meta: `an integer representing a Unix timestamp` - }, - min: { - rule: -8640000000000000, - meta: `a Unix timestamp after -8640000000000000` - }, - max: { - rule: 8640000000000000, - meta: "a Unix timestamp before 8640000000000000" - }, - meta: "an integer representing a safe Unix timestamp" -}) - -export type epoch = number.is< - DivisibleBy<1> & AtMost<8640000000000000> & AtLeast<-8640000000000000> -> diff --git a/ark/type/keywords/number/integer.ts b/ark/type/keywords/number/integer.ts deleted file mode 100644 index 86758b293f..0000000000 --- a/ark/type/keywords/number/integer.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { rootSchema } from "@ark/schema" -import type { number } from "./number.ts" - -export const integer = rootSchema({ - domain: "number", - divisor: 1 -}) - -export type integer = number.divisibleBy<1> diff --git a/ark/type/keywords/number/number.ts b/ark/type/keywords/number/number.ts deleted file mode 100644 index 31f42b1a91..0000000000 --- a/ark/type/keywords/number/number.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { intrinsic, rootSchema } from "@ark/schema" -import type { satisfy } from "@ark/util" -import type { - Anonymous, - AtLeast, - AtMost, - AttributeKind, - Attributes, - brand, - Default, - DivisibleBy, - LessThan, - MoreThan, - Nominal, - of, - Optional -} from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" -import { epoch } from "./epoch.ts" -import { integer } from "./integer.ts" - -export const number: number.module = arkModule({ - root: intrinsic.number, - integer, - epoch, - safe: rootSchema({ - domain: "number", - min: Number.MIN_SAFE_INTEGER, - max: Number.MAX_SAFE_INTEGER, - predicate: { - predicate: n => !Number.isNaN(n), - meta: "a safe number" - } - }), - NaN: ["===", Number.NaN], - Infinity: ["===", Number.POSITIVE_INFINITY], - NegativeInfinity: ["===", Number.NEGATIVE_INFINITY] -}) - -export declare namespace number { - export type atLeast = of> - - export type moreThan = of> - - export type atMost = of> - - export type lessThan = of> - - export type divisibleBy = of> - - export type anonymous = of - - export type optional = of - - export type defaultsTo = of> - - export type nominal = of> - - export type NaN = nominal<"NaN"> - - export type Infinity = nominal<"Infinity"> - - export type NegativeInfinity = nominal<"NegativeInfinity"> - - export type safe = nominal<"safe"> - - export type is = of - - export type AttributableKind = satisfy< - AttributeKind, - "divisibleBy" | "moreThan" | "atLeast" | "atMost" | "lessThan" - > - - export type minSchemaToConstraint = - schema extends { exclusive: true } ? MoreThan : AtLeast - - export type maxSchemaToConstraint = - schema extends { exclusive: true } ? LessThan : AtMost - - export type withSingleAttribute< - kind extends AttributableKind, - value extends Attributes[kind] - > = raw.withSingleAttribute - - export namespace raw { - export type withSingleAttribute = - kind extends "nominal" ? nominal - : kind extends "divisibleBy" ? divisibleBy - : kind extends "moreThan" ? moreThan - : kind extends "atLeast" ? atLeast - : kind extends "atMost" ? atMost - : kind extends "lessThan" ? lessThan - : kind extends "optional" ? optional - : kind extends "defaultsTo" ? defaultsTo - : never - } - - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: number - epoch: epoch - integer: integer - safe: safe - NaN: NaN - Infinity: Infinity - NegativeInfinity: NegativeInfinity - } - - export type branded = brand> - - export namespace branded { - export type atLeast = brand> - - export type moreThan = brand> - - export type atMost = brand> - - export type lessThan = brand> - - export type divisibleBy = brand> - - export type is = brand - - export type anonymous = brand - - export type withSingleAttribute< - kind extends AttributableKind, - value extends Attributes[kind] - > = raw.withSingleAttribute - - export namespace raw { - export type withSingleAttribute = - kind extends "nominal" ? branded - : kind extends "divisibleBy" ? divisibleBy - : kind extends "moreThan" ? moreThan - : kind extends "atLeast" ? atLeast - : kind extends "atMost" ? atMost - : kind extends "lessThan" ? lessThan - : never - } - } -} diff --git a/ark/type/keywords/string.ts b/ark/type/keywords/string.ts new file mode 100644 index 0000000000..6b08853946 --- /dev/null +++ b/ark/type/keywords/string.ts @@ -0,0 +1,820 @@ +import { + ArkErrors, + intrinsic, + node, + rootSchema, + type IntersectionNode, + type Morph, + type TraversalContext +} from "@ark/schema" +import { + flatMorph, + numericStringMatcher, + wellFormedIntegerMatcher, + type Json +} from "@ark/util" +import type { To } from "../attributes.ts" +import type { Module, Submodule } from "../module.ts" +import { number } from "./number.ts" +import { arkModule } from "./utils.ts" + +// Non-trivial expressions should have an explanation or attribution + +export const regexStringNode = ( + regex: RegExp, + description: string +): IntersectionNode => + node("intersection", { + domain: "string", + pattern: { + rule: regex.source, + flags: regex.flags, + meta: description + } + }) as never + +const stringIntegerRoot = regexStringNode( + wellFormedIntegerMatcher, + "a well-formed integer string" +) + +export const stringInteger: stringInteger.module = arkModule({ + root: stringIntegerRoot, + parse: rootSchema({ + in: stringIntegerRoot, + morphs: (s: string, ctx: TraversalContext) => { + const parsed = Number.parseInt(s) + return Number.isSafeInteger(parsed) ? parsed : ( + ctx.error( + "an integer in the range Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER" + ) + ) + }, + declaredOut: intrinsic.integer + }) +}) + +export declare namespace stringInteger { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: string + parse: (In: string) => To + } +} + +const base64 = arkModule({ + root: regexStringNode( + /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/, + "base64-encoded" + ), + url: regexStringNode( + /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}(?:==|%3D%3D)?|[A-Za-z0-9_-]{3}(?:=|%3D)?)?$/, + "base64url-encoded" + ) +}) + +declare namespace base64 { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: string + url: string + } +} + +const preformattedCapitalize = regexStringNode(/^[A-Z].*$/, "capitalized") + +export const capitalize: capitalize.module = arkModule({ + root: rootSchema({ + in: "string", + morphs: (s: string) => s.charAt(0).toUpperCase() + s.slice(1), + declaredOut: preformattedCapitalize + }), + preformatted: preformattedCapitalize +}) + +export declare namespace capitalize { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: (In: string) => To + preformatted: string + } +} + +// https://github.com/validatorjs/validator.js/blob/master/src/lib/isLuhnNumber.js +export const isLuhnValid = (creditCardInput: string): boolean => { + const sanitized = creditCardInput.replace(/[- ]+/g, "") + let sum = 0 + let digit: string + let tmpNum: number + let shouldDouble = false + for (let i = sanitized.length - 1; i >= 0; i--) { + digit = sanitized.substring(i, i + 1) + tmpNum = Number.parseInt(digit, 10) + if (shouldDouble) { + tmpNum *= 2 + if (tmpNum >= 10) sum += (tmpNum % 10) + 1 + else sum += tmpNum + } else sum += tmpNum + + shouldDouble = !shouldDouble + } + return !!(sum % 10 === 0 ? sanitized : false) +} + +// https://github.com/validatorjs/validator.js/blob/master/src/lib/isCreditCard.js +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 = rootSchema({ + domain: "string", + pattern: { + meta: "a credit card number", + rule: creditCardMatcher.source + }, + predicate: { + meta: "a credit card number", + predicate: isLuhnValid + } +}) + +type DayDelimiter = "." | "/" | "-" + +const dayDelimiterMatcher = /^[./-]$/ + +type DayPart = DayPatterns[PartKey] + +type PartKey = keyof DayPatterns + +type DayPatterns = { + y: "yy" | "yyyy" + m: "mm" | "m" + d: "dd" | "d" +} + +type fragment = + | `${delimiter}${part}` + | "" + +export type DayPattern = + delimiter extends unknown ? + { + [k1 in keyof DayPatterns]: { + [k2 in Exclude]: `${DayPatterns[k1]}${fragment< + DayPatterns[k2], + delimiter + >}${fragment< + DayPatterns[Exclude], + delimiter + >}` + }[Exclude] + }[keyof DayPatterns] + : never + +export type DateFormat = "iso" | DayPattern + +export type DateOptions = { + format?: DateFormat +} + +// ISO 8601 date/time modernized from https://github.com/validatorjs/validator.js/blob/master/src/lib/isISO8601.js +// Based on https://tc39.es/ecma262/#sec-date-time-string-format, the T +// delimiter for date/time is mandatory. Regex from validator.js strict matcher: +export const iso8601Matcher = + /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-3])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/ + +type ParsedDayParts = { + y?: string + m?: string + d?: string +} + +const isValidDateInstance = (date: Date) => !Number.isNaN(+date) + +const writeFormattedExpected = (format: DateFormat) => + `a ${format}-formatted date` + +export const tryParseDatePattern = ( + data: string, + opts?: DateOptions +): Date | string => { + if (!opts?.format) { + const result = new Date(data) + return isValidDateInstance(result) ? result : "a valid date" + } + if (opts.format === "iso") { + return iso8601Matcher.test(data) ? + new Date(data) + : writeFormattedExpected("iso") + } + const dataParts = data.split(dayDelimiterMatcher) + // will be the first delimiter matched, if there is one + const delimiter: string | undefined = data[dataParts[0].length] + const formatParts = delimiter ? opts.format.split(delimiter) : [opts.format] + + if (dataParts.length !== formatParts.length) + return writeFormattedExpected(opts.format) + + const parsedParts: ParsedDayParts = {} + for (let i = 0; i < formatParts.length; i++) { + if ( + dataParts[i].length !== formatParts[i].length && + // if format is "m" or "d", data is allowed to be 1 or 2 characters + !(formatParts[i].length === 1 && dataParts[i].length === 2) + ) + return writeFormattedExpected(opts.format) + + parsedParts[formatParts[i][0] as PartKey] = dataParts[i] + } + + const date = new Date(`${parsedParts.m}/${parsedParts.d}/${parsedParts.y}`) + + if (`${date.getDate()}` === parsedParts.d) return date + + return writeFormattedExpected(opts.format) +} + +const isParsableDate = (s: string) => !Number.isNaN(new Date(s).valueOf()) + +const parsableDate = rootSchema({ + domain: "string", + predicate: { + meta: "a parsable date", + predicate: isParsableDate + } +}).assertHasKind("intersection") + +const epochRoot = stringInteger.root.internal + .narrow((s, ctx) => { + // we know this is safe since it has already + // been validated as an integer string + const n = Number.parseInt(s) + const out = number.epoch(n) + if (out instanceof ArkErrors) { + ctx.errors.merge(out) + return false + } + return true + }) + .withMeta({ + description: "an integer string representing a safe Unix timestamp" + }) + .assertHasKind("intersection") + +const epoch = arkModule({ + root: epochRoot, + parse: rootSchema({ + in: epochRoot, + morphs: (s: string) => new Date(s), + declaredOut: intrinsic.Date + }) +}) + +const isoRoot = regexStringNode( + iso8601Matcher, + "an ISO 8601 (YYYY-MM-DDTHH:mm:ss.sssZ) date" +).internal.assertHasKind("intersection") + +const iso = arkModule({ + root: isoRoot, + parse: rootSchema({ + in: isoRoot, + morphs: (s: string) => new Date(s), + declaredOut: intrinsic.Date + }) +}) + +export const stringDate: stringDate.module = arkModule({ + root: parsableDate, + parse: rootSchema({ + declaredIn: parsableDate, + in: "string", + morphs: (s: string, ctx: TraversalContext) => { + const date = new Date(s) + if (Number.isNaN(date.valueOf())) return ctx.error("a parsable date") + return date + }, + declaredOut: intrinsic.Date + }), + iso, + epoch +}) + +export declare namespace stringDate { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: string + parse: (In: string) => To + iso: iso.submodule + epoch: epoch.submodule + } + + export namespace iso { + export type submodule = Submodule<$> + + export type $ = { + root: string + parse: (In: string) => To + } + } + + export namespace epoch { + export type submodule = Submodule<$> + + export type $ = { + root: string + parse: (In: string) => To + } + } +} + +const email = regexStringNode( + // https://www.regular-expressions.info/email.html + /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, + "an email address" +) + +// Based on https://github.com/validatorjs/validator.js/blob/master/src/lib/isIP.js +// Adjusted to incorporate unmerged fix in https://github.com/validatorjs/validator.js/pull/2083 +const ipv4Segment = "(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])" +const ipv4Address = `(${ipv4Segment}[.]){3}${ipv4Segment}` +const ipv4Matcher = new RegExp(`^${ipv4Address}$`) + +const ipv6Segment = "(?:[0-9a-fA-F]{1,4})" +const ipv6Matcher = new RegExp( + "^(" + + `(?:${ipv6Segment}:){7}(?:${ipv6Segment}|:)|` + + `(?:${ipv6Segment}:){6}(?:${ipv4Address}|:${ipv6Segment}|:)|` + + `(?:${ipv6Segment}:){5}(?::${ipv4Address}|(:${ipv6Segment}){1,2}|:)|` + + `(?:${ipv6Segment}:){4}(?:(:${ipv6Segment}){0,1}:${ipv4Address}|(:${ipv6Segment}){1,3}|:)|` + + `(?:${ipv6Segment}:){3}(?:(:${ipv6Segment}){0,2}:${ipv4Address}|(:${ipv6Segment}){1,4}|:)|` + + `(?:${ipv6Segment}:){2}(?:(:${ipv6Segment}){0,3}:${ipv4Address}|(:${ipv6Segment}){1,5}|:)|` + + `(?:${ipv6Segment}:){1}(?:(:${ipv6Segment}){0,4}:${ipv4Address}|(:${ipv6Segment}){1,6}|:)|` + + `(?::((?::${ipv6Segment}){0,5}:${ipv4Address}|(?::${ipv6Segment}){1,7}|:))` + + ")(%[0-9a-zA-Z.]{1,})?$" +) + +export const ip: ip.module = arkModule({ + root: ["v4 | v6", "@", "an IP address"], + v4: regexStringNode(ipv4Matcher, "an IPv4 address"), + v6: regexStringNode(ipv6Matcher, "an IPv6 address") +}) + +export declare namespace ip { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: string + v4: string + v6: string + } +} + +const jsonStringDescription = "a JSON string" + +export const writeJsonSyntaxErrorProblem = (error: unknown): string => { + if (!(error instanceof SyntaxError)) throw error + return `must be ${jsonStringDescription} (${error})` +} + +const jsonRoot = rootSchema({ + domain: "string", + predicate: { + meta: jsonStringDescription, + predicate: (s: string, ctx) => { + try { + JSON.parse(s) + return true + } catch (e) { + return ctx.reject({ + code: "predicate", + expected: jsonStringDescription, + problem: writeJsonSyntaxErrorProblem(e) + }) + } + } + } +}) + +const parseJson: Morph = (s: string, ctx: TraversalContext) => { + if (s.length === 0) { + return ctx.error({ + code: "predicate", + expected: jsonStringDescription, + actual: "empty" + }) + } + try { + return JSON.parse(s) + } catch (e) { + return ctx.error({ + code: "predicate", + expected: jsonStringDescription, + problem: writeJsonSyntaxErrorProblem(e) + }) + } +} + +export const json: stringJson.module = arkModule({ + root: jsonRoot, + parse: rootSchema({ + in: "string", + morphs: parseJson, + declaredOut: intrinsic.json + }) +}) + +export declare namespace stringJson { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: string + parse: (In: string) => To + } +} + +const preformattedLower = regexStringNode(/^[a-z]*$/, "only lowercase letters") + +const lower: lower.module = arkModule({ + root: rootSchema({ + in: "string", + morphs: (s: string) => s.toLowerCase(), + declaredOut: preformattedLower + }), + preformatted: preformattedLower +}) + +export declare namespace lower { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: (In: string) => To + preformatted: string + } +} + +export const normalizedForms = ["NFC", "NFD", "NFKC", "NFKD"] as const + +export type NormalizedForm = (typeof normalizedForms)[number] + +const preformattedNodes = flatMorph( + normalizedForms, + (i, form) => + [ + form, + rootSchema({ + domain: "string", + predicate: (s: string) => s.normalize(form) === s, + meta: `${form}-normalized unicode` + }) + ] as const +) + +const normalizeNodes = flatMorph( + normalizedForms, + (i, form) => + [ + form, + rootSchema({ + in: "string", + morphs: (s: string) => s.normalize(form), + declaredOut: preformattedNodes[form] + }) + ] as const +) + +export const NFC = arkModule({ + root: normalizeNodes.NFC, + preformatted: preformattedNodes.NFC +}) + +export const NFD = arkModule({ + root: normalizeNodes.NFD, + preformatted: preformattedNodes.NFD +}) + +export const NFKC = arkModule({ + root: normalizeNodes.NFKC, + preformatted: preformattedNodes.NFKC +}) + +export const NFKD = arkModule({ + root: normalizeNodes.NFKD, + preformatted: preformattedNodes.NFKD +}) + +export const normalize = arkModule({ + root: "NFC", + NFC, + NFD, + NFKC, + NFKD +}) + +export declare namespace normalize { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: (In: string) => To + NFC: NFC.submodule + NFD: NFD.submodule + NFKC: NFKC.submodule + NFKD: NFKD.submodule + } + + export namespace NFC { + export type submodule = Submodule<$> + + export type $ = { + root: (In: string) => To + preformatted: string + } + } + + export namespace NFD { + export type submodule = Submodule<$> + + export type $ = { + root: (In: string) => To + preformatted: string + } + } + + export namespace NFKC { + export type submodule = Submodule<$> + + export type $ = { + root: (In: string) => To + preformatted: string + } + } + + export namespace NFKD { + export type submodule = Submodule<$> + + export type $ = { + root: (In: string) => To + preformatted: string + } + } +} + +const numericRoot = regexStringNode( + numericStringMatcher, + "a well-formed numeric string" +) + +export const numeric: stringNumeric.module = arkModule({ + root: numericRoot, + parse: rootSchema({ + in: numericRoot, + morphs: (s: string) => Number.parseFloat(s), + declaredOut: intrinsic.number + }) +}) + +export declare namespace stringNumeric { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: string + parse: (In: string) => To + } +} + +// https://semver.org/ +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-]+)*))?$/ + +const semver = regexStringNode( + semverMatcher, + "a semantic version (see https://semver.org/)" +) + +const preformattedTrim = regexStringNode( + // no leading or trailing whitespace + /^\S.*\S$|^\S?$/, + "trimmed" +) + +const trim: trim.module = arkModule({ + root: rootSchema({ + in: "string", + morphs: (s: string) => s.trim(), + declaredOut: preformattedTrim + }), + preformatted: preformattedTrim +}) + +export declare namespace trim { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: (In: string) => To + preformatted: string + } +} + +const preformattedUpper = regexStringNode(/^[A-Z]*$/, "only uppercase letters") + +const upper: upper.module = arkModule({ + root: rootSchema({ + in: "string", + morphs: (s: string) => s.toUpperCase(), + declaredOut: preformattedUpper + }), + preformatted: preformattedUpper +}) + +declare namespace upper { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: (In: string) => To + preformatted: string + } +} + +const isParsableUrl = (s: string) => { + if (URL.canParse as unknown) return URL.canParse(s) + // Can be removed once Node 18 is EOL + try { + new URL(s) + return true + } catch { + return false + } +} + +const urlRoot = rootSchema({ + domain: "string", + predicate: { + meta: "a URL string", + predicate: isParsableUrl + } +}) + +export const url: url.module = arkModule({ + root: urlRoot, + parse: rootSchema({ + declaredIn: urlRoot, + in: "string", + morphs: (s: string, ctx: TraversalContext) => { + try { + return new URL(s) + } catch { + return ctx.error("a URL string") + } + }, + declaredOut: rootSchema(URL) + }) +}) + +export declare namespace url { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: string + parse: (In: string) => To + } +} + +// Based on https://github.com/validatorjs/validator.js/blob/master/src/lib/isUUID.js +export const uuid = arkModule({ + // the meta tuple expression ensures the error message does not delegate + // to the individual branches, which are too detailed + root: ["versioned | nil | max", "@", "a UUID"], + "#nil": "'00000000-0000-0000-0000-000000000000'", + "#max": "'ffffffff-ffff-ffff-ffff-ffffffffffff'", + "#versioned": + /[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i, + v1: regexStringNode( + /^[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + "a UUIDv1" + ), + v2: regexStringNode( + /^[0-9a-f]{8}-[0-9a-f]{4}-2[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + "a UUIDv2" + ), + v3: regexStringNode( + /^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + "a UUIDv3" + ), + v4: regexStringNode( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + "a UUIDv4" + ), + v5: regexStringNode( + /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + "a UUIDv5" + ), + v6: regexStringNode( + /^[0-9a-f]{8}-[0-9a-f]{4}-6[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + "a UUIDv6" + ), + v7: regexStringNode( + /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + "a UUIDv7" + ), + v8: regexStringNode( + /^[0-9a-f]{8}-[0-9a-f]{4}-8[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + "a UUIDv8" + ) +}) + +export declare namespace uuid { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: string + v1: string + v2: string + v3: string + v4: string + v5: string + v6: string + v7: string + v8: string + } +} + +export const string = arkModule({ + root: intrinsic.string, + alpha: regexStringNode(/^[A-Za-z]*$/, "only letters"), + alphanumeric: regexStringNode(/^[A-Za-z\d]*$/, "only letters and digits 0-9"), + base64, + capitalize, + creditCard, + date: stringDate, + digits: regexStringNode(/^\d*$/, "only digits 0-9"), + email, + integer: stringInteger, + ip, + json, + lower, + normalize, + numeric, + semver, + trim, + upper, + url, + uuid +}) + +export declare namespace string { + export type module = Module + + export type submodule = Submodule<$> + + export type $ = { + root: string + alpha: string + alphanumeric: string + base64: base64.submodule + capitalize: capitalize.submodule + creditCard: string + date: stringDate.submodule + digits: string + email: string + integer: stringInteger.submodule + ip: ip.submodule + json: stringJson.submodule + lower: lower.submodule + normalize: normalize.submodule + numeric: stringNumeric.submodule + semver: string + trim: trim.submodule + upper: upper.submodule + url: url.submodule + uuid: uuid.submodule + } +} diff --git a/ark/type/keywords/string/alpha.ts b/ark/type/keywords/string/alpha.ts deleted file mode 100644 index 43ddc5354a..0000000000 --- a/ark/type/keywords/string/alpha.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Nominal, of } from "../../attributes.ts" -import { regexStringNode } from "./utils.ts" - -declare namespace string { - export type alpha = of> -} - -export const alpha = regexStringNode(/^[A-Za-z]*$/, "only letters") - -export type alpha = string.alpha diff --git a/ark/type/keywords/string/alphanumeric.ts b/ark/type/keywords/string/alphanumeric.ts deleted file mode 100644 index 389b511986..0000000000 --- a/ark/type/keywords/string/alphanumeric.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Nominal, of } from "../../attributes.ts" -import { regexStringNode } from "./utils.ts" - -declare namespace string { - export type alphanumeric = of> -} - -export const alphanumeric = regexStringNode( - /^[A-Za-z\d]*$/, - "only letters and digits 0-9" -) - -export type alphanumeric = string.alphanumeric diff --git a/ark/type/keywords/string/base64.ts b/ark/type/keywords/string/base64.ts deleted file mode 100644 index 974d3659b6..0000000000 --- a/ark/type/keywords/string/base64.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Nominal, of } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" -import { regexStringNode } from "./utils.ts" - -export const base64 = arkModule({ - root: regexStringNode( - /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/, - "base64-encoded" - ), - url: regexStringNode( - /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}(?:==|%3D%3D)?|[A-Za-z0-9_-]{3}(?:=|%3D)?)?$/, - "base64url-encoded" - ) -}) - -declare namespace string { - export type base64 = of> - - export namespace base64 { - export type url = of> - } -} - -export declare namespace base64 { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: string.base64 - url: string.base64.url - } -} diff --git a/ark/type/keywords/string/capitalize.ts b/ark/type/keywords/string/capitalize.ts deleted file mode 100644 index 685200417d..0000000000 --- a/ark/type/keywords/string/capitalize.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { rootSchema } from "@ark/schema" -import type { Nominal, of, To } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" -import { regexStringNode } from "./utils.ts" - -declare namespace string { - export type capitalized = of> -} - -const preformatted = regexStringNode(/^[A-Z].*$/, "capitalized") - -export const capitalize: capitalize.module = arkModule({ - root: rootSchema({ - in: "string", - morphs: (s: string) => s.charAt(0).toUpperCase() + s.slice(1), - declaredOut: preformatted - }), - preformatted -}) - -export declare namespace capitalize { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: (In: string) => To - preformatted: string.capitalized - } -} diff --git a/ark/type/keywords/string/creditCard.ts b/ark/type/keywords/string/creditCard.ts deleted file mode 100644 index 9afe1a3316..0000000000 --- a/ark/type/keywords/string/creditCard.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { rootSchema } from "@ark/schema" -import type { Nominal, of } from "../../attributes.ts" - -// https://github.com/validatorjs/validator.js/blob/master/src/lib/isLuhnNumber.js -export const isLuhnValid = (creditCardInput: string): boolean => { - const sanitized = creditCardInput.replace(/[- ]+/g, "") - let sum = 0 - let digit: string - let tmpNum: number - let shouldDouble = false - for (let i = sanitized.length - 1; i >= 0; i--) { - digit = sanitized.substring(i, i + 1) - tmpNum = Number.parseInt(digit, 10) - if (shouldDouble) { - tmpNum *= 2 - if (tmpNum >= 10) sum += (tmpNum % 10) + 1 - else sum += tmpNum - } else sum += tmpNum - - shouldDouble = !shouldDouble - } - return !!(sum % 10 === 0 ? sanitized : false) -} - -// https://github.com/validatorjs/validator.js/blob/master/src/lib/isCreditCard.js -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}))$/ - -declare namespace string { - export type creditCard = of> -} - -export const creditCard = rootSchema({ - domain: "string", - pattern: { - meta: "a credit card number", - rule: creditCardMatcher.source - }, - predicate: { - meta: "a credit card number", - predicate: isLuhnValid - } -}) - -export type creditCard = string.creditCard diff --git a/ark/type/keywords/string/date.ts b/ark/type/keywords/string/date.ts deleted file mode 100644 index 8797f3a4ec..0000000000 --- a/ark/type/keywords/string/date.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { - ArkErrors, - intrinsic, - rootSchema, - type TraversalContext -} from "@ark/schema" -import type { Nominal, To, of } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { number } from "../number/number.ts" -import { arkModule } from "../utils.ts" -import { integer } from "./integer.ts" -import { regexStringNode } from "./utils.ts" - -type DayDelimiter = "." | "/" | "-" - -const dayDelimiterMatcher = /^[./-]$/ - -type DayPart = DayPatterns[PartKey] - -type PartKey = keyof DayPatterns - -type DayPatterns = { - y: "yy" | "yyyy" - m: "mm" | "m" - d: "dd" | "d" -} - -type fragment = - | `${delimiter}${part}` - | "" - -export type DayPattern = - delimiter extends unknown ? - { - [k1 in keyof DayPatterns]: { - [k2 in Exclude]: `${DayPatterns[k1]}${fragment< - DayPatterns[k2], - delimiter - >}${fragment< - DayPatterns[Exclude], - delimiter - >}` - }[Exclude] - }[keyof DayPatterns] - : never - -export type DateFormat = "iso" | DayPattern - -export type DateOptions = { - format?: DateFormat -} - -// ISO 8601 date/time modernized from https://github.com/validatorjs/validator.js/blob/master/src/lib/isISO8601.js -// Based on https://tc39.es/ecma262/#sec-date-time-string-format, the T -// delimiter for date/time is mandatory. Regex from validator.js strict matcher: -export const iso8601Matcher = - /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-3])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/ - -type ParsedDayParts = { - y?: string - m?: string - d?: string -} - -const isValidDateInstance = (date: Date) => !Number.isNaN(+date) - -const writeFormattedExpected = (format: DateFormat) => - `a ${format}-formatted date` - -export const tryParseDatePattern = ( - data: string, - opts?: DateOptions -): Date | string => { - if (!opts?.format) { - const result = new Date(data) - return isValidDateInstance(result) ? result : "a valid date" - } - if (opts.format === "iso") { - return iso8601Matcher.test(data) ? - new Date(data) - : writeFormattedExpected("iso") - } - const dataParts = data.split(dayDelimiterMatcher) - // will be the first delimiter matched, if there is one - const delimiter: string | undefined = data[dataParts[0].length] - const formatParts = delimiter ? opts.format.split(delimiter) : [opts.format] - - if (dataParts.length !== formatParts.length) - return writeFormattedExpected(opts.format) - - const parsedParts: ParsedDayParts = {} - for (let i = 0; i < formatParts.length; i++) { - if ( - dataParts[i].length !== formatParts[i].length && - // if format is "m" or "d", data is allowed to be 1 or 2 characters - !(formatParts[i].length === 1 && dataParts[i].length === 2) - ) - return writeFormattedExpected(opts.format) - - parsedParts[formatParts[i][0] as PartKey] = dataParts[i] - } - - const date = new Date(`${parsedParts.m}/${parsedParts.d}/${parsedParts.y}`) - - if (`${date.getDate()}` === parsedParts.d) return date - - return writeFormattedExpected(opts.format) -} - -const isParsableDate = (s: string) => !Number.isNaN(new Date(s).valueOf()) - -const parsableDate = rootSchema({ - domain: "string", - predicate: { - meta: "a parsable date", - predicate: isParsableDate - } -}).assertHasKind("intersection") - -const epochRoot = integer.root.internal - .narrow((s, ctx) => { - // we know this is safe since it has already - // been validated as an integer string - const n = Number.parseInt(s) - const out = number.epoch(n) - if (out instanceof ArkErrors) { - ctx.errors.merge(out) - return false - } - return true - }) - .withMeta({ - description: "an integer string representing a safe Unix timestamp" - }) - .assertHasKind("intersection") - -const epoch = arkModule({ - root: epochRoot, - parse: rootSchema({ - in: epochRoot, - morphs: (s: string) => new Date(s), - declaredOut: intrinsic.Date - }) -}) - -const isoRoot = regexStringNode( - iso8601Matcher, - "an ISO 8601 (YYYY-MM-DDTHH:mm:ss.sssZ) date" -).internal.assertHasKind("intersection") - -const iso = arkModule({ - root: isoRoot, - parse: rootSchema({ - in: isoRoot, - morphs: (s: string) => new Date(s), - declaredOut: intrinsic.Date - }) -}) - -declare namespace string { - export type date = of> - - export namespace date { - export type epoch = of> - export type iso = of> - } -} - -export const stringDate: stringDate.module = arkModule({ - root: parsableDate, - parse: rootSchema({ - declaredIn: parsableDate, - in: "string", - morphs: (s: string, ctx: TraversalContext) => { - const date = new Date(s) - if (Number.isNaN(date.valueOf())) return ctx.error("a parsable date") - return date - }, - declaredOut: intrinsic.Date - }), - iso, - epoch -}) - -export declare namespace stringDate { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: string.date - parse: (In: string.date) => To - iso: iso.submodule - epoch: epoch.submodule - } - - export namespace iso { - export type submodule = Submodule<$> - - export type $ = { - root: string.date.iso - parse: (In: string.date.iso) => To - } - } - - export namespace epoch { - export type submodule = Submodule<$> - - export type $ = { - root: string.date.epoch - parse: (In: string.date.epoch) => To - } - } -} diff --git a/ark/type/keywords/string/digits.ts b/ark/type/keywords/string/digits.ts deleted file mode 100644 index 112bd7938d..0000000000 --- a/ark/type/keywords/string/digits.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Nominal, of } from "../../attributes.ts" -import { regexStringNode } from "./utils.ts" - -declare namespace string { - export type digits = of> -} - -export const digits = regexStringNode(/^\d*$/, "only digits 0-9") - -export type digits = string.digits diff --git a/ark/type/keywords/string/email.ts b/ark/type/keywords/string/email.ts deleted file mode 100644 index f522b71883..0000000000 --- a/ark/type/keywords/string/email.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Nominal, of } from "../../attributes.ts" -import { regexStringNode } from "./utils.ts" - -declare namespace string { - export type email = of> -} - -export const email = regexStringNode( - // https://www.regular-expressions.info/email.html - /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, - "an email address" -) - -export type email = string.email diff --git a/ark/type/keywords/string/integer.ts b/ark/type/keywords/string/integer.ts deleted file mode 100644 index 92d3b4b60a..0000000000 --- a/ark/type/keywords/string/integer.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { intrinsic, rootSchema, type TraversalContext } from "@ark/schema" -import { wellFormedIntegerMatcher } from "@ark/util" -import type { Nominal, of, To } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import type { number } from "../number/number.ts" -import { arkModule } from "../utils.ts" -import { regexStringNode } from "./utils.ts" - -declare namespace string { - export type integer = of> -} - -const root = regexStringNode( - wellFormedIntegerMatcher, - "a well-formed integer string" -) - -export const integer: stringInteger.module = arkModule({ - root, - parse: rootSchema({ - in: root, - morphs: (s: string, ctx: TraversalContext) => { - const parsed = Number.parseInt(s) - return Number.isSafeInteger(parsed) ? parsed : ( - ctx.error( - "an integer in the range Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER" - ) - ) - }, - declaredOut: intrinsic.integer - }) -}) - -export declare namespace stringInteger { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: string.integer - parse: (In: string.integer) => To> - } -} diff --git a/ark/type/keywords/string/ip.ts b/ark/type/keywords/string/ip.ts deleted file mode 100644 index b10af24e72..0000000000 --- a/ark/type/keywords/string/ip.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Nominal, of } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" -import { regexStringNode } from "./utils.ts" - -// Based on https://github.com/validatorjs/validator.js/blob/master/src/lib/isIP.js -// Adjusted to incorporate unmerged fix in https://github.com/validatorjs/validator.js/pull/2083 -const ipv4Segment = "(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])" -const ipv4Address = `(${ipv4Segment}[.]){3}${ipv4Segment}` -const ipv4Matcher = new RegExp(`^${ipv4Address}$`) - -const ipv6Segment = "(?:[0-9a-fA-F]{1,4})" -const ipv6Matcher = new RegExp( - "^(" + - `(?:${ipv6Segment}:){7}(?:${ipv6Segment}|:)|` + - `(?:${ipv6Segment}:){6}(?:${ipv4Address}|:${ipv6Segment}|:)|` + - `(?:${ipv6Segment}:){5}(?::${ipv4Address}|(:${ipv6Segment}){1,2}|:)|` + - `(?:${ipv6Segment}:){4}(?:(:${ipv6Segment}){0,1}:${ipv4Address}|(:${ipv6Segment}){1,3}|:)|` + - `(?:${ipv6Segment}:){3}(?:(:${ipv6Segment}){0,2}:${ipv4Address}|(:${ipv6Segment}){1,4}|:)|` + - `(?:${ipv6Segment}:){2}(?:(:${ipv6Segment}){0,3}:${ipv4Address}|(:${ipv6Segment}){1,5}|:)|` + - `(?:${ipv6Segment}:){1}(?:(:${ipv6Segment}){0,4}:${ipv4Address}|(:${ipv6Segment}){1,6}|:)|` + - `(?::((?::${ipv6Segment}){0,5}:${ipv4Address}|(?::${ipv6Segment}){1,7}|:))` + - ")(%[0-9a-zA-Z.]{1,})?$" -) - -declare namespace string { - export type ip = of> - - export namespace ip { - export type v4 = of> - export type v6 = of> - } -} - -export const ip: ip.module = arkModule({ - root: ["v4 | v6", "@", "an IP address"], - v4: regexStringNode(ipv4Matcher, "an IPv4 address"), - v6: regexStringNode(ipv6Matcher, "an IPv6 address") -}) - -export declare namespace ip { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: string.ip - v4: string.ip.v4 - v6: string.ip.v6 - } -} diff --git a/ark/type/keywords/string/json.ts b/ark/type/keywords/string/json.ts deleted file mode 100644 index 92190d674c..0000000000 --- a/ark/type/keywords/string/json.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - intrinsic, - rootSchema, - type Morph, - type TraversalContext -} from "@ark/schema" -import type { Nominal, To, of } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" - -declare namespace string { - export type json = of> -} - -const jsonStringDescription = "a JSON string" - -export const writeJsonSyntaxErrorProblem = (error: unknown): string => { - if (!(error instanceof SyntaxError)) throw error - return `must be ${jsonStringDescription} (${error})` -} - -const root = rootSchema({ - domain: "string", - predicate: { - meta: jsonStringDescription, - predicate: (s: string, ctx) => { - try { - JSON.parse(s) - return true - } catch (e) { - return ctx.reject({ - code: "predicate", - expected: jsonStringDescription, - problem: writeJsonSyntaxErrorProblem(e) - }) - } - } - } -}) - -const parseJson: Morph = (s: string, ctx: TraversalContext) => { - if (s.length === 0) { - return ctx.error({ - code: "predicate", - expected: jsonStringDescription, - actual: "empty" - }) - } - try { - return JSON.parse(s) - } catch (e) { - return ctx.error({ - code: "predicate", - expected: jsonStringDescription, - problem: writeJsonSyntaxErrorProblem(e) - }) - } -} - -export const json: stringJson.module = arkModule({ - root, - parse: rootSchema({ - in: "string", - morphs: parseJson, - declaredOut: intrinsic.json - }) -}) - -export declare namespace stringJson { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: string.json - parse: (In: string.json) => To - } -} diff --git a/ark/type/keywords/string/lower.ts b/ark/type/keywords/string/lower.ts deleted file mode 100644 index f558ee39ab..0000000000 --- a/ark/type/keywords/string/lower.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { rootSchema } from "@ark/schema" -import type { Nominal, of, To } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" -import { regexStringNode } from "./utils.ts" - -declare namespace string { - export type lowercase = of> -} - -const preformatted = regexStringNode(/^[a-z]*$/, "only lowercase letters") - -export const lower: lower.module = arkModule({ - root: rootSchema({ - in: "string", - morphs: (s: string) => s.toLowerCase(), - declaredOut: preformatted - }), - preformatted -}) - -export declare namespace lower { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: (In: string) => To - preformatted: string.lowercase - } -} diff --git a/ark/type/keywords/string/normalize.ts b/ark/type/keywords/string/normalize.ts deleted file mode 100644 index 4528a80cdc..0000000000 --- a/ark/type/keywords/string/normalize.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { rootSchema } from "@ark/schema" -import { flatMorph } from "@ark/util" -import type { Nominal, of, To } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" - -declare namespace string { - export type normalized = normalized.NFC - - export namespace normalized { - export type NFC = of> - export type NFD = of> - export type NFKC = of> - export type NFKD = of> - } -} - -export const normalizedForms = ["NFC", "NFD", "NFKC", "NFKD"] as const - -export type NormalizedForm = (typeof normalizedForms)[number] - -const preformattedNodes = flatMorph( - normalizedForms, - (i, form) => - [ - form, - rootSchema({ - domain: "string", - predicate: (s: string) => s.normalize(form) === s, - meta: `${form}-normalized unicode` - }) - ] as const -) - -const normalizeNodes = flatMorph( - normalizedForms, - (i, form) => - [ - form, - rootSchema({ - in: "string", - morphs: (s: string) => s.normalize(form), - declaredOut: preformattedNodes[form] - }) - ] as const -) - -export const NFC = arkModule({ - root: normalizeNodes.NFC, - preformatted: preformattedNodes.NFC -}) - -export const NFD = arkModule({ - root: normalizeNodes.NFD, - preformatted: preformattedNodes.NFD -}) - -export const NFKC = arkModule({ - root: normalizeNodes.NFKC, - preformatted: preformattedNodes.NFKC -}) - -export const NFKD = arkModule({ - root: normalizeNodes.NFKD, - preformatted: preformattedNodes.NFKD -}) - -export const normalize = arkModule({ - root: "NFC", - NFC, - NFD, - NFKC, - NFKD -}) - -export declare namespace normalize { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: (In: string) => To - NFC: NFC.submodule - NFD: NFD.submodule - NFKC: NFKC.submodule - NFKD: NFKD.submodule - } - - export namespace NFC { - export type submodule = Submodule<$> - - export type $ = { - root: (In: string) => To - preformatted: string.normalized.NFC - } - } - - export namespace NFD { - export type submodule = Submodule<$> - - export type $ = { - root: (In: string) => To - preformatted: string.normalized.NFD - } - } - - export namespace NFKC { - export type submodule = Submodule<$> - - export type $ = { - root: (In: string) => To - preformatted: string.normalized.NFKC - } - } - - export namespace NFKD { - export type submodule = Submodule<$> - - export type $ = { - root: (In: string) => To - preformatted: string.normalized.NFKD - } - } -} diff --git a/ark/type/keywords/string/numeric.ts b/ark/type/keywords/string/numeric.ts deleted file mode 100644 index 0030a77a9b..0000000000 --- a/ark/type/keywords/string/numeric.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { intrinsic, rootSchema } from "@ark/schema" -import { numericStringMatcher } from "@ark/util" -import type { Nominal, of, To } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" -import { regexStringNode } from "./utils.ts" - -declare namespace string { - export type numeric = of> -} - -const root = regexStringNode( - numericStringMatcher, - "a well-formed numeric string" -) - -export const numeric: stringNumeric.module = arkModule({ - root, - parse: rootSchema({ - in: root, - morphs: (s: string) => Number.parseFloat(s), - declaredOut: intrinsic.number - }) -}) - -export declare namespace stringNumeric { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: string.numeric - parse: (In: string.numeric) => To - } -} diff --git a/ark/type/keywords/string/semver.ts b/ark/type/keywords/string/semver.ts deleted file mode 100644 index 3ffb6a89fb..0000000000 --- a/ark/type/keywords/string/semver.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Nominal, of } from "../../attributes.ts" -import { regexStringNode } from "./utils.ts" - -declare namespace string { - export type semver = of> -} - -// https://semver.org/ -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-]+)*))?$/ - -export const semver = regexStringNode( - semverMatcher, - "a semantic version (see https://semver.org/)" -) - -export type semver = string.semver diff --git a/ark/type/keywords/string/string.ts b/ark/type/keywords/string/string.ts deleted file mode 100644 index d590dfb973..0000000000 --- a/ark/type/keywords/string/string.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { intrinsic } from "@ark/schema" -import type { satisfy } from "@ark/util" -import type { - Anonymous, - AtLeastLength, - AtMostLength, - AttributeKind, - Attributes, - Default, - ExactlyLength, - LengthAttributeKind, - LessThanLength, - MoreThanLength, - Nominal, - Optional, - brand, - constraint, - of -} from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" -import { alpha } from "./alpha.ts" -import { alphanumeric } from "./alphanumeric.ts" -import { base64 } from "./base64.ts" -import { capitalize } from "./capitalize.ts" -import { creditCard } from "./creditCard.ts" -import { stringDate } from "./date.ts" -import { digits } from "./digits.ts" -import { email } from "./email.ts" -import { integer, type stringInteger } from "./integer.ts" -import { ip } from "./ip.ts" -import { json, type stringJson } from "./json.ts" -import { lower } from "./lower.ts" -import { normalize } from "./normalize.ts" -import { numeric, type stringNumeric } from "./numeric.ts" -import { semver } from "./semver.ts" -import { trim } from "./trim.ts" -import { upper } from "./upper.ts" -import { url } from "./url.ts" -import { uuid } from "./uuid.ts" - -export const string = arkModule({ - root: intrinsic.string, - alpha, - alphanumeric, - base64, - capitalize, - creditCard, - date: stringDate, - digits, - email, - integer, - ip, - json, - lower, - normalize, - numeric, - semver, - trim, - upper, - url, - uuid -}) - -export type Matching = { - matching: constraint -} - -export declare namespace string { - export type atLeastLength = of> - - export type moreThanLength = of> - - export type atMostLength = of> - - export type lessThanLength = of> - - export type exactlyLength = of> - - export type matching = of> - - export type anonymous = of - - export type optional = of - - export type defaultsTo = of> - - export type nominal = of> - - export type is = of - - export type AttributableKind = satisfy< - AttributeKind, - "matching" | LengthAttributeKind - > - - export type withSingleAttribute< - kind extends AttributableKind, - value extends Attributes[kind] - > = raw.withSingleAttribute - - export namespace raw { - export type withSingleAttribute = - kind extends "nominal" ? nominal - : kind extends "matching" ? matching - : kind extends "atLeastLength" ? atLeastLength - : kind extends "atMostLength" ? atMostLength - : kind extends "moreThanLength" ? moreThanLength - : kind extends "lessThanLength" ? lessThanLength - : kind extends "optional" ? optional - : kind extends "defaultsTo" ? defaultsTo - : never - } - - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: string - alpha: alpha - alphanumeric: alphanumeric - base64: base64.submodule - capitalize: capitalize.submodule - creditCard: creditCard - date: stringDate.submodule - digits: digits - email: email - integer: stringInteger.submodule - ip: ip.submodule - json: stringJson.submodule - lower: lower.submodule - normalize: normalize.submodule - numeric: stringNumeric.submodule - semver: semver - trim: trim.submodule - upper: upper.submodule - url: url.submodule - uuid: uuid.submodule - } - - export type branded = brand> - - export namespace branded { - export type atLeastLength = brand> - - export type moreThanLength = brand> - - export type atMostLength = brand> - - export type lessThanLength = brand> - - export type exactlyLength = brand> - - export type matching = brand> - - export type anonymous = brand - - export type is = brand - - export type withSingleAttribute< - kind extends AttributableKind, - value extends Attributes[kind] - > = raw.withSingleAttribute - - export namespace raw { - export type withSingleAttribute = - kind extends "nominal" ? branded - : kind extends "matching" ? matching - : kind extends "atLeastLength" ? atLeastLength - : kind extends "atMostLength" ? atMostLength - : kind extends "moreThanLength" ? moreThanLength - : kind extends "lessThanLength" ? lessThanLength - : never - } - } -} diff --git a/ark/type/keywords/string/trim.ts b/ark/type/keywords/string/trim.ts deleted file mode 100644 index 567f4de8f9..0000000000 --- a/ark/type/keywords/string/trim.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { rootSchema } from "@ark/schema" -import type { Nominal, of, To } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" -import { regexStringNode } from "./utils.ts" - -declare namespace string { - export type trimmed = of> -} - -const preformatted = regexStringNode( - // no leading or trailing whitespace - /^\S.*\S$|^\S?$/, - "trimmed" -) - -export const trim: trim.module = arkModule({ - root: rootSchema({ - in: "string", - morphs: (s: string) => s.trim(), - declaredOut: preformatted - }), - preformatted -}) - -export declare namespace trim { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: (In: string) => To - preformatted: string.trimmed - } -} diff --git a/ark/type/keywords/string/upper.ts b/ark/type/keywords/string/upper.ts deleted file mode 100644 index b7c9afaf6f..0000000000 --- a/ark/type/keywords/string/upper.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { rootSchema } from "@ark/schema" -import type { Nominal, of, To } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" -import { regexStringNode } from "./utils.ts" - -declare namespace string { - export type uppercase = of> -} - -const preformatted = regexStringNode(/^[A-Z]*$/, "only uppercase letters") - -export const upper: upper.module = arkModule({ - root: rootSchema({ - in: "string", - morphs: (s: string) => s.toUpperCase(), - declaredOut: preformatted - }), - preformatted -}) - -export declare namespace upper { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: (In: string) => To - preformatted: string.uppercase - } -} diff --git a/ark/type/keywords/string/url.ts b/ark/type/keywords/string/url.ts deleted file mode 100644 index f69fb663da..0000000000 --- a/ark/type/keywords/string/url.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { rootSchema, type TraversalContext } from "@ark/schema" -import type { Nominal, of, To } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" - -declare namespace string { - export type url = of> -} - -const isParsableUrl = (s: string) => { - if (URL.canParse as unknown) return URL.canParse(s) - // Can be removed once Node 18 is EOL - try { - new URL(s) - return true - } catch { - return false - } -} - -const root = rootSchema({ - domain: "string", - predicate: { - meta: "a URL string", - predicate: isParsableUrl - } -}) - -export const url: url.module = arkModule({ - root, - parse: rootSchema({ - declaredIn: root as never, - in: "string", - morphs: (s: string, ctx: TraversalContext) => { - try { - return new URL(s) - } catch { - return ctx.error("a URL string") - } - }, - declaredOut: rootSchema(URL) - }) -}) - -export declare namespace url { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: string.url - parse: (In: string.url) => To - } -} diff --git a/ark/type/keywords/string/utils.ts b/ark/type/keywords/string/utils.ts deleted file mode 100644 index 6766e94e43..0000000000 --- a/ark/type/keywords/string/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { node, type IntersectionNode } from "@ark/schema" - -// Non-trivial expressions should have an explanation or attribution - -export const regexStringNode = ( - regex: RegExp, - description: string -): IntersectionNode => - node("intersection", { - domain: "string", - pattern: { - rule: regex.source, - flags: regex.flags, - meta: description - } - }) as never diff --git a/ark/type/keywords/string/uuid.ts b/ark/type/keywords/string/uuid.ts deleted file mode 100644 index ec093a81f7..0000000000 --- a/ark/type/keywords/string/uuid.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { Nominal, of } from "../../attributes.ts" -import type { Module, Submodule } from "../../module.ts" -import { arkModule } from "../utils.ts" -import { regexStringNode } from "./utils.ts" - -// Based on https://github.com/validatorjs/validator.js/blob/master/src/lib/isUUID.js -export const uuid = arkModule({ - // the meta tuple expression ensures the error message does not delegate - // to the individual branches, which are too detailed - root: ["versioned | nil | max", "@", "a UUID"], - "#nil": "'00000000-0000-0000-0000-000000000000'", - "#max": "'ffffffff-ffff-ffff-ffff-ffffffffffff'", - "#versioned": - /[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i, - v1: regexStringNode( - /^[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - "a UUIDv1" - ), - v2: regexStringNode( - /^[0-9a-f]{8}-[0-9a-f]{4}-2[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - "a UUIDv2" - ), - v3: regexStringNode( - /^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - "a UUIDv3" - ), - v4: regexStringNode( - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - "a UUIDv4" - ), - v5: regexStringNode( - /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - "a UUIDv5" - ), - v6: regexStringNode( - /^[0-9a-f]{8}-[0-9a-f]{4}-6[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - "a UUIDv6" - ), - v7: regexStringNode( - /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - "a UUIDv7" - ), - v8: regexStringNode( - /^[0-9a-f]{8}-[0-9a-f]{4}-8[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - "a UUIDv8" - ) -}) - -declare namespace string { - export type uuid = of> - - export namespace uuid { - export type v1 = of> - export type v2 = of> - export type v3 = of> - export type v4 = of> - export type v5 = of> - export type v6 = of> - export type v7 = of> - export type v8 = of> - } -} - -export declare namespace uuid { - export type module = Module - - export type submodule = Submodule<$> - - export type $ = { - root: string.uuid - v1: string.uuid.v1 - v2: string.uuid.v2 - v3: string.uuid.v3 - v4: string.uuid.v4 - v5: string.uuid.v5 - v6: string.uuid.v6 - v7: string.uuid.v7 - v8: string.uuid.v8 - } -} diff --git a/ark/type/methods/array.ts b/ark/type/methods/array.ts index 805de3a1ae..4605cf4af8 100644 --- a/ark/type/methods/array.ts +++ b/ark/type/methods/array.ts @@ -1,10 +1,8 @@ import type { ExactLength, ExclusiveNumericRangeSchema, - exclusivizeRangeSchema, InclusiveNumericRangeSchema } from "@ark/schema" -import type { associateAttributesFromArraySchema } from "../attributes.ts" import type { ObjectType } from "./object.ts" interface Type< @@ -12,39 +10,15 @@ interface Type< out t extends readonly unknown[] = readonly unknown[], $ = {} > extends ObjectType { - atLeastLength( - schema: schema - ): Type, $> + atLeastLength(schema: InclusiveNumericRangeSchema): this - atMostLength( - schema: schema - ): Type, $> + atMostLength(schema: InclusiveNumericRangeSchema): this - moreThanLength( - schema: schema - ): Type< - associateAttributesFromArraySchema< - t, - "minLength", - exclusivizeRangeSchema - >, - $ - > + moreThanLength(schema: ExclusiveNumericRangeSchema): this - lessThanLength( - schema: schema - ): Type< - associateAttributesFromArraySchema< - t, - "maxLength", - exclusivizeRangeSchema - >, - $ - > + lessThanLength(schema: ExclusiveNumericRangeSchema): this - exactlyLength( - schema: schema - ): Type, $> + exactlyLength(schema: ExactLength.Schema): this } export type { Type as ArrayType } diff --git a/ark/type/methods/base.ts b/ark/type/methods/base.ts index ad4afab821..b6b61466ec 100644 --- a/ark/type/methods/base.ts +++ b/ark/type/methods/base.ts @@ -16,22 +16,16 @@ import type { Callable, ErrorMessage, inferred, - Json, + JsonStructure, unset } from "@ark/util" import type { - associateAttributes, - associateAttributesFromSchema, - brandAttributes, - Default, - DefaultFor, + defaultFor, distill, inferIntersection, inferMorphOut, inferPipes, InferredMorph, - Nominal, - Optional, Out, To } from "../attributes.ts" @@ -40,7 +34,6 @@ import type { type } from "../keywords/keywords.ts" import type { Scope } from "../scope.ts" import type { ArrayType } from "./array.ts" import type { instantiateType } from "./instantiate.ts" - /** @ts-ignore cast variance */ interface Type extends Callable<(data: unknown) => distill.Out | ArkErrors> { @@ -55,8 +48,7 @@ interface Type // A type representing the output the `Type` will return (after morphs are // applied to valid input) infer: this["inferOut"] - inferInWithAttributes: distill.withAttributes.In - inferOutWithAttributes: distill.withAttributes.Out + inferIntrospectableOut: distill.introspectable.Out inferOut: distill.Out // A type representing the input the `Type` will accept (before morphs are applied) @@ -71,8 +63,8 @@ interface Type : true /** Internal JSON representation of this `Type` */ - json: Json - toJSON(): Json + json: JsonStructure + toJSON(): JsonStructure meta: ArkAmbient.meta precompilation: string | undefined toJsonSchema(): JsonSchema @@ -132,19 +124,15 @@ interface Type ...args: validateChainedAsArgs ): instantiateType - brand>>( + brand>( name: name ): instantiateType - brandAttributes(): instantiateType, $> - - unbrandAttributes(): instantiateType, $> - /** * A `Type` representing the deeply-extracted input of the `Type` (before morphs are applied). * @example const inputT = T.in */ - get in(): instantiateType + get in(): instantiateType /** * A `Type` representing the deeply-extracted output of the `Type` (after morphs are applied).\ * **IMPORTANT**: If your type includes morphs, their output will likely be unknown @@ -191,16 +179,7 @@ interface Type */ narrow< narrowed extends this["infer"] = never, - r = [narrowed] extends [never] ? - t extends InferredMorph ? - o extends To ? - ( - In: i - ) => To> - : ( - In: i - ) => Out> - : associateAttributesFromSchema + r = [narrowed] extends [never] ? t : t extends InferredMorph ? o extends To ? (In: i) => To @@ -212,8 +191,7 @@ interface Type satisfying< narrowed extends this["inferIn"] = never, - r = [narrowed] extends [never] ? - associateAttributesFromSchema + r = [narrowed] extends [never] ? t : t extends InferredMorph ? (In: narrowed) => o : narrowed >( @@ -262,9 +240,7 @@ interface Type reduceMapped?: (mappedBranches: mapOut[]) => reduceOut ): reduceOut - // inferring r into an alias in the return doesn't - // work the way it does for the other methods here - optional>(): instantiateType + optional(): [this, "?"] /** * Add a default value for this `Type` when it is used as a property.\ @@ -274,12 +250,9 @@ interface Type * @example const withFactory = type({ foo: type("number[]").default(() => [1])) }); withFactory({baz: 'a'}) // { foo: [1], baz: 'a' } * @example const withMorph = type({ foo: type("string.numeric.parse").default("123") }); withMorph({}) // { foo: 123 } */ - default< - const value extends this["inferIn"], - r = associateAttributes> - >( - value: DefaultFor - ): instantiateType + default>( + value: value + ): [this, "=", value] // Standard Schema Compatibility (https://github.com/standard-schema/standard-schema) "~standard": StandardSchemaV1.ArkTypeProps diff --git a/ark/type/methods/date.ts b/ark/type/methods/date.ts index f207137a7c..424cb7c563 100644 --- a/ark/type/methods/date.ts +++ b/ark/type/methods/date.ts @@ -1,43 +1,19 @@ import type { ExclusiveDateRangeSchema, - exclusivizeRangeSchema, InclusiveDateRangeSchema } from "@ark/schema" -import type { associateAttributesFromDateSchema } from "../attributes.ts" import type { ObjectType } from "./object.ts" /** @ts-ignore cast variance */ interface Type extends ObjectType { - atOrAfter( - schema: schema - ): Type, $> + atOrAfter(schema: InclusiveDateRangeSchema): this - atOrBefore( - schema: schema - ): Type, $> + atOrBefore(schema: InclusiveDateRangeSchema): this - laterThan( - schema: schema - ): Type< - associateAttributesFromDateSchema< - t, - "after", - exclusivizeRangeSchema - >, - $ - > + laterThan(schema: ExclusiveDateRangeSchema): this - earlierThan( - schema: schema - ): Type< - associateAttributesFromDateSchema< - t, - "before", - exclusivizeRangeSchema - >, - $ - > + earlierThan(schema: ExclusiveDateRangeSchema): this } export type { Type as DateType } diff --git a/ark/type/methods/number.ts b/ark/type/methods/number.ts index bdc98b4e82..93c5654056 100644 --- a/ark/type/methods/number.ts +++ b/ark/type/methods/number.ts @@ -1,47 +1,21 @@ import type { Divisor, ExclusiveNumericRangeSchema, - exclusivizeRangeSchema, InclusiveNumericRangeSchema } from "@ark/schema" -import type { associateAttributesFromNumberSchema } from "../attributes.ts" import type { BaseType } from "./base.ts" /** @ts-ignore cast variance */ interface Type extends BaseType { - divisibleBy( - schema: schema - ): Type, $> + divisibleBy(schema: Divisor.Schema): this - atLeast( - schema: schema - ): Type, $> + atLeast(schema: InclusiveNumericRangeSchema): this - atMost( - schema: schema - ): Type, $> + atMost(schema: InclusiveNumericRangeSchema): this - moreThan( - schema: schema - ): Type< - associateAttributesFromNumberSchema< - t, - "min", - exclusivizeRangeSchema - >, - $ - > + moreThan(schema: ExclusiveNumericRangeSchema): this - lessThan( - schema: schema - ): Type< - associateAttributesFromNumberSchema< - t, - "max", - exclusivizeRangeSchema - >, - $ - > + lessThan(schema: ExclusiveNumericRangeSchema): this } export type { Type as NumberType } diff --git a/ark/type/methods/object.ts b/ark/type/methods/object.ts index a447f5ae93..cea4f0408f 100644 --- a/ark/type/methods/object.ts +++ b/ark/type/methods/object.ts @@ -11,7 +11,7 @@ import type { ErrorType, inferred, intersectUnion, - Json, + JsonStructure, Key, listable, merge, @@ -19,13 +19,7 @@ import type { show, toArkKey } from "@ark/util" -import type { - associateAttributes, - Attributes, - Default, - of, - Optional -} from "../attributes.ts" +import type { Default, withDefault } from "../attributes.ts" import type { type } from "../keywords/keywords.ts" import type { ArrayType } from "./array.ts" import type { BaseType } from "./base.ts" @@ -131,40 +125,8 @@ type typePropOf = : never type typeProp = - t extends of ? - attributes extends Default ? - DefaultedTypeProp< - k & Key, - keyof attributes extends keyof Default ? base - : of< - base, - // Shouldn't need this extends check, logged a TS bug: - // https://github.com/microsoft/TypeScript/issues/60233 - Omit extends ( - infer attributes extends Attributes - ) ? - attributes - : never - >, - defaultValue, - $ - > - : attributes extends Optional ? - BaseTypeProp< - "optional", - k & Key, - keyof attributes extends keyof Optional ? base - : of< - base, - Omit extends ( - infer attributes extends Attributes - ) ? - attributes - : never - >, - $ - > - : never + t extends Default ? + DefaultedTypeProp : BaseTypeProp< k extends optionalKeyOf ? "optional" : "required", k & Key, @@ -183,7 +145,7 @@ export interface BaseTypeProp< key: k value: instantiateType meta: ArkEnv.meta - toJSON: () => Json + toJSON: () => JsonStructure } export interface DefaultedTypeProp< @@ -237,9 +199,9 @@ type fromTypeProps> = show< [prop in props[number] as Extract< applyHomomorphicOptionality, { kind: "optional"; default: unknown } - >["key"]]: associateAttributes< + >["key"]]: withDefault< prop["value"][inferred], - Default + prop["default" & keyof prop] > } > diff --git a/ark/type/methods/string.ts b/ark/type/methods/string.ts index 0830c3fdc9..089b36ec9f 100644 --- a/ark/type/methods/string.ts +++ b/ark/type/methods/string.ts @@ -1,52 +1,24 @@ import type { ExactLength, ExclusiveNumericRangeSchema, - exclusivizeRangeSchema, InclusiveNumericRangeSchema, Pattern } from "@ark/schema" -import type { associateAttributesFromStringSchema } from "../attributes.ts" import type { BaseType } from "./base.ts" /** @ts-ignore cast variance */ interface Type extends BaseType { - matching( - schema: schema - ): Type, $> + matching(schema: Pattern.Schema): this - atLeastLength( - schema: schema - ): Type, $> + atLeastLength(schema: InclusiveNumericRangeSchema): this - atMostLength( - schema: schema - ): Type, $> + atMostLength(schema: InclusiveNumericRangeSchema): this - moreThanLength( - schema: schema - ): Type< - associateAttributesFromStringSchema< - t, - "minLength", - exclusivizeRangeSchema - >, - $ - > + moreThanLength(schema: ExclusiveNumericRangeSchema): this - lessThanLength( - schema: schema - ): Type< - associateAttributesFromStringSchema< - t, - "maxLength", - exclusivizeRangeSchema - >, - $ - > + lessThanLength(schema: ExclusiveNumericRangeSchema): this - exactlyLength( - schema: schema - ): Type, $> + exactlyLength(schema: ExactLength.Schema): this } export type { Type as StringType } diff --git a/ark/type/package.json b/ark/type/package.json index c3186260ee..5c7e018989 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.26", + "version": "2.0.0-rc.27", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/type/parser/ast/generic.ts b/ark/type/parser/ast/generic.ts new file mode 100644 index 0000000000..94223595a8 --- /dev/null +++ b/ark/type/parser/ast/generic.ts @@ -0,0 +1,74 @@ +import type { + GenericAst, + GenericParamAst, + writeUnsatisfiedParameterConstraintMessage +} from "@ark/schema" +import type { array, ErrorMessage, Hkt, typeToString } from "@ark/util" +import type { UnparsedScope } from "../../scope.ts" +import type { inferDefinition } from "../definition.ts" +import type { inferAstRoot, inferExpression } from "./infer.ts" +import type { astToString } from "./utils.ts" +import type { validateAst } from "./validate.ts" + +export type GenericInstantiationAst< + generic extends GenericAst = GenericAst, + argAsts extends unknown[] = unknown[] +> = [generic, "<>", argAsts] + +export type inferGenericInstantiation< + g extends GenericAst, + argAsts extends unknown[], + $, + args +> = + g["bodyDef"] extends Hkt ? + Hkt.apply< + g["bodyDef"], + { [i in keyof argAsts]: inferExpression } + > + : inferDefinition< + g["bodyDef"], + resolveScope, + { + // intersect `${number}` to ensure that only array indices are mapped + [i in keyof g["names"] & `${number}` as g["names"][i]]: inferExpression< + argAsts[i & keyof argAsts], + resolveScope, + args + > + } + > + +export type validateGenericInstantiation< + g extends GenericAst, + argAsts extends unknown[], + $, + args +> = validateGenericArgs + +type validateGenericArgs< + params extends array, + argAsts extends array, + $, + args, + indices extends 1[] +> = + argAsts extends readonly [infer arg, ...infer argsTail] ? + validateAst extends infer e extends ErrorMessage ? e + : inferAstRoot extends params[indices["length"]][1] ? + validateGenericArgs + : ErrorMessage< + writeUnsatisfiedParameterConstraintMessage< + params[indices["length"]][0], + typeToString, + astToString + > + > + : undefined + +type resolveScope = + // If the generic was defined in the current scope, its definition can be + // resolved using the same scope as that of the input args. + g$ extends UnparsedScope ? $ + : // Otherwise, use the scope that was explicitly bound to it. + g$ diff --git a/ark/type/parser/ast/infer.ts b/ark/type/parser/ast/infer.ts index f68ae5c429..eda6253316 100644 --- a/ark/type/parser/ast/infer.ts +++ b/ark/type/parser/ast/infer.ts @@ -1,36 +1,18 @@ -import type { GenericAst } from "@ark/schema" -import type { Hkt, arkKeyOf, array } from "@ark/util" +import type { arkKeyOf, array } from "@ark/util" import type { - After, - AtLeast, - AtLeastLength, - AtMost, - AtMostLength, - AtOrAfter, - AtOrBefore, - Before, - Default, - DivisibleBy, - ExactlyLength, - LessThan, - LessThanLength, - LimitLiteral, - MoreThan, - MoreThanLength, - Nominal, - Optional, - associateAttributes, - brandAttributes, distill, inferIntersection, - normalizeLimit + LimitLiteral, + withDefault } from "../../attributes.ts" -import type { Date } from "../../keywords/constructors/Date.ts" import type { type } from "../../keywords/keywords.ts" -import type { UnparsedScope } from "../../scope.ts" import type { inferDefinition } from "../definition.ts" import type { Comparator } from "../reduce/shared.ts" import type { ArkTypeScanner } from "../shift/scanner.ts" +import type { + GenericInstantiationAst, + inferGenericInstantiation +} from "./generic.ts" export type inferAstRoot = ast extends array ? inferExpression : never @@ -51,41 +33,12 @@ export type InferredAst = [ def ] -export type GenericInstantiationAst< - generic extends GenericAst = GenericAst, - argAsts extends unknown[] = unknown[] -> = [generic, "<>", argAsts] - -type resolveScope = - // If the generic was defined in the current scope, its definition can be - // resolved using the same scope as that of the input args. - g$ extends UnparsedScope ? $ - : // Otherwise, use the scope that was explicitly bound to it. - g$ - export type inferExpression = ast extends array ? ast extends InferredAst ? resolution : ast extends DefAst ? inferDefinition : ast extends GenericInstantiationAst ? - g["bodyDef"] extends Hkt ? - Hkt.apply< - g["bodyDef"], - { [i in keyof argAsts]: inferExpression } - > - : inferDefinition< - g["bodyDef"], - resolveScope, - { - // intersect `${number}` to ensure that only array indices are mapped - [i in keyof g["names"] & - `${number}` as g["names"][i]]: inferExpression< - argAsts[i & keyof argAsts], - resolveScope, - args - > - } - > + inferGenericInstantiation : ast[1] extends "[]" ? inferExpression[] : ast[1] extends "|" ? inferExpression | inferExpression @@ -98,82 +51,19 @@ export type inferExpression = // unscoped type.infer is safe since the default value is always a literal // as of TS5.6, inlining defaultValue causes a bunch of extra types and instantiations type.infer extends infer defaultValue ? - associateAttributes< - inferExpression, - Default - > + withDefault, defaultValue> : never - : ast[1] extends "#" ? - brandAttributes, Nominal> + : ast[1] extends "#" ? type.brand, ast[2]> : ast[1] extends Comparator ? ast[0] extends LimitLiteral ? - attachBoundAttributes, ast[1], ast[0]> - : attachBoundAttributes< - inferExpression, - ast[1], - ast[2] & LimitLiteral - > - : ast[1] extends "%" ? - associateAttributes, DivisibleBy> - : ast[1] extends "?" ? - associateAttributes, Optional> + inferExpression + : inferExpression + : ast[1] extends "%" ? inferExpression + : ast[1] extends "?" ? inferExpression : ast[0] extends "keyof" ? arkKeyOf> : never : never -export type attachBoundAttributes< - inWithAttributes, - comparator extends Comparator, - limit extends LimitLiteral -> = - distill.In extends infer In ? - comparator extends "==" ? - In extends number ? limit - : In extends Date ? Date.nominal> - : associateAttributes> - : associateAttributes< - inWithAttributes, - boundToAttributes - > - : never - -export type boundToAttributes< - In, - comparator extends Comparator, - limit extends LimitLiteral -> = - In extends string | array ? - lengthBoundToAttributes - : In extends Date ? dateBoundToAttributes> - : numericBoundToAttributes - -type lengthBoundToAttributes< - comparator extends Comparator, - limit extends number -> = - comparator extends "<" ? LessThanLength - : comparator extends "<=" ? AtMostLength - : comparator extends ">" ? MoreThanLength - : AtLeastLength - -type dateBoundToAttributes< - comparator extends Comparator, - limit extends string | number -> = - comparator extends "<" ? Before - : comparator extends "<=" ? AtOrBefore - : comparator extends ">" ? After - : AtOrAfter - -type numericBoundToAttributes< - comparator extends Comparator, - limit extends number -> = - comparator extends "<" ? LessThan - : comparator extends "<=" ? AtMost - : comparator extends ">" ? MoreThan - : AtLeast - export type PrefixOperator = "keyof" | "instanceof" | "===" | "node" export type PrefixExpression< diff --git a/ark/type/parser/ast/validate.ts b/ark/type/parser/ast/validate.ts index d25618f61c..2f07395076 100644 --- a/ark/type/parser/ast/validate.ts +++ b/ark/type/parser/ast/validate.ts @@ -1,18 +1,14 @@ import type { arkKind, - GenericParamAst, PrivateDeclaration, - writeMissingSubmoduleAccessMessage, - writeUnsatisfiedParameterConstraintMessage + writeMissingSubmoduleAccessMessage } from "@ark/schema" import type { anyOrNever, - array, BigintLiteral, Completion, ErrorMessage, NumberLiteral, - typeToString, writeMalformedNumericLiteralMessage } from "@ark/util" import type { Generic } from "../../generic.ts" @@ -24,9 +20,11 @@ import type { validateRange } from "./bounds.ts" import type { validateDefault } from "./default.ts" import type { validateDivisor } from "./divisor.ts" import type { - DefAst, GenericInstantiationAst, - inferAstRoot, + validateGenericInstantiation +} from "./generic.ts" +import type { + DefAst, InferredAst, InfixExpression, PostfixExpression @@ -41,20 +39,26 @@ export type validateAst = ast[2] extends PrivateDeclaration ? ErrorMessage> : undefined - : ast extends PostfixExpression ? - operator extends "[]" ? validateAst - : operator extends "?" ? validateAst - : never + : ast extends PostfixExpression<"[]" | "?", infer operand> ? + // shallowOptionalMessage is handled in type.validate + // invalidOptionalKeyKindMessage is handled in property parsing + + // it would be natural to handle them here by adding context + // to the generic args, but it makes the cache less reusable + // (was tested and had a significant impact on repo-wide perf) + validateAst : ast extends InfixExpression ? operator extends "&" | "|" ? validateInfix : operator extends Comparator ? validateRange : operator extends "%" ? validateDivisor - : operator extends "=" ? validateDefault + : // shallowDefaultableMessage is handled in type.validate + // invalidDefaultableKeyKindMessage is handled in property parsing + operator extends "=" ? validateDefault : operator extends "#" ? validateAst : ErrorMessage>> : ast extends ["keyof", infer operand] ? validateKeyof : ast extends GenericInstantiationAst ? - validateGenericArgs + validateGenericInstantiation : ErrorMessage>> & { ast: ast } @@ -62,26 +66,6 @@ export type validateAst = type writeUnexpectedExpressionMessage = `Unexpectedly failed to parse the expression resulting from ${expression}` -type validateGenericArgs< - params extends array, - argAsts extends array, - $, - args, - indices extends 1[] -> = - argAsts extends readonly [infer arg, ...infer argsTail] ? - validateAst extends infer e extends ErrorMessage ? e - : inferAstRoot extends params[indices["length"]][1] ? - validateGenericArgs - : ErrorMessage< - writeUnsatisfiedParameterConstraintMessage< - params[indices["length"]][0], - typeToString, - astToString - > - > - : undefined - export const writePrefixedPrivateReferenceMessage = ( name: name ): writePrefixedPrivateReferenceMessage => @@ -114,15 +98,27 @@ type validateInferredAst = : undefined export type validateString = - validateAst, $, args> extends ( - infer result extends ErrorMessage - ) ? - result extends Completion ? - text - : result - : def + parseString extends infer ast ? + validateAst extends infer result extends ErrorMessage ? + // completions have the same suffix as error messages as a sentinel + // but don't want to include that in what TS suggests + result extends Completion ? + text + : result + : def + : never type validateInfix = validateAst extends infer e extends ErrorMessage ? e : validateAst extends infer e extends ErrorMessage ? e : undefined + +export const shallowOptionalMessage = + "Optional definitions like 'string?' are only valid as properties in an object or tuple" + +export type shallowOptionalMessage = typeof shallowOptionalMessage + +export const shallowDefaultableMessage = + "Defaultable definitions like 'number = 0' are only valid as properties in an object or tuple" + +export type shallowDefaultableMessage = typeof shallowDefaultableMessage diff --git a/ark/type/parser/definition.ts b/ark/type/parser/definition.ts index fb694cc5b9..0de511eb01 100644 --- a/ark/type/parser/definition.ts +++ b/ark/type/parser/definition.ts @@ -1,38 +1,81 @@ import { hasArkKind, type BaseParseContext, type BaseRoot } from "@ark/schema" import { + domainOf, + hasDomain, isThunk, objectKindOf, printable, throwParseError, - type Dict, - type ErrorMessage, - type Fn, - type Primitive, type anyOrNever, type array, type defined, + type Dict, type equals, + type ErrorMessage, + type Fn, type ifEmptyObjectLiteral, type objectKindOrDomainOf, type optionalKeyOf, + type Primitive, type requiredKeyOf, type show } from "@ark/util" import type { type } from "../keywords/keywords.ts" -import type { string } from "../keywords/string/string.ts" -import type { validateString } from "./ast/validate.ts" +import type { InnerParseResult } from "../scope.ts" +import type { + shallowDefaultableMessage, + shallowOptionalMessage, + validateString +} from "./ast/validate.ts" import { parseObjectLiteral, type inferObjectLiteral, type validateObjectLiteral } from "./objectLiteral.ts" -import type { BaseCompletions, inferString } from "./string.ts" +import type { + DefaultablePropertyTuple, + OptionalPropertyDefinition, + PossibleDefaultableStringDefinition, + validatePossibleStringDefault +} from "./property.ts" +import { + parseString, + type BaseCompletions, + type inferString +} from "./string.ts" import { - parseTuple, - type TupleExpression, - type inferTuple, - type validateTuple -} from "./tuple.ts" + maybeParseTupleExpression, + type inferTupleExpression, + type maybeValidateTupleExpression, + type TupleExpression +} from "./tupleExpressions.ts" +import { + parseTupleLiteral, + type inferTupleLiteral, + type validateTupleLiteral +} from "./tupleLiteral.ts" + +const parseCache: { + [scopeId: string]: { [def: string]: InnerParseResult } | undefined +} = {} + +export const parseInnerDefinition = ( + def: unknown, + ctx: BaseParseContext +): InnerParseResult => { + if (typeof def === "string") { + if (ctx.args && Object.keys(ctx.args).some(k => def.includes(k))) { + // we can only rely on the cache if there are no contextual + // resolutions like "this" or generic args + return parseString(def, ctx) + } + const scopeCache = (parseCache[ctx.$.id] ??= {}) + return (scopeCache[def] ??= parseString(def, ctx)) + } + return hasDomain(def, "object") ? + parseObject(def, ctx) + : throwParseError(writeBadDefinitionTypeMessage(domainOf(def))) +} export const parseObject = (def: object, ctx: BaseParseContext): BaseRoot => { const objectKind = objectKindOf(def) @@ -73,14 +116,35 @@ export type inferDefinition = : def extends ThunkCast ? t : def extends string ? inferString : def extends array ? inferTuple - : def extends RegExp ? string.matching + : def extends RegExp ? string : def extends object ? inferObjectLiteral : never +// validates a shallow definition, ensuring it does not represent an optional or +// defaultable before drilling down further. a definition is shallow if it is either... + +// 1. the root value passed to type() +// 2. a tuple expression export type validateDefinition = null extends undefined ? ErrorMessage<`'strict' or 'strictNullChecks' must be set to true in your tsconfig's 'compilerOptions'`> - : [def] extends [Terminal] ? def + : [def] extends [anyOrNever] ? def + : def extends OptionalPropertyDefinition ? + ErrorMessage + : def extends DefaultablePropertyTuple ? + ErrorMessage + : def extends PossibleDefaultableStringDefinition ? + validatePossibleStringDefault + : validateInnerDefinition + +// validates the definition without checking for optionals/defaults this should +// be used when one of these is true... + +// 1. we are validating a shallow definition and have already ensured it does not +// represent an optional or defaultable +// 2. we are validating the root of a property definition +export type validateInnerDefinition = + [def] extends [Terminal] ? def : def extends string ? validateString : def extends array ? validateTuple : def extends BadDefinitionType ? @@ -92,10 +156,24 @@ export type validateDefinition = : RegExp extends def ? def : validateObjectLiteral +export const parseTuple = (def: array, ctx: BaseParseContext): BaseRoot => + maybeParseTupleExpression(def, ctx) ?? parseTupleLiteral(def, ctx) + +export type validateTuple = + maybeValidateTupleExpression extends infer result ? + result extends null ? + validateTupleLiteral + : result + : never + +export type inferTuple = + def extends TupleExpression ? inferTupleExpression + : inferTupleLiteral + export type validateDeclared = - def extends validateDefinition ? + def extends type.validate ? validateInference - : validateDefinition + : type.validate type validateInference = def extends RegExp | type.cast | ThunkCast | TupleExpression ? diff --git a/ark/type/parser/objectLiteral.ts b/ark/type/parser/objectLiteral.ts index 7be4ef8295..fd82f59faa 100644 --- a/ark/type/parser/objectLiteral.ts +++ b/ark/type/parser/objectLiteral.ts @@ -2,17 +2,17 @@ import { normalizeIndex, type BaseParseContext, type BaseRoot, - type Index, + type Inner, type NodeSchema, - type Optional, - type Required, + type Prop, type Structure, - type UndeclaredKeyBehavior, type writeInvalidPropertyKeyMessage } from "@ark/schema" import { append, escapeChar, + isArray, + isEmptyObject, printable, stringAndSymbolicEntriesOf, throwParseError, @@ -22,50 +22,126 @@ import { type ErrorType, type EscapeChar, type Key, - type listable, type merge, type mutable, type show } from "@ark/util" -import type { of } from "../attributes.ts" -import type { astToString } from "./ast/utils.ts" import type { validateString } from "./ast/validate.ts" -import type { inferDefinition, validateDefinition } from "./definition.ts" +import type { inferDefinition } from "./definition.ts" +import { + invalidDefaultableKeyKindMessage, + invalidOptionalKeyKindMessage, + parseProperty, + type OptionalPropertyDefinition, + type validateProperty +} from "./property.ts" + +type MutableStructureSchema = mutable, 2> export const parseObjectLiteral = ( def: Dict, ctx: BaseParseContext ): BaseRoot => { let spread: Structure.Node | undefined - const structure: mutable, 2> = {} + const structure: MutableStructureSchema = {} // 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).flatMap(entry => - parseEntry(entry[0], entry[1], ctx) - ) - if (parsedEntries[0]?.kind === "spread") { - // remove the spread entry so we can iterate over the remaining entries - // expecting non-spread entries - const spreadEntry = parsedEntries.shift()! as ParsedSpreadEntry - if ( - !spreadEntry.node.hasKind("intersection") || - !spreadEntry.node.structure - ) { - return throwParseError( - writeInvalidSpreadTypeMessage(typeof spreadEntry.node.expression) - ) + const defEntries = stringAndSymbolicEntriesOf(def) + + for (const [k, v] of defEntries) { + const parsedKey = preparseKey(k) + + if (parsedKey.kind === "spread") { + if (!isEmptyObject(structure)) + return throwParseError(nonLeadingSpreadError) + const operand = ctx.$.parseOwnDefinitionFormat(v, ctx) + if (!operand.hasKind("intersection") || !operand.structure) { + return throwParseError( + writeInvalidSpreadTypeMessage(operand.expression) + ) + } + spread = operand.structure + continue } - spread = spreadEntry.node.structure - } - for (const entry of parsedEntries) { - if (entry.kind === "spread") return throwParseError(nonLeadingSpreadError) - if (entry.kind === "undeclared") { - structure.undeclared = entry.behavior + + if (parsedKey.kind === "undeclared") { + if (v !== "reject" && v !== "delete" && v !== "ignore") + throwParseError(writeInvalidUndeclaredBehaviorMessage(v)) + structure.undeclared = v continue } - structure[entry.kind] = append(structure[entry.kind], entry) as never + + const parsedValue = parseProperty(v, ctx) + const parsedEntryKey = parsedKey as PreparsedEntryKey + + if (parsedKey.kind === "required") { + if (!isArray(parsedValue)) { + appendNamedProp( + structure, + "required", + { + key: parsedKey.normalized, + value: parsedValue + }, + ctx + ) + } else { + appendNamedProp( + structure, + "optional", + parsedValue[1] === "=" ? + { + key: parsedKey.normalized, + value: parsedValue[0], + default: parsedValue[2] + } + : { + key: parsedKey.normalized, + value: parsedValue[0] + }, + ctx + ) + } + continue + } + if (isArray(parsedValue)) { + if (parsedValue[1] === "?") throwParseError(invalidOptionalKeyKindMessage) + + if (parsedValue[1] === "=") + throwParseError(invalidDefaultableKeyKindMessage) + } + + // value must be a BaseRoot at this point + + if (parsedKey.kind === "optional") { + appendNamedProp( + structure, + "optional", + { + key: parsedKey.normalized, + value: parsedValue + }, + ctx + ) + continue + } + + // must be index at this point + + const signature = ctx.$.parseOwnDefinitionFormat( + parsedEntryKey.normalized, + ctx + ) + + const normalized = normalizeIndex(signature, parsedValue, ctx.$) + + if (normalized.index) + structure.index = append(structure.index, normalized.index) + + if (normalized.required) + structure.required = append(structure.required, normalized.required) } const structureNode = ctx.$.node("structure", structure) @@ -76,13 +152,18 @@ export const parseObjectLiteral = ( }) } -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" +const appendNamedProp = ( + structure: MutableStructureSchema, + kind: kind, + inner: Inner, + ctx: BaseParseContext +) => { + structure[kind] = append( + // doesn't seem like this cast should be necessary + structure[kind] as MutableStructureSchema[Prop.Kind], + ctx.$.node(kind, inner) + ) +} export type inferObjectLiteral = show< "..." extends keyof def ? @@ -100,166 +181,153 @@ 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]: [ - def[k] - ] extends [anyOrNever] ? - def[k] - : inferDefinition -} & { - -readonly [k in keyof def as optionalKeyFrom]?: inferDefinition< + -readonly [k in keyof def as nonOptionalKeyFromEntry< + k, def[k], $, args - > + >]: inferDefinition +} & { + -readonly [k in keyof def as optionalKeyFromEntry< + k, + def[k] + >]?: def[k] extends OptionalPropertyDefinition ? + inferDefinition + : inferDefinition } 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 - ErrorType - : inferDefinition extends Key | of ? - // if the indexDef is syntactically and semantically valid, - // move on to the validating the value definition - validateDefinition - : ErrorType> - : k extends "..." ? - inferDefinition extends object ? - validateDefinition - : ErrorType>> - : k extends "+" ? UndeclaredKeyBehavior - : validateDefinition + [k in keyof def]: preparseKey extends ( + infer parsedKey extends PreparsedKey + ) ? + parsedKey extends PreparsedEntryKey<"index"> ? + validateString extends ( + ErrorMessage + ) ? + // add a nominal type here to avoid allowing the error message as input + ErrorType + : inferDefinition extends Key ? + // if the index def is syntactically and semantically valid, + // move on to the validating the value definition + validateProperty + : ErrorMessage> + : validateProperty + : never } -type nonOptionalKeyFrom = - parseKey extends PreparsedKey<"required", infer inner> ? inner - : parseKey extends PreparsedKey<"index", infer inner> ? - inferDefinition extends infer t extends Key ? - t - : never +type nonOptionalKeyFromEntry = + preparseKey extends ( + infer parsedKey extends PreparsedEntryKey<"required" | "index"> + ) ? + parsedKey["kind"] extends "index" ? + inferDefinition & Key + : [v] extends [OptionalPropertyDefinition] ? + [v] extends [anyOrNever] ? + parsedKey["normalized"] + : never + : parsedKey["normalized"] : // "..." is handled at the type root so is handled neither here nor in optionalKeyFrom // "+" has no effect on inference never -type optionalKeyFrom = - parseKey extends PreparsedKey<"optional", infer inner> ? inner : never - -export type PreparsedKey< - kind extends ParsedKeyKind = ParsedKeyKind, - key extends Key = Key -> = { - kind: kind - key: key -} - -declare namespace PreparsedKey { - export type from = t -} +type optionalKeyFromEntry = + preparseKey extends PreparsedEntryKey<"optional", infer name> ? name + : v extends OptionalPropertyDefinition ? k + : never -type ParsedKeyKind = "required" | "optional" | "index" | MetaKey +export const writeInvalidUndeclaredBehaviorMessage = ( + actual: unknown +): string => + `Value of '+' key must be 'reject', 'delete', or 'ignore' (was ${printable(actual)})` -export type MetaKey = "..." | "+" +export const nonLeadingSpreadError = + "Spread operator may only be used as the first key in an object" -export type IndexKey = `[${def}]` +export type PreparsedKey = PreparsedEntryKey | PreparsedSpecialKey -export type ParsedEntry = - | ParsedUndeclaredEntry - | ParsedSpreadEntry - | Required.Node - | Optional.Node - | Index.Node +type normalizedKeyKind = + kind extends "index" ? string : Key -export type ParsedUndeclaredEntry = { - kind: "undeclared" - behavior: UndeclaredKeyBehavior +export type PreparsedEntryKey< + kind extends EntryKeyKind = EntryKeyKind, + normalized extends normalizedKeyKind = normalizedKeyKind +> = { + kind: kind + normalized: normalized } -export type ParsedSpreadEntry = { - kind: "spread" - node: BaseRoot -} +export type PreparsedSpecialKey = + { + kind: kind + } -export const parseEntry = ( - key: Key, - value: unknown, - ctx: BaseParseContext -): listable => { - const parsedKey = parseKey(key) +declare namespace PreparsedKey { + export type from = t +} - if (parsedKey.kind === "+") { - if (value !== "reject" && value !== "delete" && value !== "ignore") - throwParseError(writeInvalidUndeclaredBehaviorMessage(value)) - return { kind: "undeclared", behavior: value } - } +export type ParsedKeyKind = EntryKeyKind | SpecialKeyKind - if (parsedKey.kind === "...") - return { kind: "spread", node: ctx.$.parseOwnDefinitionFormat(value, ctx) } +export type EntryKeyKind = "required" | "optional" | "index" - const parsedValue = ctx.$.parseOwnDefinitionFormat(value, ctx) +export type SpecialKeyKind = "spread" | "undeclared" - if (parsedKey.kind === "index") { - const signature = ctx.$.parseOwnDefinitionFormat(parsedKey.key, ctx) - const normalized = normalizeIndex(signature, parsedValue, ctx.$) - return ( - normalized.index ? - normalized.required ? - [normalized.index, ...normalized.required] - : normalized.index - : (normalized.required ?? []) - ) - } +export type MetaKey = "..." | "+" - return ctx.$.node(parsedKey.kind, { - key: parsedKey.key, - value: parsedValue - }) -} +export type IndexKey = `[${def}]` -const parseKey = (key: Key): PreparsedKey => - typeof key === "symbol" ? { kind: "required", key } +export const preparseKey = (key: Key): PreparsedKey => + typeof key === "symbol" ? { kind: "required", normalized: key } : key.at(-1) === "?" ? key.at(-2) === escapeChar ? - { kind: "required", key: `${key.slice(0, -2)}?` } + { kind: "required", normalized: `${key.slice(0, -2)}?` } : { kind: "optional", - key: key.slice(0, -1) + normalized: key.slice(0, -1) } : key[0] === "[" && key.at(-1) === "]" ? - { kind: "index", key: key.slice(1, -1) } + { kind: "index", normalized: key.slice(1, -1) } : key[0] === escapeChar && key[1] === "[" && key.at(-1) === "]" ? - { kind: "required", key: key.slice(1) } - : key === "..." ? { kind: key, key } - : key === "+" ? { kind: key, key } + { kind: "required", normalized: key.slice(1) } + : key === "..." ? { kind: "spread" } + : key === "+" ? { kind: "undeclared" } : { kind: "required", - key: + normalized: key === "\\..." ? "..." : key === "\\+" ? "+" : key } -type parseKey = - k extends `${infer inner}?` ? +export type preparseKey = + k extends symbol ? + PreparsedKey.from<{ + kind: "required" + normalized: k + }> + : k extends `${infer inner}?` ? inner extends `${infer baseName}${EscapeChar}` ? PreparsedKey.from<{ kind: "required" - key: `${baseName}?` + normalized: `${baseName}?` }> : PreparsedKey.from<{ kind: "optional" - key: inner + normalized: inner }> - : k extends MetaKey ? PreparsedKey.from<{ kind: k; key: k }> + : k extends "+" ? { kind: "undeclared" } + : k extends "..." ? { kind: "spread" } : k extends `${EscapeChar}${infer escapedMeta extends MetaKey}` ? - PreparsedKey.from<{ kind: "required"; key: escapedMeta }> + PreparsedKey.from<{ kind: "required"; normalized: escapedMeta }> : k extends IndexKey ? PreparsedKey.from<{ kind: "index" - key: def + normalized: def }> : PreparsedKey.from<{ kind: "required" - key: k extends `${EscapeChar}${infer escapedIndexKey extends IndexKey}` ? + normalized: k extends ( + `${EscapeChar}${infer escapedIndexKey extends IndexKey}` + ) ? escapedIndexKey : k extends Key ? k : `${k & number}` @@ -270,5 +338,5 @@ export const writeInvalidSpreadTypeMessage = ( ): writeInvalidSpreadTypeMessage => `Spread operand must resolve to an object literal type (was ${def})` -type writeInvalidSpreadTypeMessage = +export type writeInvalidSpreadTypeMessage = `Spread operand must resolve to an object literal type (was ${def})` diff --git a/ark/type/parser/property.ts b/ark/type/parser/property.ts new file mode 100644 index 0000000000..7c7eae7a0d --- /dev/null +++ b/ark/type/parser/property.ts @@ -0,0 +1,127 @@ +import type { + BaseParseContext, + BaseRoot, + UndeclaredKeyBehavior +} from "@ark/schema" +import { + isArray, + type anyOrNever, + type ErrorMessage, + type ErrorType, + type typeToString +} from "@ark/util" +import type { validateString } from "./ast/validate.ts" +import { + parseInnerDefinition, + type inferDefinition, + type validateInnerDefinition +} from "./definition.ts" +import type { + ParsedKeyKind, + writeInvalidSpreadTypeMessage +} from "./objectLiteral.ts" +import type { ParsedDefaultableProperty } from "./shift/operator/default.ts" +import type { parseString } from "./string.ts" + +export type ParsedPropertyKind = "plain" | "optional" | "defaultable" + +export type ParsedProperty = + | ParsedRequiredProperty + | ParsedOptionalProperty + | ParsedDefaultableProperty + +export type ParsedRequiredProperty = BaseRoot + +export type ParsedOptionalProperty = readonly [BaseRoot, "?"] + +export const parseProperty = ( + def: unknown, + ctx: BaseParseContext +): ParsedProperty => { + if (isArray(def)) { + if (def[1] === "=") + return [ctx.$.parseOwnDefinitionFormat(def[0], ctx), "=", def[2]] + + if (def[1] === "?") + return [ctx.$.parseOwnDefinitionFormat(def[0], ctx), "?"] + } + + // string-embedded defaults/optionals are handled by the string parser + return parseInnerDefinition(def, ctx) +} + +export type validateProperty = + [def] extends [anyOrNever] ? + /** this extra [anyOrNever] check is required to ensure that nested `type` invocations + * like the following are not prematurely validated by the outer call: + * + * ```ts + * type({ + * "test?": type("string").pipe(x => x === "true") + * }) + * ``` + */ + def + : keyKind extends "spread" ? + validateSpread, $, args> + : keyKind extends "undeclared" ? UndeclaredKeyBehavior + : keyKind extends "required" ? validateInnerDefinition + : // check to ensure we don't have an optional or defaultable value on + // an already optional or index key + def extends OptionalPropertyDefinition ? + ErrorMessage + : def extends DefaultablePropertyTuple ? + ErrorMessage + : def extends PossibleDefaultableStringDefinition ? + validatePossibleStringDefault< + def, + $, + args, + invalidDefaultableKeyKindMessage + > + : validateInnerDefinition + +export type validatePossibleStringDefault< + def extends string, + $, + args, + errorMessage extends string +> = + parseString extends DefaultablePropertyTuple ? + ErrorMessage + : validateString + +type validateSpread = + inferredProperty extends object ? validateInnerDefinition + : ErrorType>> + +export type OptionalPropertyDefinition = + | OptionalPropertyTuple + | OptionalPropertyString + +export type OptionalPropertyString = + `${baseDef}?` + +export type OptionalPropertyTuple = readonly [baseDef, "?"] + +// a precise type for a defaultable string definition +// isn't possible due to arbitrary whitespace surrounding "=", +// so that must be checked by parsing and testing if +// the root is a DefaultablePropertyTuple +export type PossibleDefaultableStringDefinition = `${string}=${string}` + +export type DefaultablePropertyTuple< + baseDef = unknown, + thunkableProperty = unknown +> = readonly [baseDef, "=", thunkableProperty] + +// single quote use here is better for TypeScript's inlined error to avoid escapes +export const invalidOptionalKeyKindMessage = `Only required keys may make their values optional, e.g. { [mySymbol]: ['number', '?'] }` + +export type invalidOptionalKeyKindMessage = typeof invalidOptionalKeyKindMessage + +// single quote use here is better for TypeScript's inlined error to avoid escapes +export const invalidDefaultableKeyKindMessage = `Only required keys may specify default values, e.g. { value: 'number = 0' }` + +export type invalidDefaultableKeyKindMessage = + typeof invalidDefaultableKeyKindMessage diff --git a/ark/type/parser/reduce/dynamic.ts b/ark/type/parser/reduce/dynamic.ts index 2bffb03f06..12e3efaf54 100644 --- a/ark/type/parser/reduce/dynamic.ts +++ b/ark/type/parser/reduce/dynamic.ts @@ -35,7 +35,7 @@ export type DynamicStateWithRoot = requireKeys export class DynamicState { // set root type to `any` so that all constraints can be applied - root: BaseRoot | undefined + root: BaseRoot | undefined branches: BranchState = { prefixes: [], leftBound: null, diff --git a/ark/type/parser/shift/operand/enclosed.ts b/ark/type/parser/shift/operand/enclosed.ts index 8956dba67c..df687098c8 100644 --- a/ark/type/parser/shift/operand/enclosed.ts +++ b/ark/type/parser/shift/operand/enclosed.ts @@ -1,6 +1,4 @@ import { isKeyOf, throwParseError, type Scanner } from "@ark/util" -import type { string } from "../../../attributes.ts" -import type { Date } from "../../../keywords/constructors/Date.ts" import type { InferredAst } from "../../ast/infer.ts" import type { DynamicState } from "../../reduce/dynamic.ts" import type { StaticState, state } from "../../reduce/static.ts" @@ -67,8 +65,8 @@ export type parseEnclosed< s, InferredAst< enclosingStart extends EnclosingQuote ? scanned - : enclosingStart extends "/" ? string.matching - : Date.nominal, + : enclosingStart extends "/" ? string + : Date, `${enclosingStart}${scanned}${EnclosingTokens[enclosingStart]}` >, nextUnscanned extends ArkTypeScanner.shift ? diff --git a/ark/type/parser/shift/operand/unenclosed.ts b/ark/type/parser/shift/operand/unenclosed.ts index a4d0849a0d..30516c159e 100644 --- a/ark/type/parser/shift/operand/unenclosed.ts +++ b/ark/type/parser/shift/operand/unenclosed.ts @@ -21,7 +21,8 @@ import { } from "@ark/util" import type { ArkAmbient } from "../../../config.ts" import type { resolutionToAst } from "../../../scope.ts" -import type { GenericInstantiationAst, InferredAst } from "../../ast/infer.ts" +import type { GenericInstantiationAst } from "../../ast/generic.ts" +import type { InferredAst } from "../../ast/infer.ts" import { writePrefixedPrivateReferenceMessage } from "../../ast/validate.ts" import type { DynamicState } from "../../reduce/dynamic.ts" import type { StaticState, state } from "../../reduce/static.ts" diff --git a/ark/type/parser/shift/operator/default.ts b/ark/type/parser/shift/operator/default.ts index e01eef5986..96677d09ed 100644 --- a/ark/type/parser/shift/operator/default.ts +++ b/ark/type/parser/shift/operator/default.ts @@ -18,7 +18,11 @@ export type UnitLiteral = | DateLiteral | UnitLiteralKeyword -export const parseDefault = (s: DynamicStateWithRoot): BaseRoot => { +export type ParsedDefaultableProperty = readonly [BaseRoot, "=", unknown] + +export const parseDefault = ( + s: DynamicStateWithRoot +): ParsedDefaultableProperty => { // store the node that will be bounded const baseNode = s.unsetRoot() s.parseOperand() @@ -31,7 +35,7 @@ export const parseDefault = (s: DynamicStateWithRoot): BaseRoot => { defaultNode.unit instanceof Date ? () => new Date(defaultNode.unit as Date) : defaultNode.unit - return baseNode.default(defaultValue) + return [baseNode, "=", defaultValue] } export type parseDefault = diff --git a/ark/type/parser/string.ts b/ark/type/parser/string.ts index f1eea1abad..877c50862d 100644 --- a/ark/type/parser/string.ts +++ b/ark/type/parser/string.ts @@ -1,13 +1,13 @@ -import type { BaseRoot, resolvableReferenceIn } from "@ark/schema" +import type { BaseParseContext, resolvableReferenceIn } from "@ark/schema" import { throwInternalError, throwParseError, type ErrorMessage } from "@ark/util" import type { ArkAmbient } from "../config.ts" -import type { resolutionToAst } from "../scope.ts" +import type { InnerParseResult, resolutionToAst } from "../scope.ts" import type { inferAstRoot } from "./ast/infer.ts" -import type { DynamicState, DynamicStateWithRoot } from "./reduce/dynamic.ts" +import { DynamicState, type DynamicStateWithRoot } from "./reduce/dynamic.ts" import type { StringifiablePrefixOperator } from "./reduce/shared.ts" import type { state, StaticState } from "./reduce/static.ts" import type { parseOperand } from "./shift/operand/operand.ts" @@ -16,6 +16,30 @@ import { writeUnexpectedCharacterMessage, type parseOperator } from "./shift/operator/operator.ts" +import { ArkTypeScanner } from "./shift/scanner.ts" + +export const parseString = ( + def: string, + ctx: BaseParseContext +): InnerParseResult => { + const aliasResolution = ctx.$.maybeResolveRoot(def) + if (aliasResolution) return aliasResolution + + const aliasArrayResolution = + def.endsWith("[]") ? + ctx.$.maybeResolveRoot(def.slice(0, -2))?.array() + : undefined + + if (aliasArrayResolution) return aliasArrayResolution + + const s = new DynamicState(new ArkTypeScanner(def), ctx) + + const node = fullStringParse(s) + + if (s.finalizer === ">") throwParseError(writeUnexpectedCharacterMessage(">")) + + return node +} /** * Try to parse the definition from right to left using the most common syntax. @@ -45,9 +69,9 @@ export type BaseCompletions<$, args, otherSuggestions extends string = never> = | StringifiablePrefixOperator | otherSuggestions -export const fullStringParse = (s: DynamicState): BaseRoot => { +export const fullStringParse = (s: DynamicState): InnerParseResult => { s.parseOperand() - let result = parseUntilFinalizer(s).root + let result: InnerParseResult = parseUntilFinalizer(s).root if (!result) { return throwInternalError( `Root was unexpectedly unset after parsing string '${s.scanner.scanned}'` @@ -55,7 +79,7 @@ export const fullStringParse = (s: DynamicState): BaseRoot => { } if (s.finalizer === "=") result = parseDefault(s as DynamicStateWithRoot) - else if (s.finalizer === "?") result = result.optional() + else if (s.finalizer === "?") result = [result, "?"] s.scanner.shiftUntilNonWhitespace() if (s.scanner.lookahead) { diff --git a/ark/type/parser/tuple.ts b/ark/type/parser/tuple.ts deleted file mode 100644 index 650103e00d..0000000000 --- a/ark/type/parser/tuple.ts +++ /dev/null @@ -1,589 +0,0 @@ -import { - $ark, - Disjoint, - intersectNodesRoot, - makeRootAndArrayPropertiesMutable, - type BaseParseContext, - type BaseRoot, - type MetaSchema, - type Morph, - type mutableInnerOfKind, - type nodeOfKind, - type Predicate, - type Union -} from "@ark/schema" -import { - append, - isEmptyObject, - objectKindOrDomainOf, - throwParseError, - type anyOrNever, - type array, - type BuiltinObjectKind, - type conform, - type Constructor, - type Domain, - type ErrorMessage, - type show, - type Thunk -} from "@ark/util" -import type { - associateAttributes, - Default, - DefaultFor, - distill, - inferIntersection, - inferMorphOut, - inferPredicate, - InferredOptional, - Optional, - Out -} from "../attributes.ts" -import type { type } from "../keywords/keywords.ts" -import type { PostfixExpression } from "./ast/infer.ts" -import type { inferDefinition, validateDefinition } from "./definition.ts" -import { writeMissingRightOperandMessage } from "./shift/operand/unenclosed.ts" -import type { ArkTypeScanner } from "./shift/scanner.ts" -import type { BaseCompletions } from "./string.ts" - -export const parseTuple = (def: array, ctx: BaseParseContext): BaseRoot => - maybeParseTupleExpression(def, ctx) ?? parseTupleLiteral(def, ctx) - -export const parseTupleLiteral = ( - def: array, - ctx: BaseParseContext -): BaseRoot => { - let sequences: mutableInnerOfKind<"sequence">[] = [{}] - let i = 0 - while (i < def.length) { - let spread = false - if (def[i] === "..." && i < def.length - 1) { - spread = true - i++ - } - - const element = ctx.$.parseOwnDefinitionFormat(def[i], ctx) - - i++ - if (spread) { - if (!element.extends($ark.intrinsic.Array)) - return throwParseError(writeNonArraySpreadMessage(element.expression)) - - // a spread must be distributed over branches e.g.: - // def: [string, ...(number[] | [true, false])] - // nodes: [string, ...number[]] | [string, true, false] - sequences = sequences.flatMap(base => - // since appendElement mutates base, we have to shallow-ish clone it for each branch - element.distribute(branch => - appendSpreadBranch(makeRootAndArrayPropertiesMutable(base), branch) - ) - ) - } else { - sequences = sequences.map(base => - appendElement( - base, - element.meta.optional ? "optional" : "required", - element - ) - ) - } - } - return ctx.$.parseSchema( - sequences.map(sequence => - isEmptyObject(sequence) ? - { - proto: Array, - exactLength: 0 - } - : ({ - proto: Array, - sequence - } as const) - ) - ) -} - -type ElementKind = "optional" | "required" | "variadic" - -const appendElement = ( - base: mutableInnerOfKind<"sequence">, - kind: ElementKind, - element: BaseRoot -): mutableInnerOfKind<"sequence"> => { - switch (kind) { - case "required": - if (base.optionals) - // e.g. [string?, number] - return throwParseError(requiredPostOptionalMessage) - if (base.variadic) { - // e.g. [...string[], number] - base.postfix = append(base.postfix, element) - } else { - // e.g. [string, number] - base.prefix = append(base.prefix, element) - } - return base - case "optional": - if (base.variadic) - // e.g. [...string[], number?] - return throwParseError(optionalPostVariadicMessage) - // e.g. [string, number?] - base.optionals = append(base.optionals, element) - return base - case "variadic": - // e.g. [...string[], number, ...string[]] - if (base.postfix) throwParseError(multipleVariadicMesage) - if (base.variadic) { - if (!base.variadic.equals(element)) { - // e.g. [...string[], ...number[]] - throwParseError(multipleVariadicMesage) - } - // e.g. [...string[], ...string[]] - // do nothing, second spread doesn't change the type - } else { - // e.g. [string, ...number[]] - base.variadic = element.internal - } - return base - } -} - -const appendSpreadBranch = ( - base: mutableInnerOfKind<"sequence">, - branch: nodeOfKind -): mutableInnerOfKind<"sequence"> => { - const spread = branch.firstReferenceOfKind("sequence") - if (!spread) { - // the only array with no sequence reference is unknown[] - return appendElement(base, "variadic", $ark.intrinsic.unknown) - } - spread.prefix?.forEach(node => appendElement(base, "required", node)) - spread.optionals?.forEach(node => appendElement(base, "optional", node)) - if (spread.variadic) appendElement(base, "variadic", spread.variadic) - spread.postfix?.forEach(node => appendElement(base, "required", node)) - return base -} - -const maybeParseTupleExpression = ( - def: array, - ctx: BaseParseContext -): 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 -} - -// It is *extremely* important we use readonly any time we check a tuple against -// something like this. Not doing so will always cause the check to fail, since -// def is declared as a const parameter. -type InfixExpression = readonly [ - unknown, - ArkTypeScanner.InfixToken, - ...unknown[] -] - -export type validateTuple = - def extends IndexZeroExpression ? validatePrefixExpression - : def extends PostfixExpression ? validatePostfixExpression - : def extends InfixExpression ? validateInfixExpression - : def extends ( - readonly ["", ...unknown[]] | readonly [unknown, "", ...unknown[]] - ) ? - readonly [ - def[0] extends "" ? BaseCompletions<$, args, IndexZeroOperator | "..."> - : def[0], - def[1] extends "" ? BaseCompletions<$, args, IndexOneOperator | "..."> - : def[1] - ] - : validateTupleLiteral - -export type validateTupleLiteral = - parseSequence extends infer s extends SequenceParseState ? - Readonly - : never - -type inferTupleLiteral = - parseSequence extends infer s extends SequenceParseState ? - s["inferred"] - : never - -type SequenceParseState = { - unscanned: array - inferred: array - validated: array - includesOptional: boolean -} - -type parseSequence = parseNextElement< - { - unscanned: def - inferred: [] - validated: [] - includesOptional: false - }, - $, - args -> - -type PreparsedElement = { - head: unknown - tail: array - inferred: unknown - optional: boolean - spread: boolean -} - -declare namespace PreparsedElement { - export type from = result -} - -type preparseNextElement = - s["unscanned"] extends readonly ["...", infer head, ...infer tail] ? - inferDefinition extends infer t ? - [t] extends [anyOrNever] ? - PreparsedElement.from<{ - head: head - tail: tail - inferred: t - optional: false - spread: true - }> - : [t] extends [InferredOptional] ? - PreparsedElement.from<{ - head: head - tail: tail - inferred: base - // this will be an error we have to handle - optional: true - spread: true - }> - : PreparsedElement.from<{ - head: head - tail: tail - inferred: t - optional: false - spread: true - }> - : never - : s["unscanned"] extends readonly [infer head, ...infer tail] ? - inferDefinition extends infer t ? - [t] extends [anyOrNever] ? - PreparsedElement.from<{ - head: head - tail: tail - inferred: t - optional: false - spread: false - }> - : [t] extends [InferredOptional] ? - PreparsedElement.from<{ - head: head - tail: tail - inferred: base - optional: true - spread: false - }> - : PreparsedElement.from<{ - head: head - tail: tail - inferred: t - optional: false - spread: false - }> - : never - : null - -type parseNextElement = - preparseNextElement extends infer next extends PreparsedElement ? - parseNextElement< - { - unscanned: next["tail"] - inferred: next["spread"] extends true ? - [...s["inferred"], ...conform] - : next["optional"] extends true ? [...s["inferred"], next["inferred"]?] - : [...s["inferred"], next["inferred"]] - validated: [ - ...s["validated"], - ...(next["spread"] extends true ? - [ - next["inferred"] extends infer spreadOperand extends array ? - [number, number] extends ( - [s["inferred"]["length"], spreadOperand["length"]] - ) ? - ErrorMessage - : "..." - : ErrorMessage> - ] - : []), - next["optional"] extends true ? - next["spread"] extends true ? ErrorMessage - : number extends s["inferred"]["length"] ? - ErrorMessage - : validateDefinition - : [s["includesOptional"], next["spread"]] extends [true, false] ? - ErrorMessage - : validateDefinition - ] - includesOptional: s["includesOptional"] | next["optional"] extends ( - false - ) ? - false - : true - }, - $, - args - > - : s - -export const writeNonArraySpreadMessage = ( - operand: operand -): writeNonArraySpreadMessage => - `Spread element must be an array (was ${operand})` as never - -type writeNonArraySpreadMessage = - `Spread element must be an array${operand extends string ? ` (was ${operand})` - : ""}` - -export const multipleVariadicMesage = - "A tuple may have at most one variadic element" - -type multipleVariadicMessage = typeof multipleVariadicMesage - -export const requiredPostOptionalMessage = - "A required element may not follow an optional element" - -type requiredPostOptionalMessage = typeof requiredPostOptionalMessage - -export const optionalPostVariadicMessage = - "An optional element may not follow a variadic element" - -type optionalPostVariadicMessage = typeof optionalPostVariadicMessage - -export const spreadOptionalMessage = "A spread element cannot be optional" - -type spreadOptionalMessage = typeof optionalPostVariadicMessage - -export type inferTuple = - def extends TupleExpression ? inferTupleExpression - : inferTupleLiteral - -export type inferTupleExpression = - def[1] extends "[]" ? inferDefinition[] - : def[1] extends "&" ? - inferIntersection< - inferDefinition, - inferDefinition - > - : def[1] extends "|" ? - inferDefinition | inferDefinition - : def[1] extends ":" ? - inferPredicate, def[2]> - : def[1] extends "=>" ? parseMorph - : def[1] extends "@" ? inferDefinition - : def[1] extends "=" ? - associateAttributes< - inferDefinition, - Default ? returnValue : def[2]> - > - : def[1] extends "?" ? - associateAttributes, Optional> - : def extends readonly ["===", ...infer values] ? values[number] - : def extends ( - readonly ["instanceof", ...infer constructors extends Constructor[]] - ) ? - InstanceType - : def[0] extends "keyof" ? inferKeyOfExpression - : never - -export type validatePrefixExpression = - def["length"] extends 1 ? readonly [writeMissingRightOperandMessage] - : def[0] extends "keyof" ? - readonly [def[0], validateDefinition] - : def[0] extends "===" ? readonly [def[0], ...unknown[]] - : def[0] extends "instanceof" ? readonly [def[0], ...Constructor[]] - : never - -export type validatePostfixExpression< - def extends PostfixExpression, - $, - args - // conform here is needed to preserve completions for shallow tuple - // expressions at index 1 after TS 5.1 -> = conform, "[]" | "?"]> - -export type validateInfixExpression = - def["length"] extends 2 ? - readonly [def[0], writeMissingRightOperandMessage] - : readonly [ - validateDefinition, - def[1], - def[1] extends "|" ? validateDefinition - : def[1] extends "&" ? validateDefinition - : def[1] extends ":" ? Predicate> - : def[1] extends "=>" ? Morph> - : def[1] extends "@" ? MetaSchema - : def[1] extends "=" ? DefaultFor> - : validateDefinition - ] - -export type UnparsedTupleExpressionInput = { - instanceof: Constructor - "===": unknown -} - -export type UnparsedTupleOperator = show - -export const parseKeyOfTuple: PrefixParser<"keyof"> = (def, ctx) => - ctx.$.parseOwnDefinitionFormat(def[1], ctx).keyof() - -export type inferKeyOfExpression = show< - keyof inferDefinition -> - -const parseBranchTuple: IndexOneParser<"|" | "&"> = (def, ctx) => { - if (def[2] === undefined) - return throwParseError(writeMissingRightOperandMessage(def[1], "")) - - const l = ctx.$.parseOwnDefinitionFormat(def[0], ctx) - const r = ctx.$.parseOwnDefinitionFormat(def[2], ctx) - if (def[1] === "|") return ctx.$.node("union", { branches: [l, r] }) - const result = intersectNodesRoot(l, r, ctx.$) - if (result instanceof Disjoint) return result.throw() - return result -} - -const parseArrayTuple: IndexOneParser<"[]"> = (def, ctx) => - ctx.$.parseOwnDefinitionFormat(def[0], ctx).array() - -export type IndexOneParser = ( - def: IndexOneExpression, - ctx: BaseParseContext -) => BaseRoot - -export type PrefixParser = ( - def: IndexZeroExpression, - ctx: BaseParseContext -) => BaseRoot - -export type TupleExpression = IndexZeroExpression | IndexOneExpression - -export type TupleExpressionOperator = IndexZeroOperator | IndexOneOperator - -export type IndexOneOperator = TuplePostfixOperator | TupleInfixOperator - -export type TuplePostfixOperator = "[]" | "?" - -export type TupleInfixOperator = "&" | "|" | "=>" | ":" | "@" | "=" - -export type IndexOneExpression< - token extends IndexOneOperator = IndexOneOperator -> = readonly [unknown, token, ...unknown[]] - -const isIndexOneExpression = (def: array): def is IndexOneExpression => - indexOneParsers[def[1] as IndexOneOperator] !== undefined - -export const parseMorphTuple: IndexOneParser<"=>"> = (def, ctx) => { - if (typeof def[2] !== "function") { - return throwParseError( - writeMalformedFunctionalExpressionMessage("=>", def[2]) - ) - } - return ctx.$.parseOwnDefinitionFormat(def[0], ctx).pipe(def[2] as Morph) -} - -export const writeMalformedFunctionalExpressionMessage = ( - operator: ":" | "=>", - value: unknown -): string => - `${ - operator === ":" ? "Narrow" : "Morph" - } expression requires a function following '${operator}' (was ${typeof value})` - -export type parseMorph = - morph extends Morph ? - inferMorphOut extends infer out ? - ( - In: distill.withAttributes.In> - ) => Out - : never - : never - -export const parseNarrowTuple: IndexOneParser<":"> = (def, ctx) => { - if (typeof def[2] !== "function") { - return throwParseError( - writeMalformedFunctionalExpressionMessage(":", def[2]) - ) - } - return ctx.$.parseOwnDefinitionFormat(def[0], ctx).constrain( - "predicate", - def[2] as Predicate - ) -} - -const parseAttributeTuple: IndexOneParser<"@"> = (def, ctx) => - ctx.$.parseOwnDefinitionFormat(def[0], ctx).configureShallowDescendants( - def[2] as never - ) - -const parseDefaultTuple: IndexOneParser<"="> = (def, ctx) => - ctx.$.parseOwnDefinitionFormat(def[0], ctx).default(def[2] as never) - -const parseOptionalTuple: IndexOneParser<"?"> = (def, ctx) => - ctx.$.parseOwnDefinitionFormat(def[0], ctx).optional() - -const indexOneParsers: { - [token in IndexOneOperator]: IndexOneParser -} = { - "[]": parseArrayTuple, - "?": parseOptionalTuple, - "|": parseBranchTuple, - "&": parseBranchTuple, - ":": parseNarrowTuple, - "=>": parseMorphTuple, - "@": parseAttributeTuple, - "=": parseDefaultTuple -} - -export type IndexZeroOperator = "keyof" | "instanceof" | "===" - -export type IndexZeroExpression< - token extends IndexZeroOperator = IndexZeroOperator -> = readonly [token, ...unknown[]] - -const prefixParsers: { - [token in IndexZeroOperator]: PrefixParser -} = { - keyof: parseKeyOfTuple, - instanceof: (def, ctx) => { - if (typeof def[1] !== "function") { - return throwParseError( - writeInvalidConstructorMessage(objectKindOrDomainOf(def[1])) - ) - } - const branches = def - .slice(1) - .map(ctor => - typeof ctor === "function" ? - ctx.$.node("proto", { proto: ctor as Constructor }) - : throwParseError( - writeInvalidConstructorMessage(objectKindOrDomainOf(ctor)) - ) - ) - return branches.length === 1 ? - branches[0] - : ctx.$.node("union", { branches }) - }, - "===": (def, ctx) => ctx.$.units(def.slice(1)) -} - -const isIndexZeroExpression = (def: array): def is IndexZeroExpression => - prefixParsers[def[0] as IndexZeroOperator] !== undefined - -export const writeInvalidConstructorMessage = < - actual extends Domain | BuiltinObjectKind ->( - actual: actual -): string => - `Expected a constructor following 'instanceof' operator (was ${actual})` diff --git a/ark/type/parser/tupleExpressions.ts b/ark/type/parser/tupleExpressions.ts new file mode 100644 index 0000000000..e24dbbd78d --- /dev/null +++ b/ark/type/parser/tupleExpressions.ts @@ -0,0 +1,273 @@ +import { + Disjoint, + intersectNodesRoot, + type BaseParseContext, + type BaseRoot, + type MetaSchema, + type Morph, + type Predicate, + type unwrapDefault +} from "@ark/schema" +import { + objectKindOrDomainOf, + throwParseError, + type array, + type BuiltinObjectKind, + type Constructor, + type Domain, + type show +} from "@ark/util" +import type { + defaultFor, + distill, + inferIntersection, + inferMorphOut, + inferPredicate, + Out, + withDefault +} from "../attributes.ts" +import type { type } from "../keywords/keywords.ts" +import { + shallowDefaultableMessage, + shallowOptionalMessage +} from "./ast/validate.ts" +import type { inferDefinition, validateDefinition } from "./definition.ts" +import { writeMissingRightOperandMessage } from "./shift/operand/unenclosed.ts" +import type { ArkTypeScanner } from "./shift/scanner.ts" +import type { BaseCompletions } from "./string.ts" + +export const maybeParseTupleExpression = ( + def: array, + ctx: BaseParseContext +): BaseRoot | null => + isIndexZeroExpression(def) ? prefixParsers[def[0]](def as never, ctx) + : isIndexOneExpression(def) ? indexOneParsers[def[1]](def as never, ctx) + : null + +export type maybeValidateTupleExpression = + def extends IndexZeroExpression ? validatePrefixExpression + : def extends IndexOneExpression ? validateIndexOneExpression + : def extends ( + readonly ["", ...unknown[]] | readonly [unknown, "", ...unknown[]] + ) ? + readonly [ + def[0] extends "" ? BaseCompletions<$, args, IndexZeroOperator | "..."> + : def[0], + def[1] extends "" ? BaseCompletions<$, args, IndexOneOperator | "..."> + : def[1] + ] + : null + +export type inferTupleExpression = + def[1] extends "[]" ? inferDefinition[] + : def[1] extends "?" ? inferDefinition + : def[1] extends "&" ? + inferIntersection< + inferDefinition, + inferDefinition + > + : def[1] extends "|" ? + inferDefinition | inferDefinition + : def[1] extends ":" ? + inferPredicate, def[2]> + : def[1] extends "=>" ? parseMorph + : def[1] extends "=" ? + withDefault, unwrapDefault> + : def[1] extends "@" ? inferDefinition + : def extends readonly ["===", ...infer values] ? values[number] + : def extends ( + readonly ["instanceof", ...infer constructors extends Constructor[]] + ) ? + InstanceType + : def[0] extends "keyof" ? inferKeyOfExpression + : never + +export type validatePrefixExpression = + def["length"] extends 1 ? readonly [writeMissingRightOperandMessage] + : def[0] extends "keyof" ? + readonly [def[0], validateDefinition] + : def[0] extends "===" ? readonly [def[0], ...unknown[]] + : def[0] extends "instanceof" ? readonly [def[0], ...Constructor[]] + : never + +export type validateIndexOneExpression< + def extends IndexOneExpression, + $, + args +> = + def[1] extends TuplePostfixOperator ? + // use type.validate here since optional/defaultables are not allowed + // within tuple expressions + readonly [validateDefinition, def[1]] + : readonly [ + validateDefinition, + def["length"] extends 2 ? writeMissingRightOperandMessage + : def[1], + def[1] extends "|" ? validateDefinition + : def[1] extends "&" ? validateDefinition + : def[1] extends ":" ? Predicate> + : def[1] extends "=>" ? Morph> + : def[1] extends "=" ? defaultFor> + : def[1] extends "@" ? MetaSchema + : validateDefinition + ] + +export type UnparsedTupleExpressionInput = { + instanceof: Constructor + "===": unknown +} + +export type UnparsedTupleOperator = show + +export const parseKeyOfTuple: PrefixParser<"keyof"> = (def, ctx) => + ctx.$.parseOwnDefinitionFormat(def[1], ctx).keyof() + +export type inferKeyOfExpression = show< + keyof inferDefinition +> + +const parseBranchTuple: IndexOneParser<"|" | "&"> = (def, ctx) => { + if (def[2] === undefined) + return throwParseError(writeMissingRightOperandMessage(def[1], "")) + + const l = ctx.$.parseOwnDefinitionFormat(def[0], ctx) + const r = ctx.$.parseOwnDefinitionFormat(def[2], ctx) + if (def[1] === "|") return ctx.$.node("union", { branches: [l, r] }) + const result = intersectNodesRoot(l, r, ctx.$) + if (result instanceof Disjoint) return result.throw() + return result +} + +const parseArrayTuple: IndexOneParser<"[]"> = (def, ctx) => + ctx.$.parseOwnDefinitionFormat(def[0], ctx).array() + +export type IndexOneParser = ( + def: IndexOneExpression, + ctx: BaseParseContext +) => BaseRoot + +export type PrefixParser = ( + def: IndexZeroExpression, + ctx: BaseParseContext +) => BaseRoot + +export type TupleExpression = IndexZeroExpression | IndexOneExpression + +export type TupleExpressionOperator = IndexZeroOperator | IndexOneOperator + +export type IndexOneOperator = TuplePostfixOperator | TupleInfixOperator + +export type TuplePostfixOperator = "[]" | "?" + +export type TupleInfixOperator = "&" | "|" | "=>" | "=" | ":" | "@" + +export type IndexOneExpression< + token extends IndexOneOperator = IndexOneOperator +> = readonly [unknown, token, ...unknown[]] + +const isIndexOneExpression = (def: array): def is IndexOneExpression => + indexOneParsers[def[1] as IndexOneOperator] !== undefined + +export const parseMorphTuple: IndexOneParser<"=>"> = (def, ctx) => { + if (typeof def[2] !== "function") { + return throwParseError( + writeMalformedFunctionalExpressionMessage("=>", def[2]) + ) + } + return ctx.$.parseOwnDefinitionFormat(def[0], ctx).pipe(def[2] as Morph) +} + +export const writeMalformedFunctionalExpressionMessage = ( + operator: ":" | "=>", + value: unknown +): string => + `${ + operator === ":" ? "Narrow" : "Morph" + } expression requires a function following '${operator}' (was ${typeof value})` + +export type parseMorph = + morph extends Morph ? + inferMorphOut extends infer out ? + (In: distill.In>) => Out + : never + : never + +export const parseNarrowTuple: IndexOneParser<":"> = (def, ctx) => { + if (typeof def[2] !== "function") { + return throwParseError( + writeMalformedFunctionalExpressionMessage(":", def[2]) + ) + } + return ctx.$.parseOwnDefinitionFormat(def[0], ctx).constrain( + "predicate", + def[2] as Predicate + ) +} + +const parseAttributeTuple: IndexOneParser<"@"> = (def, ctx) => + ctx.$.parseOwnDefinitionFormat(def[0], ctx).configureShallowDescendants( + def[2] as never + ) + +const indexOneParsers: { + [token in IndexOneOperator]: IndexOneParser +} = { + "[]": parseArrayTuple, + "|": parseBranchTuple, + "&": parseBranchTuple, + ":": parseNarrowTuple, + "=>": parseMorphTuple, + "@": parseAttributeTuple, + // since object and tuple literals parse there via `parseProperty`, + // they must be shallow if parsed directly as a tuple expression + "=": () => throwParseError(shallowDefaultableMessage), + "?": () => throwParseError(shallowOptionalMessage) +} + +export type IndexZeroOperator = "keyof" | "instanceof" | "===" + +export type IndexZeroExpression< + token extends IndexZeroOperator = IndexZeroOperator +> = readonly [token, ...unknown[]] + +export type InfixExpression = readonly [ + unknown, + ArkTypeScanner.InfixToken, + ...unknown[] +] + +const prefixParsers: { + [token in IndexZeroOperator]: PrefixParser +} = { + keyof: parseKeyOfTuple, + instanceof: (def, ctx) => { + if (typeof def[1] !== "function") { + return throwParseError( + writeInvalidConstructorMessage(objectKindOrDomainOf(def[1])) + ) + } + const branches = def + .slice(1) + .map(ctor => + typeof ctor === "function" ? + ctx.$.node("proto", { proto: ctor as Constructor }) + : throwParseError( + writeInvalidConstructorMessage(objectKindOrDomainOf(ctor)) + ) + ) + return branches.length === 1 ? + branches[0] + : ctx.$.node("union", { branches }) + }, + "===": (def, ctx) => ctx.$.units(def.slice(1)) +} + +const isIndexZeroExpression = (def: array): def is IndexZeroExpression => + prefixParsers[def[0] as IndexZeroOperator] !== undefined + +export const writeInvalidConstructorMessage = < + actual extends Domain | BuiltinObjectKind +>( + actual: actual +): string => + `Expected a constructor following 'instanceof' operator (was ${actual})` diff --git a/ark/type/parser/tupleLiteral.ts b/ark/type/parser/tupleLiteral.ts new file mode 100644 index 0000000000..fcec5973a0 --- /dev/null +++ b/ark/type/parser/tupleLiteral.ts @@ -0,0 +1,356 @@ +import { + $ark, + makeRootAndArrayPropertiesMutable, + type BaseParseContext, + type BaseRoot, + type mutableInnerOfKind, + type nodeOfKind, + type Sequence, + type Union +} from "@ark/schema" +import { + append, + isArray, + isEmptyObject, + throwParseError, + type array, + type conform, + type ErrorMessage, + type satisfy +} from "@ark/util" +import type { inferDefinition, validateInnerDefinition } from "./definition.ts" +import { + parseProperty, + type DefaultablePropertyTuple, + type OptionalPropertyDefinition, + type PossibleDefaultableStringDefinition +} from "./property.ts" + +export const parseTupleLiteral = ( + def: array, + ctx: BaseParseContext +): BaseRoot => { + let sequences: mutableInnerOfKind<"sequence">[] = [{}] + let i = 0 + while (i < def.length) { + let spread = false + if (def[i] === "..." && i < def.length - 1) { + spread = true + i++ + } + + const parsedProperty = parseProperty(def[i], ctx) + + const [valueNode, operator, possibleDefaultValue] = + !isArray(parsedProperty) ? [parsedProperty] : parsedProperty + + i++ + if (spread) { + if (!valueNode.extends($ark.intrinsic.Array)) + return throwParseError(writeNonArraySpreadMessage(valueNode.expression)) + + // a spread must be distributed over branches e.g.: + // def: [string, ...(number[] | [true, false])] + // nodes: [string, ...number[]] | [string, true, false] + sequences = sequences.flatMap(base => + // since appendElement mutates base, we have to shallow-ish clone it for each branch + valueNode.distribute(branch => + appendSpreadBranch(makeRootAndArrayPropertiesMutable(base), branch) + ) + ) + } else { + sequences = sequences.map(base => { + if (operator === "?") return appendOptionalElement(base, valueNode) + + if (operator === "=") + return appendDefaultableElement(base, valueNode, possibleDefaultValue) + + return appendRequiredElement(base, valueNode) + }) + } + } + + return ctx.$.parseSchema( + sequences.map(sequence => + isEmptyObject(sequence) ? + { + proto: Array, + exactLength: 0 + } + : ({ + proto: Array, + sequence + } as const) + ) + ) +} + +export type validateTupleLiteral = + parseSequence extends infer s extends SequenceParseState ? + Readonly + : never + +export type inferTupleLiteral = + parseSequence extends infer s extends SequenceParseState ? + s["inferred"] + : never + +const appendRequiredElement = ( + base: mutableInnerOfKind<"sequence">, + element: BaseRoot +): mutableInnerOfKind<"sequence"> => { + if (base.optionals) + // e.g. [string?, number] + return throwParseError(requiredPostOptionalMessage) + if (base.variadic) { + // e.g. [...string[], number] + base.postfix = append(base.postfix, element) + } else { + // e.g. [string, number] + base.prefix = append(base.prefix, element) + } + return base +} + +const appendOptionalElement = ( + base: mutableInnerOfKind<"sequence">, + element: BaseRoot +): mutableInnerOfKind<"sequence"> => { + if (base.variadic) + // e.g. [...string[], number?] + return throwParseError(optionalPostVariadicMessage) + // e.g. [string, number?] + base.optionals = append(base.optionals, element) + return base +} + +const appendDefaultableElement = ( + base: mutableInnerOfKind<"sequence">, + element: BaseRoot, + value: unknown +): mutableInnerOfKind<"sequence"> => { + if (base.variadic) + // e.g. [...string[], number = 0] + return throwParseError(defaultablePostVariadicMessage) + if (base.optionals) + // e.g. [string?, number = 0] + return throwParseError(defaultablePostOptionalMessage) + + // value's assignability to element will be checked when the + // sequence is instantiated by @ark/schema + // e.g. [string, number = 0] + base.defaultables = append(base.defaultables, [[element, value]]) + return base +} + +const appendVariadicElement = ( + base: mutableInnerOfKind<"sequence">, + element: BaseRoot +): mutableInnerOfKind<"sequence"> => { + // e.g. [...string[], number, ...string[]] + if (base.postfix) throwParseError(multipleVariadicMesage) + if (base.variadic) { + if (!base.variadic.equals(element)) { + // e.g. [...string[], ...number[]] + throwParseError(multipleVariadicMesage) + } + // e.g. [...string[], ...string[]] + // do nothing, second spread doesn't change the type + } else { + // e.g. [string, ...number[]] + base.variadic = element.internal + } + return base +} + +const appendSpreadBranch = ( + base: mutableInnerOfKind<"sequence">, + branch: nodeOfKind +): mutableInnerOfKind<"sequence"> => { + const spread = branch.firstReferenceOfKind("sequence") + if (!spread) { + // the only array with no sequence reference is unknown[] + return appendVariadicElement(base, $ark.intrinsic.unknown) + } + + spread.prefix?.forEach(node => appendRequiredElement(base, node)) + spread.optionals?.forEach(node => appendOptionalElement(base, node)) + if (spread.variadic) appendVariadicElement(base, spread.variadic) + spread.postfix?.forEach(node => appendRequiredElement(base, node)) + + return base +} + +type SequenceParsePhase = satisfy< + keyof Sequence.Inner, + "prefix" | "optionals" | "defaultables" | "postfix" +> + +type SequenceParseState = { + unscanned: array + inferred: array + validated: array + phase: SequenceParsePhase +} + +type parseSequence = parseNextElement< + { + unscanned: def + inferred: [] + validated: [] + phase: "prefix" + }, + $, + args +> + +type PreparsedElementKind = "required" | "optionals" | "defaultables" + +type PreparsedElement = { + head: unknown + tail: array + inferred: unknown + validated: unknown + kind: PreparsedElementKind + spread: boolean +} + +declare namespace PreparsedElement { + export type from = result +} + +type preparseNextState = + s["unscanned"] extends readonly ["...", infer head, ...infer tail] ? + preparseNextElement + : s["unscanned"] extends readonly [infer head, ...infer tail] ? + preparseNextElement + : null + +type preparseNextElement< + head, + tail extends array, + spread extends boolean, + $, + args +> = PreparsedElement.from<{ + head: head + tail: tail + inferred: inferDefinition + validated: validateInnerDefinition + // if inferredHead is optional and the element is spread, this will be an error + // handled in nextValidatedSpreadElements + kind: head extends OptionalPropertyDefinition ? "optionals" + : head extends DefaultablePropertyTuple ? "defaultables" + : // TODO: more precise + head extends PossibleDefaultableStringDefinition ? "defaultables" + : "required" + spread: spread +}> + +type parseNextElement = + preparseNextState extends infer next extends PreparsedElement ? + parseNextElement< + { + unscanned: next["tail"] + inferred: nextInferred + validated: nextValidated + phase: next["kind"] extends "optionals" | "defaultables" ? next["kind"] + : number extends nextInferred["length"] ? "postfix" + : "prefix" + }, + $, + args + > + : s + +type nextInferred = + next["spread"] extends true ? + [...s["inferred"], ...conform] + : next["kind"] extends "optionals" ? [...s["inferred"], next["inferred"]?] + : [...s["inferred"], next["inferred"]] + +type nextValidated< + s extends SequenceParseState, + next extends PreparsedElement +> = [ + ...s["validated"], + ...nextValidatedSpreadOperatorIfPresent, + nextValidatedElement +] + +type nextValidatedSpreadOperatorIfPresent< + s extends SequenceParseState, + next extends PreparsedElement +> = + next["spread"] extends true ? + [ + next["inferred"] extends infer spreadOperand extends array ? + // if the spread operand is a fixed-length tuple, it won't be a variadic element + // and therefore doesn't need to be validated as one + [s["phase"], number] extends ["postfix", spreadOperand["length"]] ? + ErrorMessage + : "..." + : ErrorMessage> + ] + : [] + +type nextValidatedElement< + s extends SequenceParseState, + next extends PreparsedElement +> = + next["kind"] extends "optionals" ? + next["spread"] extends true ? ErrorMessage + : s["phase"] extends "postfix" ? ErrorMessage + : next["validated"] + : next["kind"] extends "defaultables" ? + next["spread"] extends true ? ErrorMessage + : s["phase"] extends "optionals" ? + ErrorMessage + : s["phase"] extends "postfix" ? + ErrorMessage + : next["validated"] + : [s["phase"], next["spread"]] extends ["optionals" | " defaults", false] ? + ErrorMessage + : next["validated"] + +export const writeNonArraySpreadMessage = ( + operand: operand +): writeNonArraySpreadMessage => + `Spread element must be an array (was ${operand})` as never + +type writeNonArraySpreadMessage = + `Spread element must be an array${operand extends string ? ` (was ${operand})` + : ""}` + +export const multipleVariadicMesage = + "A tuple may have at most one variadic element" + +type multipleVariadicMessage = typeof multipleVariadicMesage + +export const requiredPostOptionalMessage = + "A required element may not follow an optional element" + +type requiredPostOptionalMessage = typeof requiredPostOptionalMessage + +export const optionalPostVariadicMessage = + "An optional element may not follow a variadic element" + +type optionalPostVariadicMessage = typeof optionalPostVariadicMessage + +export const spreadOptionalMessage = "A spread element cannot be optional" + +type spreadOptionalMessage = typeof spreadOptionalMessage + +export const spreadDefaultableMessage = "A spread element cannot have a default" + +type spreadDefaultableMessage = typeof spreadDefaultableMessage + +export const defaultablePostVariadicMessage = + "A defaultable element may not follow a variadic element" + +type defaultablePostVariadicMessage = typeof defaultablePostVariadicMessage + +export const defaultablePostOptionalMessage = + "A defaultable element may not follow an optional element without a default" + +type defaultablePostOptionalMessage = typeof defaultablePostOptionalMessage diff --git a/ark/type/scope.ts b/ark/type/scope.ts index 2bb8f8108f..da881050bb 100644 --- a/ark/type/scope.ts +++ b/ark/type/scope.ts @@ -28,19 +28,18 @@ import { type writeDuplicateAliasError } from "@ark/schema" import { - domainOf, flatMorph, - hasDomain, + isArray, isThunk, throwParseError, + type Brand, type Dict, type ErrorType, - type Json, + type JsonStructure, type anyOrNever, type array, type flattenListable, - type noSuggest, - type nominal + type noSuggest } from "@ark/util" import type { ArkSchemaRegistry } from "./config.ts" import { @@ -61,15 +60,16 @@ import type { } from "./module.ts" import type { DefAst, InferredAst } from "./parser/ast/infer.ts" import { - parseObject, - writeBadDefinitionTypeMessage, - type inferDefinition, - type validateDefinition + shallowDefaultableMessage, + shallowOptionalMessage +} from "./parser/ast/validate.ts" +import { + parseInnerDefinition, + type inferDefinition } from "./parser/definition.ts" -import { DynamicState } from "./parser/reduce/dynamic.ts" -import { writeUnexpectedCharacterMessage } from "./parser/shift/operator/operator.ts" +import type { ParsedOptionalProperty } from "./parser/property.ts" +import type { ParsedDefaultableProperty } from "./parser/shift/operator/default.ts" import { ArkTypeScanner } from "./parser/shift/scanner.ts" -import { fullStringParse } from "./parser/string.ts" import { InternalTypeParser, type DeclarationParser, @@ -105,7 +105,7 @@ export type ModuleParser = ( export type bindThis = { this: Def } /** nominal type for an unparsed definition used during scope bootstrapping */ -type Def = nominal +type Def = Brand /** sentinel indicating a scope that will be associated with a generic has not yet been parsed */ export type UnparsedScope = "$" @@ -184,8 +184,6 @@ export interface InternalScope { } export class InternalScope<$ extends {} = {}> extends BaseScope<$> { - private parseCache: Record = {} - protected cacheGetter( name: name, value: this[name] @@ -264,42 +262,20 @@ export class InternalScope<$ extends {} = {}> extends BaseScope<$> { const isScopeAlias = ctx.alias && ctx.alias in this.aliases // if the definition being parsed is not a scope alias and is not a - // generic instantiation (i.e. opts don't include args), add this as a resolution. + // generic instantiation (i.e. opts don't include args), add `this` as a resolution. + // if we're parsing a nested string, ctx.args will have already been set if (!isScopeAlias && !ctx.args) ctx.args = { this: ctx.id } - if (typeof def === "string") { - if (ctx.args && Object.keys(ctx.args).some(k => def.includes(k))) { - // we can only rely on the cache if there are no contextual - // resolutions like "this" or generic args - return this.parseString(def, ctx) - } - return (this.parseCache[def] ??= this.parseString(def, ctx)) - } - return hasDomain(def, "object") ? - parseObject(def, ctx) - : throwParseError(writeBadDefinitionTypeMessage(domainOf(def))) - } - - parseString(def: string, ctx: BaseParseContext): BaseRoot { - const aliasResolution = this.maybeResolveRoot(def) - if (aliasResolution) return aliasResolution + const result = parseInnerDefinition(def, ctx) - const aliasArrayResolution = - def.endsWith("[]") ? - this.maybeResolveRoot(def.slice(0, -2))?.array() - : undefined + if (isArray(result)) { + if (result[1] === "=") return throwParseError(shallowDefaultableMessage) - if (aliasArrayResolution) return aliasArrayResolution - - const s = new DynamicState(new ArkTypeScanner(def), ctx) - - const node = fullStringParse(s) - - if (s.finalizer === ">") - throwParseError(writeUnexpectedCharacterMessage(">")) + if (result[1] === "?") return throwParseError(shallowOptionalMessage) + } - return node as never + return result } unit: UnitTypeParser<$> = value => this.units([value]) as never @@ -345,8 +321,8 @@ export declare namespace scope { PrivateDeclaration ) ? ErrorType> - : validateDefinition, {}> - : validateDefinition< + : type.validate, {}> + : type.validate< def[k], bootstrapAliases, baseGenericConstraints @@ -366,7 +342,7 @@ export interface Scope<$ = {}> { [arkKind]: "scope" config: ArkScopeConfig references: readonly BaseNode[] - json: Json + json: JsonStructure exportedNames: array> /** The set of names defined at the root-level of the scope mapped to their @@ -441,3 +417,8 @@ type parseGenericScopeKey = { name: name params: parseGenericParams> } + +export type InnerParseResult = + | BaseRoot + | ParsedOptionalProperty + | ParsedDefaultableProperty diff --git a/ark/type/type.ts b/ark/type/type.ts index 38db4d6dd9..4a13d946ae 100644 --- a/ark/type/type.ts +++ b/ark/type/type.ts @@ -15,7 +15,7 @@ import { type array, type conform } from "@ark/util" -import type { DefaultFor, distill } from "./attributes.ts" +import type { distill } from "./attributes.ts" import type { Generic, GenericParser, @@ -27,15 +27,12 @@ import type { import type { Ark, keywords, type } from "./keywords/keywords.ts" import type { BaseType } from "./methods/base.ts" import type { instantiateType } from "./methods/instantiate.ts" -import type { - validateDeclared, - validateDefinition -} from "./parser/definition.ts" +import type { validateDeclared } from "./parser/definition.ts" import type { IndexOneOperator, IndexZeroOperator, TupleInfixOperator -} from "./parser/tuple.ts" +} from "./parser/tupleExpressions.ts" import type { InternalScope, ModuleParser, @@ -43,7 +40,6 @@ import type { ScopeParser, bindThis } from "./scope.ts" - /** The convenience properties attached to `type` */ export type TypeParserAttachments = // map over to remove call signatures @@ -56,7 +52,7 @@ export interface TypeParser<$ = {}> extends Ark.boundTypeAttachments<$> { ( params: validateParameterString, - def: validateDefinition< + def: type.validate< def, $, baseGenericConstraints> @@ -81,7 +77,6 @@ export interface TypeParser<$ = {}> extends Ark.boundTypeAttachments<$> { one extends ":" ? [Predicate>>] : one extends "=>" ? [Morph>, unknown>] : one extends "@" ? [MetaSchema] - : one extends "=" ? [DefaultFor, $>>>] : [type.validate] : [] ): r diff --git a/ark/util/__tests__/traits.test.ts b/ark/util/__tests__/traits.test.ts index b248b00c9f..c6160679ae 100644 --- a/ark/util/__tests__/traits.test.ts +++ b/ark/util/__tests__/traits.test.ts @@ -147,7 +147,7 @@ contextualize(() => { class B extends Trait<{ abstractMethods: { b(): number } }> {} // @ts-expect-error attest(class C extends implement(A, B, {}) {}).type.errors( - `Type '{}' is missing the following properties from type '{ a: () => number; b: () => number; }': a, b` + `Type '{}' is missing the following properties from type '{ b: () => number; a: () => number; }': b, a` ) }) diff --git a/ark/util/arrays.ts b/ark/util/arrays.ts index e14c6bb8fb..1b9a08fa58 100644 --- a/ark/util/arrays.ts +++ b/ark/util/arrays.ts @@ -174,7 +174,7 @@ export type AppendOptions = { */ export const append = < to extends unknown[] | undefined, - value extends listable<(to & {})[number]> + value extends appendableValue >( to: to, value: value, @@ -198,6 +198,14 @@ export const append = < return to as never } +// ensure a nested array element is not treated as a list to append +export type appendableValue = + to extends array ? + element extends array ? + array + : listable + : never + /** * Concatenates an element or list with a readonly list * @@ -206,7 +214,7 @@ export const append = < */ export const conflatenate = ( to: readonly element[] | undefined | null, - elementOrList: listable | undefined | null + elementOrList: appendableValue | undefined | null ): readonly element[] => { if (elementOrList === undefined || elementOrList === null) return to ?? ([] as never) @@ -219,13 +227,12 @@ export const conflatenate = ( /** * Concatenates a variadic list of elements or lists with a readonly list * - * @param {to} to - The base list. * @param {elementsOrLists} elementsOrLists - The elements or lists to concatenate. */ export const conflatenateAll = ( ...elementsOrLists: (listable | undefined | null)[] ): readonly element[] => - elementsOrLists.reduce(conflatenate, []) + elementsOrLists.reduce(conflatenate as never, []) export interface ComparisonOptions { isEqual?: (l: t, r: t) => boolean diff --git a/ark/util/generics.ts b/ark/util/generics.ts index 72e107ea05..349ef35ffe 100644 --- a/ark/util/generics.ts +++ b/ark/util/generics.ts @@ -69,10 +69,12 @@ export type exactEquals = */ export const keyNonimal = " keyNonimal" -export type nominal = t & { +export type Brand = t & { readonly [keyNonimal]: [t, id] } +export type unbrand = t extends Brand ? base : never + export type satisfy = t export type defined = t & ({} | null) diff --git a/ark/util/package.json b/ark/util/package.json index fec368e0a3..5965a14feb 100644 --- a/ark/util/package.json +++ b/ark/util/package.json @@ -1,6 +1,6 @@ { "name": "@ark/util", - "version": "0.26.0", + "version": "0.27.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/util/registry.ts b/ark/util/registry.ts index 9e203df908..977b79b882 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.26.0" +export const arkUtilVersion = "0.27.0" export const initialRegistryContents = { version: arkUtilVersion, diff --git a/ark/util/serialize.ts b/ark/util/serialize.ts index cdee394064..470b040cc5 100644 --- a/ark/util/serialize.ts +++ b/ark/util/serialize.ts @@ -12,17 +12,17 @@ export type SerializationOptions = { onBigInt?: (value: bigint) => string } -export type Json = JsonObject | JsonArray +export type JsonStructure = JsonObject | JsonArray export interface JsonObject { - [k: string]: JsonData + [k: string]: Json } -export type JsonArray = JsonData[] +export type JsonArray = Json[] export type JsonPrimitive = string | boolean | number | null -export type JsonData = Json | JsonPrimitive +export type Json = JsonStructure | JsonPrimitive export const snapshot = ( data: t, diff --git a/eslint.config.js b/eslint.config.js index 75cf021a90..fa38e5275e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -178,13 +178,18 @@ export default tseslint.config( } }, { - files: ["**/ark/attest/**", "**/ark/fs/**", "**/ark/docs/**"], + files: [ + "**/ark/attest/**", + "**/ark/fs/**", + "**/ark/docs/**", + "**/ark/fuma/**" + ], rules: { "import/no-extraneous-dependencies": "warn" } }, { - files: ["**/ark/repo/**", "**/ark/docs/**"], + files: ["**/ark/repo/**", "**/ark/docs/**", "**/ark/fuma/**"], rules: { "@typescript-eslint/explicit-module-boundary-types": "off" } diff --git a/package.json b/package.json index 32754f452a..a86267406f 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,8 @@ "prettier-plugin-astro": "0.14.1", "tsx": "4.19.2", "typescript": "catalog:", - "typescript-eslint": "8.17.0" + "typescript-eslint": "8.17.0", + "vitest": "2.1.8" }, "mocha": { "//": "IF YOU UPDATE THE MOCHA CONFIG HERE, PLEASE ALSO UPDATE ark/repo/mocha.jsonc AND .vscode/settings.json", diff --git a/tsconfig.json b/tsconfig.json index c3677030bd..72a611d736 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,5 +7,5 @@ "types": ["mocha", "node"] // "noErrorTruncation": true }, - "exclude": ["**/out", "**/node_modules", "./ark/docs"] + "exclude": ["**/out", "**/node_modules", "./ark/docs", "./ark/fuma"] }