diff --git a/.changeset/eager-brooms-carry.md b/.changeset/eager-brooms-carry.md new file mode 100644 index 00000000..889c0d4c --- /dev/null +++ b/.changeset/eager-brooms-carry.md @@ -0,0 +1,9 @@ +--- +"@traversable/schema-to-json-schema": patch +"@traversable/derive-validators": patch +"@traversable/schema-generator": patch +"@traversable/registry": patch +"@traversable/schema": patch +--- + +feat(generator): adds a shadcn-like schema generator feature diff --git a/.changeset/great-ghosts-fix.md b/.changeset/great-ghosts-fix.md deleted file mode 100644 index 3ea61144..00000000 --- a/.changeset/great-ghosts-fix.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -"@traversable/schema": patch ---- - -## fixes - -At some point, I broke object schemas. - -Previously, object schemas _preserved the structure_ of object they were given, even -across the type-level transformation of applying property optionality. - -The fancy name for this kind of transformation (one that _preserves structure_) is a -[_homomorphism_](https://en.wikipedia.org/wiki/Homomorphism). - -If you're curious how this idea applies to TypeScript, specifically, see this -excellent [StackOverflow answer](https://stackoverflow.com/a/59791889) by @jcalz: his -explanation is approachable, thorough, and enjoyable to read. - -Anyway, at some point I made a change that caused the compiler to lose its reference -to the property it was mapping over, which means that any metadata that was attached -to the original node (like JSDoc comments, for example) is no longer preserved. - -This PR fixes that. - -```typescript -import { t } from '@traversable/schema' - -let User = t.object({ - /** ## {@link User.def.quirks `User.quirks`} */ - quirks: t.array(t.string) -}) - -declare let x: unknown - -if (User(x)) { - x.quirks - // Hovering over `x.quirks` property now shows the JSDoc annotations - // we added to `User.quirks` -} -``` diff --git a/README.md b/README.md index 649bae65..9296fabc 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,18 @@

- NPM Version + NPM Version   TypeScript   Static Badge   - npm + npm  
- npm bundle size (scoped) + npm bundle size (scoped)   Static Badge   @@ -34,14 +34,14 @@   •   TypeScript Playground   •   - npm + npm


-`@traversable/schema` exploits a TypeScript feature called +`@traversable/schema-core` exploits a TypeScript feature called [inferred type predicates](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#inferred-type-predicates) to do what libaries like `zod` do, without the additional runtime overhead or abstraction. @@ -55,14 +55,14 @@ to do what libaries like `zod` do, without the additional runtime overhead or ab ## Requirements The only hard requirement is [TypeScript 5.5](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/). -Since the core primitive that `@traversable/schema` is built on top of is +Since the core primitive that `@traversable/schema-core` is built on top of is [inferred type predicates](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#inferred-type-predicates), we do not have plans to backport to previous versions. ## Quick start ```typescript -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' declare let ex_01: unknown @@ -90,7 +90,7 @@ if (schema_01(ex_01)) { ## Features -`@traversable/schema` is modular by schema (like valibot), but takes it a step further by making its feature set opt-in by default. +`@traversable/schema-core` is modular by schema (like valibot), but takes it a step further by making its feature set opt-in by default. The ability to add features like this is a knock-on effect of traversable's extensible core. @@ -104,14 +104,14 @@ which no other schema library currently does (although please file an issue if t This is possible because the traversable schemas are themselves just type predicates with a few additional properties that allow them to also be used for reflection. -- **Instructions:** To use this feature, define a predicate inline and `@traversable/schema` will figure out the rest. +- **Instructions:** To use this feature, define a predicate inline and `@traversable/schema-core` will figure out the rest. #### Example You can play with this example in the TypeScript Playground. ```typescript -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' export let Classes = t.object({ promise: (v) => v instanceof Promise, @@ -195,7 +195,7 @@ type Shorthand = t.typeof Play with this example in the [TypeScript playground](https://tsplay.dev/NaBEPm). ```typescript -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import '@traversable/derive-validators/install' // ↑↑ importing `@traversable/derive-validators/install` adds `.validate` to all schemas @@ -241,7 +241,7 @@ keys are printed at runtime might differ from the order they appear on the type- Play with this example in the [TypeScript playground](https://tsplay.dev/W49jew) ```typescript -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import '@traversable/schema-to-string/install' // ↑↑ importing `@traversable/schema-to-string/install` adds the upgraded `.toString` method on all schemas @@ -284,7 +284,7 @@ Play with this example in the [TypeScript playground](https://tsplay.dev/NB98Vw) ```typescript import * as vi from 'vitest' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import '@traversable/schema-to-json-schema/install' // ↑↑ importing `@traversable/schema-to-json-schema/install` adds `.toJsonSchema` on all schemas @@ -348,22 +348,21 @@ vi.assertType<{ ### Codec (`.pipe`, `.extend`, `.parse`, `.decode` & `.encode`) -- **Instructions:** to install the `.codec` method on all schemas, all you need to do is import `@traversable/derive-codec`. - - To create a covariant codec (similar to zod's `.transform`), use `.codec.pipe` - - To create a contravariant codec (similar to zod's `.preprocess`), use `.codec.extend` (WIP) +- **Instructions:** to install the `.pipe` and `.extend` methods on all schemas, simply `@traversable/derive-codec/install`. + - To create a covariant codec (similar to zod's `.transform`), use `.pipe` + - To create a contravariant codec (similar to zod's `.preprocess`), use `.extend` (WIP) #### Example Play with this example in the [TypeScript playground](https://tsplay.dev/mbbv3m). ```typescript -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import '@traversable/derive-codec/install' -// ↑↑ importing `@traversable/derive-codec/install` adds `.codec` on all schemas +// ↑↑ importing `@traversable/derive-codec/install` adds `.pipe` and `.extend` on all schemas let User = t .object({ name: t.optional(t.string), createdAt: t.string }) - .codec // <-- notice we're pulling off the `.codec` property .pipe((user) => ({ ...user, createdAt: new Date(user.createdAt) })) .unpipe((user) => ({ ...user, createdAt: user.createdAt.toISOString() })) @@ -384,24 +383,42 @@ let toAPI = User.encode(fromAPI) flowchart TD registry(registry) json(json) -.-> registry(registry) - schema(schema) -.-> registry(registry) + schema-core(schema-core) -.-> registry(registry) derive-codec(derive-codec) -.-> registry(registry) - derive-codec(derive-codec) -.-> schema(schema) + derive-codec(derive-codec) -.-> schema-core(schema-core) derive-equals(derive-equals) -.-> json(json) derive-equals(derive-equals) -.-> registry(registry) - derive-equals(derive-equals) -.-> schema(schema) + derive-equals(derive-equals) -.-> schema-core(schema-core) derive-validators(derive-validators) -.-> json(json) derive-validators(derive-validators) -.-> registry(registry) - derive-validators(derive-validators) -.-> schema(schema) + derive-validators(derive-validators) -.-> schema-core(schema-core) + schema-arbitrary(schema-arbitrary) -.-> registry(registry) + schema-arbitrary(schema-arbitrary) -.-> schema-core(schema-core) + schema-jit-compiler(schema-jit-compiler) -.-> registry(registry) + schema-jit-compiler(schema-jit-compiler) -.-> schema-core(schema-core) schema-seed(schema-seed) -.-> json(json) schema-seed(schema-seed) -.-> registry(registry) - schema-seed(schema-seed) -.-> schema(schema) + schema-seed(schema-seed) -.-> schema-core(schema-core) schema-to-json-schema(schema-to-json-schema) -.-> registry(registry) - schema-to-json-schema(schema-to-json-schema) -.-> schema(schema) + schema-to-json-schema(schema-to-json-schema) -.-> schema-core(schema-core) schema-to-string(schema-to-string) -.-> registry(registry) - schema-to-string(schema-to-string) -.-> schema(schema) + schema-to-string(schema-to-string) -.-> schema-core(schema-core) schema-valibot-adapter(schema-valibot-adapter) -.-> json(json) schema-valibot-adapter(schema-valibot-adapter) -.-> registry(registry) schema-zod-adapter(schema-zod-adapter) -.-> json(json) - schema-zod-adapter(schema-zod-adapter) -.depends on.-> registry(registry) + schema-zod-adapter(schema-zod-adapter) -.-> registry(registry) + schema-generator(schema-generator) -.-> derive-validators(derive-validators) + schema-generator(schema-generator) -.-> derive-equals(derive-equals) + schema-generator(schema-generator) -.-> registry(registry) + schema-generator(schema-generator) -.-> schema-core(schema-core) + schema-generator(schema-generator) -.-> schema-to-json-schema(schema-to-json-schema) + schema-generator(schema-generator) -.-> schema-to-string(schema-to-string) + schema(schema) -.-> derive-codec(derive-codec) + schema(schema) -.-> derive-equals(derive-equals) + schema(schema) -.-> derive-validators(derive-validators) + schema(schema) -.-> registry(registry) + schema(schema) -.-> schema-core(schema-core) + schema(schema) -.-> schema-generator(schema-generator) + schema(schema) -.-> schema-to-json-schema(schema-to-json-schema) + schema(schema) -.depends on.-> schema-to-string(schema-to-string) ``` diff --git a/bin/assets/_README.md b/bin/assets/_README.md new file mode 100644 index 00000000..d620d429 --- /dev/null +++ b/bin/assets/_README.md @@ -0,0 +1,38 @@ +
+

ᯓ𝘁𝗿𝗮𝘃𝗲𝗿𝘀𝗮𝗯𝗹𝗲/<%= pkgHeader =>

+
+ +

+ TODO: write me +

+ +
+ NPM Version +   + TypeScript +   + Static Badge +   + npm +   +
+ +
+ npm bundle size (scoped) +   + Static Badge +   + Static Badge +   +
+ +
+ Demo (StackBlitz) +   •   + TypeScript Playground +   •   + npm +
+
+
+
diff --git a/bin/assets/index.ts b/bin/assets/index.ts index 68177c53..53243968 100644 --- a/bin/assets/index.ts +++ b/bin/assets/index.ts @@ -1 +1,10 @@ +import * as fs from 'node:fs' +import * as path from 'node:path' + +const PATH = { + README: path.join(path.resolve(), 'bin', 'assets', '_README.md') +} as const + export { default as template } from "./_package.json" + +export let getReadmeTemplate = () => fs.readFileSync(PATH.README).toString('utf8') diff --git a/bin/bench.ts b/bin/bench.ts new file mode 100755 index 00000000..76a3c5fe --- /dev/null +++ b/bin/bench.ts @@ -0,0 +1,326 @@ +#!/usr/bin/env pnpm dlx tsx +import * as fs from 'node:fs' +import * as path from 'node:path' +import { execSync } from 'node:child_process' + +import { PACKAGES as packagePaths } from 'bin/constants.js' + +let CR = '\r' +let INIT_CWD = process.env.INIT_CWD ?? path.resolve() +let PACKAGES = packagePaths.map((path) => path.startsWith('packages/') ? path.slice('packages/'.length) : path) + +let WS = { + NEWLINE: '\r\n', + 2: ' '.repeat(2), + 4: ' '.repeat(4), +} + +let PATHSPEC = { + BENCH_SOURCE_DIR: ['test', 'types'], + BENCH_TARGET_DIR: 'bench', + TSCONFIG: 'tsconfig.json', + TSCONFIG_BENCH: 'tsconfig.bench.json', + TSCONFIG_TMP: 'tsconfig.tmp.json', + TYPELEVEL_BENCHMARK_SUFFIX: '.bench.types.ts', +} as const + +let PATTERN = { + RESULTS_START: 'export declare let RESULTS: [', + RESULTS_END: ']', +} + +let esc = (xs: string) => { + let char: string | undefined = undefined + let chars = [...xs] + let out = '' + while ((char = chars.shift()) !== undefined) { + if (char === '[' || char === ']') out += `\\${char}` + else out += char + } + return char +} + +let REG_EXP = { + LIBRARY_NAME: /bench\(["'`](.+):.+"/g, + INSTANTIATION_COUNT: /\.types\s*\(\[(.+),\s*["'`]instantiations["'`]\]\)/g, + RESULTS: new RegExp(esc(PATTERN.RESULTS_START) + '([^]*?)' + esc(PATTERN.RESULTS_END), 'g'), +} + +let [, , arg /* , ...worspaces */] = process.argv +let exec = (cmd: string) => execSync(cmd, { stdio: 'inherit' }) + +let Cmd = { + Terms: () => exec('BENCH=true pnpm vitest bench --outputJson benchmarks/benchmark--$(date -Iseconds).json'), + Types: (pkg: string, path: string) => exec(`cd packages/${pkg} && pnpm dlx tsx ${path} --tsVersions '*'`), +} + +let Script = { + run: runBenchmarks, + prepareTypes: () => prepareTypelevelBenchmarks(PACKAGES), + runTypes: () => runTypelevelBenchmarks(PACKAGES), + cleanupTypes: () => cleanupTypelevelBenchmarks(PACKAGES), +} + +let LOG = { + onPrepare: (pkgName: string, PATH: Paths) => { + console.group(`${WS.NEWLINE}Preparing benchmark run for workspace: ${pkgName}${WS.NEWLINE}`) + console.info(`${CR}${WS[4]}Temporarily moving... ${WS.NEWLINE}${WS[4]} -> ${PATH.tsconfig}${WS.NEWLINE}${WS[4]} -> ${PATH.tsconfigTmp}${WS.NEWLINE}`) + console.info(`${CR}${WS[4]}Temporarily moving... ${WS.NEWLINE}${WS[4]} -> ${PATH.tsconfigBench}${WS.NEWLINE}${WS[4]} -> ${PATH.tsconfig}${WS.NEWLINE}`) + console.info(`${CR}${WS[4]}Temporarily moving... ${WS.NEWLINE}${WS[4]} -> ${PATH.benchSourceDir}${WS.NEWLINE}${WS[4]} -> ${PATH.benchTargetDir}${WS.NEWLINE}`) + console.groupEnd() + }, + onRun: (filePath: string) => { + console.info(`Running typelevel benchmark: ` + filePath) + }, + onCleanup: (pkgName: string, PATH: Paths) => { + console.group(`${WS.NEWLINE}Cleaning up benchmark run for workspace: ${pkgName}${WS.NEWLINE}`) + console.info(`${CR}${WS[4]}Putting back... ${WS.NEWLINE}${WS[4]} -> ${PATH.tsconfig}${WS.NEWLINE}${WS[4]} -> ${PATH.tsconfigBench}${WS.NEWLINE}`) + console.info(`${CR}${WS[4]}Putting back... ${WS.NEWLINE}${WS[4]} -> ${PATH.tsconfigTmp}${WS.NEWLINE}${WS[4]} -> ${PATH.tsconfig}${WS.NEWLINE}`) + console.info(`${CR}${WS[4]}Putting back... ${WS.NEWLINE}${WS[4]} -> ${PATH.benchTargetDir}${WS.NEWLINE}${WS[4]} -> ${PATH.benchSourceDir}${WS.NEWLINE}`) + console.groupEnd() + }, +} + +let isKeyOf = (x: T, k: keyof any): k is keyof T => !!x && typeof x === 'object' && k in x +let has + : (k: K, guard?: (u: unknown) => u is V) => (x: unknown) => x is Record + = (k, guard) => (x): x is never => !!x && typeof x === 'object' + && globalThis.Object.prototype.hasOwnProperty.call(x, k) + && (guard ? guard(x[k as never]) : true) + +interface Paths { + benchSourceDir: string + benchTargetDir: string + tsconfig: string + tsconfigBench: string + tsconfigTmp: string +} + +let makePaths + : (pkgName: string) => Paths + = (pkgName) => { + let WS = path.join(path.resolve(), 'packages', pkgName) + return { + benchSourceDir: path.join(WS, ...PATHSPEC.BENCH_SOURCE_DIR), + benchTargetDir: path.join(WS, PATHSPEC.BENCH_TARGET_DIR), + tsconfig: path.join(WS, PATHSPEC.TSCONFIG), + tsconfigBench: path.join(WS, PATHSPEC.TSCONFIG_BENCH), + tsconfigTmp: path.join(WS, PATHSPEC.TSCONFIG_TMP), + } + } + +function runBenchmarks() { + try { Cmd.Terms() } + catch (_) { process.exit(0) } +} + +interface TsConfig { + references: { path: string }[] +} + +let hasPath = has('path', (u) => typeof u === 'string') +let isArrayOf + : (guard: (u: unknown) => u is T) => (xs: unknown) => xs is T[] + = (guard) => (xs): xs is never => Array.isArray(xs) && xs.every(guard) + +let References = isArrayOf(hasPath) + +let isTsConfig + : (u: unknown) => u is TsConfig + = has('references', References) + +function appendTsConfigBenchPathToTsConfig(filepath: string): TsConfig { + let tsconfig: TsConfig | undefined = void 0 + try { + let _ = JSON.parse(fs.readFileSync(filepath).toString('utf8')) + if (!isTsConfig(_)) + throw Error(`Expected '${PATHSPEC.TSCONFIG}' to match type \'TsConfig\' type.`) + else { + void (tsconfig = _) + } + } + catch (e) { + throw Error(`Could not parse '${PATHSPEC.TSCONFIG}' . Check to make sure the file is valid JSON.`) + } + finally { + if (!tsconfig) throw Error('Illegal state') + else if (!tsconfig.references.find(({ path }) => path === PATHSPEC.TSCONFIG_BENCH)) { + tsconfig.references.push({ path: PATHSPEC.TSCONFIG_BENCH }) + } + } + return tsconfig +} + +function unappendTsConfigBenchPathFromTsConfig(filepath: string): TsConfig { + let tsconfig: TsConfig | undefined = void 0 + try { + let _ = JSON.parse(fs.readFileSync(filepath).toString('utf8')) + if (!isTsConfig(_)) + throw Error(`Expected temporary 'tsconfig.json' file at '${PATHSPEC.TSCONFIG_TMP}' to match type \'TsConfig\' type.`) + else { + void (tsconfig = _) + } + } catch (e) { + throw Error(`Could not parse '${PATHSPEC.TSCONFIG_TMP}' . Check to make sure the file is valid JSON.`) + } finally { + if (!tsconfig) throw Error('Illegal state') + let tsconfigBenchIndex = tsconfig.references.findIndex(({ path }) => path === PATHSPEC.TSCONFIG_BENCH) + if (tsconfigBenchIndex === -1) { + return tsconfig + } else { + void (tsconfig.references.splice(tsconfigBenchIndex, 1)) + return tsconfig + } + } +} + +function prepareTypelevelBenchmarks(packages: string[]): void { + return void packages.forEach((pkgName) => { + let PATH = makePaths(pkgName) + if ( + fs.existsSync(PATH.tsconfig) + && fs.existsSync(PATH.tsconfigBench) + ) { + let tsconfig = appendTsConfigBenchPathToTsConfig(PATH.tsconfig) + void LOG.onPrepare(pkgName, PATH) + void fs.rmSync(PATH.tsconfig) + void fs.writeFileSync(PATH.tsconfigTmp, JSON.stringify(tsconfig, null, 2)) + void fs.renameSync(PATH.tsconfigBench, PATH.tsconfig) + void fs.renameSync(PATH.benchSourceDir, PATH.benchTargetDir) + } + }) +} + +function cleanupTypelevelBenchmarks(packages: string[]) { + packages.forEach((pkgName) => { + let PATH = makePaths(pkgName) + if ( + fs.existsSync(PATH.tsconfig) + && fs.existsSync(PATH.tsconfigTmp) + ) { + let tsconfig = unappendTsConfigBenchPathFromTsConfig(PATH.tsconfigTmp) + void LOG.onCleanup(pkgName, PATH) + void fs.renameSync(PATH.benchTargetDir, PATH.benchSourceDir) + void fs.renameSync(PATH.tsconfig, PATH.tsconfigBench) + void fs.rmSync(PATH.tsconfigTmp) + void fs.writeFileSync(PATH.tsconfig, JSON.stringify(tsconfig, null, 2)) + } + }) +} + +function runTypelevelBenchmarks(packages: string[]) { + return void packages.forEach((pkgName) => { + let PATH = makePaths(pkgName) + if ( + fs.existsSync(PATH.tsconfig) + && fs.existsSync(PATH.tsconfigTmp) + ) { + let packagePath = `${INIT_CWD}/packages/${pkgName}` + let benchTargetPath = PATH.benchTargetDir + let filePaths = fs + .readdirSync(PATH.benchTargetDir, { withFileTypes: true }) + .filter((dirent) => dirent.isFile() && dirent.name.endsWith(PATHSPEC.TYPELEVEL_BENCHMARK_SUFFIX)) + .map(({ parentPath, name }) => path.join(parentPath, name)) + .map((path) => path.startsWith(packagePath) ? '.' + path.slice(packagePath.length) : '.' + path) + + void filePaths.forEach((filePath) => { + void LOG.onRun(filePath) + try { Cmd.Types(pkgName, filePath) } + catch (e) { process.exit(1) } + }) + + void parseBenchFiles(benchTargetPath).forEach(({ content, filePath }) => { + console.log('\r\n\r\nfilePath:\r\n', filePath, '\r\n\r\n') + console.log('\r\n\r\ncontent:\r\n', content, '\r\n\r\n') + return fs.writeFileSync(filePath, content) + }) + } + }) +} + +let zip = (xs: T[], ys: T[]): [T, T][] => { + let out = Array.of<[T, T]>() + let len = Math.min(xs.length, ys.length) + for (let ix = 0; ix < len; ix++) { + let x = xs[ix] + let y = ys[ix] + out.push([x, y]) + } + return out +} + +let resultsComparator = ({ instantiations: l }: BenchResult, { instantiations: r }: BenchResult) => + Number.parseInt(l) < Number.parseInt(r) ? -1 + : Number.parseInt(r) < Number.parseInt(l) ? +1 + : 0 + +interface BenchResult { + libraryName: string + instantiations: string +} + +function createResults(benchResults: BenchResult[]) { + return `${PATTERN.RESULTS_START}${WS.NEWLINE}${benchResults.map(({ libraryName, instantiations }) => + `${WS[2]}{${WS.NEWLINE}${WS[4]}libraryName: "${libraryName}"${WS.NEWLINE}${WS[4]}instantiations: ${instantiations}${WS.NEWLINE}${WS[2]}}` + ).join(`,${WS.NEWLINE}`)}\r\n${PATTERN.RESULTS_END}` +} + +function parseBenchFile(benchFile: string): BenchResult[] { + let libs = benchFile.matchAll(REG_EXP.LIBRARY_NAME) + let results = benchFile.matchAll(REG_EXP.INSTANTIATION_COUNT) + if (libs === null) throw Error('parseBenchFile did not find any matches in libNames') + if (results === null) throw Error('parseBenchFile did not find any matches in instantiations') + let libraryNames = [...libs].map(([, libName]) => libName) + let counts = [...results].map(([, count]) => count) + let zipped = zip(libraryNames, counts) + return zipped + .map(([libraryName, instantiations]) => ({ libraryName, instantiations })) + .sort(resultsComparator) +} + +interface ParsedBenchFile { + filePath: string + content: string +} +function parseBenchFiles(dirpath: string): ParsedBenchFile[] { + let files = fs + .readdirSync(dirpath, { withFileTypes: true }) + let parsedFiles = files + .map(({ name, parentPath }) => path.join(parentPath, name)) + .map((filePath) => { + let originalContent = fs.readFileSync(filePath).toString('utf8') + let parsed = parseBenchFile(originalContent) + let index = originalContent.indexOf('bench.baseline') + let before_ = originalContent.slice(0, index).trim() + let after_ = originalContent.slice(index).trim() + let resultsStart = originalContent.indexOf(PATTERN.RESULTS_START) + let resultsEnd = resultsStart === -1 ? -1 : originalContent.indexOf(PATTERN.RESULTS_END, resultsStart) + let before = resultsStart === -1 ? before_ : originalContent.slice(0, resultsStart).trim() + let after = resultsStart === -1 ? after_ : originalContent.slice(resultsEnd + 1).trim() + return { + filePath, + content: '' + + before + + WS.NEWLINE + + WS.NEWLINE + + createResults(parsed) + + WS.NEWLINE + + WS.NEWLINE + + after + + WS.NEWLINE + } + }) + return parsedFiles +} + +function main(arg: string) { + if (!isKeyOf(Script, arg)) { + throw Error('' + + `[bin/bench.ts]: bench script expected to receive command to run. Available commands:${WS.NEWLINE}` + + Object.keys(Script).join(`${WS.NEWLINE}${WS[4]} - `) + ) + } + else return void Script[arg]() +} + +main(arg) diff --git a/bin/constants.ts b/bin/constants.ts index 4619e403..60cadece 100644 --- a/bin/constants.ts +++ b/bin/constants.ts @@ -3,17 +3,17 @@ import * as path from 'node:path' import * as glob from 'glob' import type { Repo } from './types.js' -export const PKG_LIST = { +export let PKG_LIST = { Start: '<\!-- codegen:start -->', End: '<\!-- codegen:end -->', } as const -export const MARKER = { +export let MARKER = { Start: `\`\`\`mermaid`, End: `\`\`\``, } as const -export const PATTERN = { +export let PATTERN = { ChartReplacement: (chart: string) => `${MARKER.Start}\n${chart}\n${MARKER.End}`, DependencyGraph: `${MARKER.Start}([^]*?)${MARKER.End}`, FlattenOnce: { open: `(.*)../`, close: `(.*)` }, @@ -22,7 +22,7 @@ export const PATTERN = { PackageList: `${PKG_LIST.Start}([^]*?)${PKG_LIST.End}`, } as const -export const REG_EXP = { +export let REG_EXP = { DependencyGraph: new globalThis.RegExp(PATTERN.DependencyGraph, 'g'), FlattenOnce: (dirPath: string) => new RegExp(`${PATTERN.FlattenOnce.open}${dirPath}${PATTERN.FlattenOnce.close}`, 'gm'), @@ -33,15 +33,15 @@ export const REG_EXP = { WordBoundary: /([-_][a-z])/gi, } as const -export const PATH = { +export let PATH = { readme: path.join(path.resolve(), 'README.md'), - schemaReadme: path.join(path.resolve(), 'packages', 'schema', 'README.md'), + schemaReadme: path.join(path.resolve(), 'packages', 'schema-core', 'README.md'), generated: path.join(path.resolve(), 'config', '__generated__'), generated_repo_metadata: path.join(path.resolve(), 'config', '__generated__', 'repo.json'), generated_package_list: path.join(path.resolve(), 'config', '__generated__', 'package-list.ts'), } as const -export const RELATIVE_PATH = { +export let RELATIVE_PATH = { dist: 'dist', build: 'build', src: 'src', @@ -73,13 +73,13 @@ export const RELATIVE_PATH = { ], } as const -export const REPO +export let REPO : Repo = globalThis.JSON.parse(fs.readFileSync(PATH.generated_repo_metadata).toString('utf8')) export const SCOPE: '@traversable' = REPO.scope as never -export const defaults = { +export let defaults = { config: { generateExports: { include: ['*.ts'], @@ -96,25 +96,25 @@ export const defaults = { }, } as const -export const GLOB = { +export let GLOB = { all_packages: 'packages/*/', all_packages_src: 'packages/*/src/**/*.ts', } as const -export const PACKAGES: string[] = glob.sync(GLOB.all_packages) -export const GRAPH = ['.', ...PACKAGES] as const +export let PACKAGES: string[] = glob.sync(GLOB.all_packages) +export let GRAPH = ['.', ...PACKAGES] as const -export const BUILD_ARTIFACTS = [ +export let BUILD_ARTIFACTS = [ '.tsbuildinfo', 'dist', 'build', ] as const -export const BUILD_DEPS = [ +export let BUILD_DEPS = [ 'node_modules', ] as const -export const EMOJI = { +export let EMOJI = { ERR: '٩◔̯◔۶', HEY: 'ʕ•̫͡•ʔ', OOO: '°ﺑ°', @@ -136,3 +136,38 @@ export const EMOJI = { ADMIT_ONE: '🎟', FLAG: '🚩', } as const + +export let ALPHABET_MAP = { + a: '𝗮', + b: '𝗯', + c: '𝗰', + d: '𝗱', + e: '𝗲', + f: '𝗳', + g: '𝗴', + h: '𝗵', + i: '𝗶', + j: '𝗷', + k: '𝗸', + l: '𝗹', + m: '𝗺', + n: '𝗻', + o: '𝗼', + p: '𝗽', + q: '𝗾', + r: '𝗿', + s: '𝘀', + t: '𝘁', + u: '𝘂', + v: '𝘃', + w: '𝘄', + x: '𝘅', + y: '𝘆', + z: '𝘇', +} as const satisfies Record + +export let TEMPLATE = { + Start: '<%= ', + End: ' =>', + new: (varName: T) => `<%= ${varName} =>` as const, +} as const diff --git a/bin/docs.ts b/bin/docs.ts index d93d9c9f..17e54faa 100644 --- a/bin/docs.ts +++ b/bin/docs.ts @@ -53,9 +53,9 @@ let writeChangelogs: (list: string) => SideEffect = flow( * * ``` * [ - * { name: '@traversable/data', dependencies: [], order: 0 }, - * { name: '@traversable/core', dependencies: ['@traversable/data'], order: 1 }, - * { name: '@traversable/node', dependencies: ['@traversable/core', '@traversable/data'], order: 2 } + * { name: '@traversable/registry', dependencies: [], order: 0 }, + * { name: '@traversable/json', dependencies: ['@traversable/registry'], order: 1 }, + * { name: '@traversable/schema-core', dependencies: ['@traversable/registry', '@traversable/json'], order: 2 } * ] * ``` * @@ -63,9 +63,9 @@ let writeChangelogs: (list: string) => SideEffect = flow( * * ``` * flowchart TD - * core(@traversable/core) --> data(@traversable/data) - * node(@traversable/node) --> core(@traversable/core) - * node(@traversable/node) --> data(@traversable/data) + * core(@traversable/schema-core) --> data(@traversable/registry) + * node(@traversable/schema-core) --> core(@traversable/json) + * node(@traversable/json) --> data(@traversable/registry) * ``` * * The `README.md` file contains a block that looks like this: diff --git a/bin/process.ts b/bin/process.ts index 9a0417b2..43abfd26 100644 --- a/bin/process.ts +++ b/bin/process.ts @@ -1,4 +1,4 @@ -import * as cp from "node:child_process" +import * as ChildProcess from "node:child_process" import type { ShellOptions } from "./types.js" /** @@ -6,10 +6,10 @@ import type { ShellOptions } from "./types.js" * * Runs a command synchronously. Output goes to terminal. */ -export const $ +export const $ : (cmd: string, options?: ShellOptions) => void = (cmd: string, { env, ...rest } = {}) => { - cp.execSync(cmd, { + ChildProcess.execSync(cmd, { env: { ...process.env, ...env }, ...rest, stdio: "inherit", @@ -21,12 +21,12 @@ export const $ * * Runs a command synchronously. Output returned as a string. */ -export const shell +export const shell : (cmd: string, options?: ShellOptions) => string - = (cmd, { env, ...rest } = {}) => cp.execSync( - cmd, { - env: { ...process.env, ...env }, - ...rest, - stdio: "pipe" - } + = (cmd, { env, ...rest } = {}) => ChildProcess.execSync( + cmd, { + env: { ...process.env, ...env }, + ...rest, + stdio: "pipe" + } ).toString("utf8") diff --git a/bin/util.ts b/bin/util.ts index 5d5e70d3..e33e7307 100644 --- a/bin/util.ts +++ b/bin/util.ts @@ -221,7 +221,7 @@ const prefix = `${REPO.scope}/` /** * @example * assert.equal( - * withoutPrefix("@traversable/core"), + * withoutPrefix("@traversable/schema-core"), * "core", * ) */ @@ -230,8 +230,8 @@ const withoutPrefix = (name: string) => name.substring(prefix.length) /** * @example * assert.equal( - * wrap("@traversable/core"), - * "core(@traversable/core)", + * wrap("@traversable/schema-core"), + * "core(@traversable/schema-core)", * ) */ const wrap = (name: string) => withoutPrefix(name).concat(`(${withoutPrefix(name)})`) @@ -239,8 +239,8 @@ const wrap = (name: string) => withoutPrefix(name).concat(`(${withoutPrefix(name /** * @example * assert.equal( - * bracket("@traversable/core"), - * "[@traversable/core](./packages/core)", + * bracket("@traversable/schema-core"), + * "[@traversable/schema-core](./packages/schema-core)", * ) */ const bracket = (name: string, version: string): `[${string}](./packages/${string})` => @@ -249,8 +249,8 @@ const bracket = (name: string, version: string): `[${string}](./packages/${strin /** * @example * assert.equal( - * drawRelation({ name: "@traversable/core" })("@traversable/data"), - * "core(@traversable/core) -.-> data(@traversable/data)", + * drawRelation({ name: "@traversable/schema-core" })("@traversable/data"), + * "core(@traversable/schema-core) -.-> data(@traversable/data)", * ) */ const drawRelation @@ -335,9 +335,6 @@ export const topological return graph } -// export const tap -// : (fn: (t: T) => U) => (t: T) => T -// = (fn) => (t) => (fn(t), t) export function tap(msg?: string): (x: T) => T export function tap(msg?: string | void): (x: T) => T export function tap(msg?: string, toString?: (x: T) => string): (x: T) => T diff --git a/bin/watch.ts b/bin/watch.ts deleted file mode 100755 index e63c2743..00000000 --- a/bin/watch.ts +++ /dev/null @@ -1,1061 +0,0 @@ -#!/usr/bin/env pnpm dlx tsx -import chokidar from "chokidar" -import * as path from "node:path" - -const logging = (_?: unknown) => ({ - ...globalThis.console, -}) - -// const debounce = -function debounce(handler: () => Promise, ms?: number): (_file: string) => void -function debounce(handler: () => Promise, ms: number = 10) { - let timeout: ReturnType | undefined - return () => { - if (timeout) globalThis.clearTimeout(timeout) - timeout = globalThis.setTimeout(handler, ms) - } -} - -const PATH = { - dir: path.join(path.resolve(), "packages", "algebra", "__schemas__"), -} as const - -function watch(root: string) { - const configPath = "path" - const config = { - dir: PATH.dir - } - // resolveConfigPath({ configDirectory: root, }) - const configWatcher = chokidar.watch(configPath) - - let watcher = new chokidar.FSWatcher({}) - - const generatorWatcher = () => { - // const config = getConfig() - - watcher.close() - - console.info(`TSR: Watching schemas in (${config.dir})...`) - watcher = chokidar.watch(config.dir) - - watcher.on('ready', async () => { - const handle = async () => { - try { - await generator(config, root) - } catch (err) { - console.error(err) - console.info() - } - } - - await handle() - - let timeout: ReturnType | undefined - - const deduped = (_file: string) => { - if (timeout) { - clearTimeout(timeout) - } - - timeout = setTimeout(handle, 10) - } - - watcher.on('change', deduped) - watcher.on('add', deduped) - watcher.on('unlink', deduped) - }) - } - - configWatcher.on('ready', generatorWatcher) - configWatcher.on('change', generatorWatcher) -} - -async function generator(config: {}, root: string): Promise { - console.log("calling generator") - return Promise.resolve("42") -} - -watch("root") - -// export async function generator(config: Config, root: string) { -// const logger = logging({ disabled: config.disableLogging }) -// logger.log('') - -// if (!isFirst) { -// logger.log('♻️ Generating routes...') -// isFirst = true -// } else if (skipMessage) { -// skipMessage = false -// } else { -// logger.log('♻️ Regenerating routes...') -// } - -// const taskId = latestTask + 1 -// latestTask = taskId - -// const checkLatest = () => { -// if (latestTask !== taskId) { -// skipMessage = true -// return false -// } - -// return true -// } - -// const start = Date.now() - -// const TYPES_DISABLED = config.disableTypes - -// const prettierOptions: prettier.Options = { -// semi: config.semicolons, -// singleQuote: config.quoteStyle === 'single', -// parser: 'typescript', -// } - -// let getRouteNodesResult: GetRouteNodesResult - -// if (config.virtualRouteConfig) { -// getRouteNodesResult = await virtualGetRouteNodes(config, root) -// } else { -// getRouteNodesResult = await physicalGetRouteNodes(config, root) -// } - -// const { rootRouteNode, routeNodes: beforeRouteNodes } = getRouteNodesResult -// if (rootRouteNode === undefined) { -// let errorMessage = `rootRouteNode must not be undefined. Make sure you've added your root route into the route-tree.` -// if (!config.virtualRouteConfig) { -// errorMessage += `\nMake sure that you add a "${rootPathId}.${config.disableTypes ? 'js' : 'tsx'}" file to your routes directory.\nAdd the file in: "${config.routesDirectory}/${rootPathId}.${config.disableTypes ? 'js' : 'tsx'}"` -// } -// throw new Error(errorMessage) -// } - -// const preRouteNodes = multiSortBy(beforeRouteNodes, [ -// (d) => (d.routePath === '/' ? -1 : 1), -// (d) => d.routePath?.split('/').length, -// (d) => -// d.filePath.match(new RegExp(`[./]${config.indexToken}[.]`)) ? 1 : -1, -// (d) => -// d.filePath.match( -// /[./](component|errorComponent|pendingComponent|loader|lazy)[.]/, -// ) -// ? 1 -// : -1, -// (d) => -// d.filePath.match(new RegExp(`[./]${config.routeToken}[.]`)) ? -1 : 1, -// (d) => (d.routePath?.endsWith('/') ? -1 : 1), -// (d) => d.routePath, -// ]).filter((d) => ![`/${rootPathId}`].includes(d.routePath || '')) - -// const routeTree: Array = [] -// const routePiecesByPath: Record = {} - -// // Loop over the flat list of routeNodes and -// // build up a tree based on the routeNodes' routePath -// const routeNodes: Array = [] - -// // the handleRootNode function is not being collapsed into the handleNode function -// // because it requires only a subset of the logic that the handleNode function requires -// // and it's easier to read and maintain this way -// const handleRootNode = async (node?: RouteNode) => { -// if (!node) { -// // currently this is not being handled, but it could be in the future -// // for example to handle a virtual root route -// return -// } - -// // from here on, we are only handling the root node that's present in the file system -// const routeCode = fs.readFileSync(node.fullPath, 'utf-8') - -// if (!routeCode) { -// const replaced = fillTemplate( -// [ -// 'import * as React from "react"\n', -// '%%tsrImports%%', -// '\n\n', -// '%%tsrExportStart%%{\n component: RootComponent\n }%%tsrExportEnd%%\n\n', -// 'function RootComponent() { return (
Hello "%%tsrPath%%"!
) };\n', -// ].join(''), -// { -// tsrImports: -// "import { Outlet, createRootRoute } from '@tanstack/react-router';", -// tsrPath: rootPathId, -// tsrExportStart: `export const Route = createRootRoute(`, -// tsrExportEnd: ');', -// }, -// ) - -// logger.log(`🟡 Creating ${node.fullPath}`) -// fs.writeFileSync( -// node.fullPath, -// await prettier.format(replaced, prettierOptions), -// ) -// } -// } - -// await handleRootNode(rootRouteNode) - -// const handleNode = async (node: RouteNode) => { -// let parentRoute = hasParentRoute(routeNodes, node, node.routePath) - -// // if the parent route is a virtual parent route, we need to find the real parent route -// if (parentRoute?.isVirtualParentRoute && parentRoute.children?.length) { -// // only if this sub-parent route returns a valid parent route, we use it, if not leave it as it -// const possibleParentRoute = hasParentRoute( -// parentRoute.children, -// node, -// node.routePath, -// ) -// if (possibleParentRoute) { -// parentRoute = possibleParentRoute -// } -// } - -// if (parentRoute) node.parent = parentRoute - -// node.path = determineNodePath(node) - -// const trimmedPath = trimPathLeft(node.path ?? '') - -// const split = trimmedPath.split('/') -// const lastRouteSegment = split[split.length - 1] ?? trimmedPath - -// node.isNonPath = -// lastRouteSegment.startsWith('_') || -// routeGroupPatternRegex.test(lastRouteSegment) - -// node.cleanedPath = removeGroups( -// removeUnderscores(removeLayoutSegments(node.path)) ?? '', -// ) - -// // Ensure the boilerplate for the route exists, which can be skipped for virtual parent routes and virtual routes -// if (!node.isVirtualParentRoute && !node.isVirtual) { -// const routeCode = fs.readFileSync(node.fullPath, 'utf-8') - -// const escapedRoutePath = node.routePath?.replaceAll('$', '$$') ?? '' - -// let replaced = routeCode - -// if (!routeCode) { -// if (node.isLazy) { -// replaced = fillTemplate(config.customScaffolding.routeTemplate, { -// tsrImports: -// "import { createLazyFileRoute } from '@tanstack/react-router';", -// tsrPath: escapedRoutePath, -// tsrExportStart: `export const Route = createLazyFileRoute('${escapedRoutePath}')(`, -// tsrExportEnd: ');', -// }) -// } else if ( -// node.isRoute || -// (!node.isComponent && -// !node.isErrorComponent && -// !node.isPendingComponent && -// !node.isLoader) -// ) { -// replaced = fillTemplate(config.customScaffolding.routeTemplate, { -// tsrImports: -// "import { createFileRoute } from '@tanstack/react-router';", -// tsrPath: escapedRoutePath, -// tsrExportStart: `export const Route = createFileRoute('${escapedRoutePath}')(`, -// tsrExportEnd: ');', -// }) -// } -// } else { -// replaced = routeCode -// .replace( -// /(FileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g, -// (_, p1, __, p3) => `${p1}${escapedRoutePath}${p3}`, -// ) -// .replace( -// /(import\s*\{.*)(create(Lazy)?FileRoute)(.*\}\s*from\s*['"]@tanstack\/react-router['"])/gs, -// (_, p1, __, ___, p4) => -// `${p1}${node.isLazy ? 'createLazyFileRoute' : 'createFileRoute'}${p4}`, -// ) -// .replace( -// /create(Lazy)?FileRoute(\(\s*['"])([^\s]*)(['"],?\s*\))/g, -// (_, __, p2, ___, p4) => -// `${node.isLazy ? 'createLazyFileRoute' : 'createFileRoute'}${p2}${escapedRoutePath}${p4}`, -// ) -// } - -// await writeIfDifferent( -// node.fullPath, -// prettierOptions, -// routeCode, -// replaced, -// { -// beforeWrite: () => { -// logger.log(`🟡 Updating ${node.fullPath}`) -// }, -// }, -// ) -// } - -// if ( -// !node.isVirtual && -// (node.isLoader || -// node.isComponent || -// node.isErrorComponent || -// node.isPendingComponent || -// node.isLazy) -// ) { -// routePiecesByPath[node.routePath!] = -// routePiecesByPath[node.routePath!] || {} - -// routePiecesByPath[node.routePath!]![ -// node.isLazy -// ? 'lazy' -// : node.isLoader -// ? 'loader' -// : node.isErrorComponent -// ? 'errorComponent' -// : node.isPendingComponent -// ? 'pendingComponent' -// : 'component' -// ] = node - -// const anchorRoute = routeNodes.find((d) => d.routePath === node.routePath) - -// if (!anchorRoute) { -// await handleNode({ -// ...node, -// isVirtual: true, -// isLazy: false, -// isLoader: false, -// isComponent: false, -// isErrorComponent: false, -// isPendingComponent: false, -// }) -// } -// return -// } - -// const cleanedPathIsEmpty = (node.cleanedPath || '').length === 0 -// const nonPathRoute = node.isRoute && node.isNonPath -// node.isVirtualParentRequired = -// node.isLayout || nonPathRoute ? !cleanedPathIsEmpty : false -// if (!node.isVirtual && node.isVirtualParentRequired) { -// const parentRoutePath = removeLastSegmentFromPath(node.routePath) || '/' -// const parentVariableName = routePathToVariable(parentRoutePath) - -// const anchorRoute = routeNodes.find( -// (d) => d.routePath === parentRoutePath, -// ) - -// if (!anchorRoute) { -// const parentNode = { -// ...node, -// path: removeLastSegmentFromPath(node.path) || '/', -// filePath: removeLastSegmentFromPath(node.filePath) || '/', -// fullPath: removeLastSegmentFromPath(node.fullPath) || '/', -// routePath: parentRoutePath, -// variableName: parentVariableName, -// isVirtual: true, -// isLayout: false, -// isVirtualParentRoute: true, -// isVirtualParentRequired: false, -// } - -// parentNode.children = parentNode.children ?? [] -// parentNode.children.push(node) - -// node.parent = parentNode - -// if (node.isLayout) { -// // since `node.path` is used as the `id` on the route definition, we need to update it -// node.path = determineNodePath(node) -// } - -// await handleNode(parentNode) -// } else { -// anchorRoute.children = anchorRoute.children ?? [] -// anchorRoute.children.push(node) - -// node.parent = anchorRoute -// } -// } - -// if (node.parent) { -// if (!node.isVirtualParentRequired) { -// node.parent.children = node.parent.children ?? [] -// node.parent.children.push(node) -// } -// } else { -// routeTree.push(node) -// } - -// routeNodes.push(node) -// } - -// for (const node of preRouteNodes.filter((d) => !d.isAPIRoute)) { -// await handleNode(node) -// } -// checkRouteFullPathUniqueness( -// preRouteNodes.filter( -// (d) => !d.isAPIRoute && d.children === undefined && d.isLazy !== true, -// ), -// config, -// ) - -// const startAPIRouteNodes: Array = checkStartAPIRoutes( -// preRouteNodes.filter((d) => d.isAPIRoute), -// config, -// ) - -// const handleAPINode = async (node: RouteNode) => { -// const routeCode = fs.readFileSync(node.fullPath, 'utf-8') - -// const escapedRoutePath = node.routePath?.replaceAll('$', '$$') ?? '' - -// if (!routeCode) { -// const replaced = fillTemplate(config.customScaffolding.apiTemplate, { -// tsrImports: "import { createAPIFileRoute } from '@tanstack/start/api';", -// tsrPath: escapedRoutePath, -// tsrExportStart: `export const ${CONSTANTS.APIRouteExportVariable} = createAPIFileRoute('${escapedRoutePath}')(`, -// tsrExportEnd: ');', -// }) - -// logger.log(`🟡 Creating ${node.fullPath}`) -// fs.writeFileSync( -// node.fullPath, -// await prettier.format(replaced, prettierOptions), -// ) -// } else { -// await writeIfDifferent( -// node.fullPath, -// prettierOptions, -// routeCode, -// routeCode.replace( -// /(createAPIFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g, -// (_, p1, __, p3) => `${p1}${escapedRoutePath}${p3}`, -// ), -// { -// beforeWrite: () => { -// logger.log(`🟡 Updating ${node.fullPath}`) -// }, -// }, -// ) -// } -// } - -// for (const node of startAPIRouteNodes) { -// await handleAPINode(node) -// } - -// function buildRouteTreeConfig(nodes: Array, depth = 1): string { -// const children = nodes.map((node) => { -// if (node.isRoot) { -// return -// } - -// if (node.isLayout && !node.children?.length) { -// return -// } - -// const route = `${node.variableName}Route` - -// if (node.children?.length) { -// const childConfigs = buildRouteTreeConfig(node.children, depth + 1) - -// const childrenDeclaration = TYPES_DISABLED -// ? '' -// : `interface ${route}Children { -// ${node.children.map((child) => `${child.variableName}Route: typeof ${getResolvedRouteNodeVariableName(child)}`).join(',')} -// }` - -// const children = `const ${route}Children${TYPES_DISABLED ? '' : `: ${route}Children`} = { -// ${node.children.map((child) => `${child.variableName}Route: ${getResolvedRouteNodeVariableName(child)}`).join(',')} -// }` - -// const routeWithChildren = `const ${route}WithChildren = ${route}._addFileChildren(${route}Children)` - -// return [ -// childConfigs, -// childrenDeclaration, -// children, -// routeWithChildren, -// ].join('\n\n') -// } - -// return undefined -// }) - -// return children.filter(Boolean).join('\n\n') -// } - -// const routeConfigChildrenText = buildRouteTreeConfig(routeTree) - -// const sortedRouteNodes = multiSortBy(routeNodes, [ -// (d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1), -// (d) => d.routePath?.split('/').length, -// (d) => (d.routePath?.endsWith(config.indexToken) ? -1 : 1), -// (d) => d, -// ]) - -// const imports = Object.entries({ -// createFileRoute: sortedRouteNodes.some((d) => d.isVirtual), -// lazyFn: sortedRouteNodes.some( -// (node) => routePiecesByPath[node.routePath!]?.loader, -// ), -// lazyRouteComponent: sortedRouteNodes.some( -// (node) => -// routePiecesByPath[node.routePath!]?.component || -// routePiecesByPath[node.routePath!]?.errorComponent || -// routePiecesByPath[node.routePath!]?.pendingComponent, -// ), -// }) -// .filter((d) => d[1]) -// .map((d) => d[0]) - -// const virtualRouteNodes = sortedRouteNodes.filter((d) => d.isVirtual) - -// function getImportPath(node: RouteNode) { -// return replaceBackslash( -// removeExt( -// path.relative( -// path.dirname(config.generatedRouteTree), -// path.resolve(config.routesDirectory, node.filePath), -// ), -// config.addExtensions, -// ), -// ) -// } -// const routeImports = [ -// ...config.routeTreeFileHeader, -// `// This file was automatically generated by TanStack Router. -// // You should NOT make any changes in this file as it will be overwritten. -// // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.`, -// imports.length -// ? `import { ${imports.join(', ')} } from '@tanstack/react-router'\n` -// : '', -// '// Import Routes', -// [ -// `import { Route as rootRoute } from './${getImportPath(rootRouteNode)}'`, -// ...sortedRouteNodes -// .filter((d) => !d.isVirtual) -// .map((node) => { -// return `import { Route as ${ -// node.variableName -// }Import } from './${getImportPath(node)}'` -// }), -// ].join('\n'), -// virtualRouteNodes.length ? '// Create Virtual Routes' : '', -// virtualRouteNodes -// .map((node) => { -// return `const ${ -// node.variableName -// }Import = createFileRoute('${node.routePath}')()` -// }) -// .join('\n'), -// '// Create/Update Routes', -// sortedRouteNodes -// .map((node) => { -// const loaderNode = routePiecesByPath[node.routePath!]?.loader -// const componentNode = routePiecesByPath[node.routePath!]?.component -// const errorComponentNode = -// routePiecesByPath[node.routePath!]?.errorComponent -// const pendingComponentNode = -// routePiecesByPath[node.routePath!]?.pendingComponent -// const lazyComponentNode = routePiecesByPath[node.routePath!]?.lazy - -// return [ -// `const ${node.variableName}Route = ${node.variableName}Import.update({ -// ${[ -// `id: '${node.path}'`, -// !node.isNonPath ? `path: '${node.cleanedPath}'` : undefined, -// `getParentRoute: () => ${node.parent?.variableName ?? 'root'}Route`, -// ] -// .filter(Boolean) -// .join(',')} -// }${TYPES_DISABLED ? '' : 'as any'})`, -// loaderNode -// ? `.updateLoader({ loader: lazyFn(() => import('./${replaceBackslash( -// removeExt( -// path.relative( -// path.dirname(config.generatedRouteTree), -// path.resolve(config.routesDirectory, loaderNode.filePath), -// ), -// config.addExtensions, -// ), -// )}'), 'loader') })` -// : '', -// componentNode || errorComponentNode || pendingComponentNode -// ? `.update({ -// ${( -// [ -// ['component', componentNode], -// ['errorComponent', errorComponentNode], -// ['pendingComponent', pendingComponentNode], -// ] as const -// ) -// .filter((d) => d[1]) -// .map((d) => { -// return `${ -// d[0] -// }: lazyRouteComponent(() => import('./${replaceBackslash( -// removeExt( -// path.relative( -// path.dirname(config.generatedRouteTree), -// path.resolve(config.routesDirectory, d[1]!.filePath), -// ), -// config.addExtensions, -// ), -// )}'), '${d[0]}')` -// }) -// .join('\n,')} -// })` -// : '', -// lazyComponentNode -// ? `.lazy(() => import('./${replaceBackslash( -// removeExt( -// path.relative( -// path.dirname(config.generatedRouteTree), -// path.resolve( -// config.routesDirectory, -// lazyComponentNode.filePath, -// ), -// ), -// config.addExtensions, -// ), -// )}').then((d) => d.Route))` -// : '', -// ].join('') -// }) -// .join('\n\n'), -// ...(TYPES_DISABLED -// ? [] -// : [ -// '// Populate the FileRoutesByPath interface', -// `declare module '@tanstack/react-router' { -// interface FileRoutesByPath { -// ${routeNodes -// .map((routeNode) => { -// const filePathId = routeNode.routePath - -// return `'${filePathId}': { -// id: '${filePathId}' -// path: '${inferPath(routeNode)}' -// fullPath: '${inferFullPath(routeNode)}' -// preLoaderRoute: typeof ${routeNode.variableName}Import -// parentRoute: typeof ${ -// routeNode.isVirtualParentRequired -// ? `${routeNode.parent?.variableName}Route` -// : routeNode.parent?.variableName -// ? `${routeNode.parent.variableName}Import` -// : 'rootRoute' -// } -// }` -// }) -// .join('\n')} -// } -// }`, -// ]), -// '// Create and export the route tree', -// routeConfigChildrenText, -// ...(TYPES_DISABLED -// ? [] -// : [ -// `export interface FileRoutesByFullPath { -// ${[...createRouteNodesByFullPath(routeNodes).entries()].map( -// ([fullPath, routeNode]) => { -// return `'${fullPath}': typeof ${getResolvedRouteNodeVariableName(routeNode)}` -// }, -// )} -// }`, -// `export interface FileRoutesByTo { -// ${[...createRouteNodesByTo(routeNodes).entries()].map(([to, routeNode]) => { -// return `'${to}': typeof ${getResolvedRouteNodeVariableName(routeNode)}` -// })} -// }`, -// `export interface FileRoutesById { -// '__root__': typeof rootRoute, -// ${[...createRouteNodesById(routeNodes).entries()].map(([id, routeNode]) => { -// return `'${id}': typeof ${getResolvedRouteNodeVariableName(routeNode)}` -// })} -// }`, -// `export interface FileRouteTypes { -// fileRoutesByFullPath: FileRoutesByFullPath -// fullPaths: ${routeNodes.length > 0 ? [...createRouteNodesByFullPath(routeNodes).keys()].map((fullPath) => `'${fullPath}'`).join('|') : 'never'} -// fileRoutesByTo: FileRoutesByTo -// to: ${routeNodes.length > 0 ? [...createRouteNodesByTo(routeNodes).keys()].map((to) => `'${to}'`).join('|') : 'never'} -// id: ${[`'__root__'`, ...[...createRouteNodesById(routeNodes).keys()].map((id) => `'${id}'`)].join('|')} -// fileRoutesById: FileRoutesById -// }`, -// `export interface RootRouteChildren { -// ${routeTree.map((child) => `${child.variableName}Route: typeof ${getResolvedRouteNodeVariableName(child)}`).join(',')} -// }`, -// ]), -// `const rootRouteChildren${TYPES_DISABLED ? '' : ': RootRouteChildren'} = { -// ${routeTree.map((child) => `${child.variableName}Route: ${getResolvedRouteNodeVariableName(child)}`).join(',')} -// }`, -// `export const routeTree = rootRoute._addFileChildren(rootRouteChildren)${TYPES_DISABLED ? '' : '._addFileTypes()'}`, -// ...config.routeTreeFileFooter, -// ] -// .filter(Boolean) -// .join('\n\n') - -// const createRouteManifest = () => { -// const routesManifest = { -// __root__: { -// filePath: rootRouteNode.filePath, -// children: routeTree.map((d) => d.routePath), -// }, -// ...Object.fromEntries( -// routeNodes.map((d) => { -// const filePathId = d.routePath - -// return [ -// filePathId, -// { -// filePath: d.filePath, -// parent: d.parent?.routePath ? d.parent.routePath : undefined, -// children: d.children?.map((childRoute) => childRoute.routePath), -// }, -// ] -// }), -// ), -// } - -// return JSON.stringify( -// { -// routes: routesManifest, -// }, -// null, -// 2, -// ) -// } - -// const routeConfigFileContent = config.disableManifestGeneration -// ? routeImports -// : [ -// routeImports, -// '\n', -// '/* ROUTE_MANIFEST_START', -// createRouteManifest(), -// 'ROUTE_MANIFEST_END */', -// ].join('\n') - -// if (!checkLatest()) return - -// const existingRouteTreeContent = await fsp -// .readFile(path.resolve(config.generatedRouteTree), 'utf-8') -// .catch((err) => { -// if (err.code === 'ENOENT') { -// return '' -// } - -// throw err -// }) - -// if (!checkLatest()) return - -// // Ensure the directory exists -// await fsp.mkdir(path.dirname(path.resolve(config.generatedRouteTree)), { -// recursive: true, -// }) - -// if (!checkLatest()) return - -// // Write the route tree file, if it has changed -// const routeTreeWriteResult = await writeIfDifferent( -// path.resolve(config.generatedRouteTree), -// prettierOptions, -// existingRouteTreeContent, -// routeConfigFileContent, -// { -// beforeWrite: () => { -// logger.log(`🟡 Updating ${config.generatedRouteTree}`) -// }, -// }, -// ) -// if (routeTreeWriteResult && !checkLatest()) { -// return -// } - -// logger.log( -// `✅ Processed ${routeNodes.length === 1 ? 'route' : 'routes'} in ${ -// Date.now() - start -// }ms`, -// ) -// } - -// function removeGroups(s: string) { -// return s.replace(possiblyNestedRouteGroupPatternRegex, '') -// } - -// /** -// * The `node.path` is used as the `id` in the route definition. -// * This function checks if the given node has a parent and if so, it determines the correct path for the given node. -// * @param node - The node to determine the path for. -// * @returns The correct path for the given node. -// */ -// function determineNodePath(node: RouteNode) { -// return (node.path = node.parent -// ? node.routePath?.replace(node.parent.routePath ?? '', '') || '/' -// : node.routePath) -// } - -// /** -// * Removes the last segment from a given path. Segments are considered to be separated by a '/'. -// * -// * @param {string} routePath - The path from which to remove the last segment. Defaults to '/'. -// * @returns {string} The path with the last segment removed. -// * @example -// * removeLastSegmentFromPath('/workspace/_auth/foo') // '/workspace/_auth' -// */ -// export function removeLastSegmentFromPath(routePath: string = '/'): string { -// const segments = routePath.split('/') -// segments.pop() // Remove the last segment -// return segments.join('/') -// } - -// /** -// * Removes all segments from a given path that start with an underscore ('_'). -// * -// * @param {string} routePath - The path from which to remove segments. Defaults to '/'. -// * @returns {string} The path with all underscore-prefixed segments removed. -// * @example -// * removeLayoutSegments('/workspace/_auth/foo') // '/workspace/foo' -// */ -// function removeLayoutSegments(routePath: string = '/'): string { -// const segments = routePath.split('/') -// const newSegments = segments.filter((segment) => !segment.startsWith('_')) -// return newSegments.join('/') -// } - -// export function hasParentRoute( -// routes: Array, -// node: RouteNode, -// routePathToCheck: string | undefined, -// ): RouteNode | null { -// if (!routePathToCheck || routePathToCheck === '/') { -// return null -// } - -// const sortedNodes = multiSortBy(routes, [ -// (d) => d.routePath!.length * -1, -// (d) => d.variableName, -// ]).filter((d) => d.routePath !== `/${rootPathId}`) - -// for (const route of sortedNodes) { -// if (route.routePath === '/') continue - -// if ( -// routePathToCheck.startsWith(`${route.routePath}/`) && -// route.routePath !== routePathToCheck -// ) { -// return route -// } -// } - -// const segments = routePathToCheck.split('/') -// segments.pop() // Remove the last segment -// const parentRoutePath = segments.join('/') - -// return hasParentRoute(routes, node, parentRoutePath) -// } - -// /** -// * Gets the final variable name for a route -// */ -// export const getResolvedRouteNodeVariableName = ( -// routeNode: RouteNode, -// ): string => { -// return routeNode.children?.length -// ? `${routeNode.variableName}RouteWithChildren` -// : `${routeNode.variableName}Route` -// } - -// /** -// * Creates a map from fullPath to routeNode -// */ -// export const createRouteNodesByFullPath = ( -// routeNodes: Array, -// ): Map => { -// return new Map( -// routeNodes.map((routeNode) => [inferFullPath(routeNode), routeNode]), -// ) -// } - -// /** -// * Create a map from 'to' to a routeNode -// */ -// export const createRouteNodesByTo = ( -// routeNodes: Array, -// ): Map => { -// return new Map( -// dedupeBranchesAndIndexRoutes(routeNodes).map((routeNode) => [ -// inferTo(routeNode), -// routeNode, -// ]), -// ) -// } - -// /** -// * Create a map from 'id' to a routeNode -// */ -// export const createRouteNodesById = ( -// routeNodes: Array, -// ): Map => { -// return new Map( -// routeNodes.map((routeNode) => { -// const id = routeNode.routePath ?? '' -// return [id, routeNode] -// }), -// ) -// } - -// /** -// * Infers the full path for use by TS -// */ -// export const inferFullPath = (routeNode: RouteNode): string => { -// const fullPath = removeGroups( -// removeUnderscores(removeLayoutSegments(routeNode.routePath)) ?? '', -// ) - -// return routeNode.cleanedPath === '/' ? fullPath : fullPath.replace(/\/$/, '') -// } - -// /** -// * Infers the path for use by TS -// */ -// export const inferPath = (routeNode: RouteNode): string => { -// return routeNode.cleanedPath === '/' -// ? routeNode.cleanedPath -// : (routeNode.cleanedPath?.replace(/\/$/, '') ?? '') -// } - -// /** -// * Infers to path -// */ -// export const inferTo = (routeNode: RouteNode): string => { -// const fullPath = inferFullPath(routeNode) - -// if (fullPath === '/') return fullPath - -// return fullPath.replace(/\/$/, '') -// } - -// /** -// * Dedupes branches and index routes -// */ -// export const dedupeBranchesAndIndexRoutes = ( -// routes: Array, -// ): Array => { -// return routes.filter((route) => { -// if (route.children?.find((child) => child.cleanedPath === '/')) return false -// return true -// }) -// } - -// function checkUnique(routes: Array, key: keyof TElement) { -// // Check no two routes have the same `key` -// // if they do, throw an error with the conflicting filePaths -// const keys = routes.map((d) => d[key]) -// const uniqueKeys = new Set(keys) -// if (keys.length !== uniqueKeys.size) { -// const duplicateKeys = keys.filter((d, i) => keys.indexOf(d) !== i) -// const conflictingFiles = routes.filter((d) => -// duplicateKeys.includes(d[key]), -// ) -// return conflictingFiles -// } -// return undefined -// } - -// function checkRouteFullPathUniqueness( -// _routes: Array, -// config: Config, -// ) { -// const routes = _routes.map((d) => { -// const inferredFullPath = inferFullPath(d) -// return { ...d, inferredFullPath } -// }) - -// const conflictingFiles = checkUnique(routes, 'inferredFullPath') - -// if (conflictingFiles !== undefined) { -// const errorMessage = `Conflicting configuration paths were found for the following route${conflictingFiles.length > 1 ? 's' : ''}: ${conflictingFiles -// .map((p) => `"${p.inferredFullPath}"`) -// .join(', ')}. -// Please ensure each route has a unique full path. -// Conflicting files: \n ${conflictingFiles.map((d) => path.resolve(config.routesDirectory, d.filePath)).join('\n ')}\n` -// throw new Error(errorMessage) -// } -// } - -// function checkStartAPIRoutes(_routes: Array, config: Config) { -// if (_routes.length === 0) { -// return [] -// } - -// // Make sure these are valid URLs -// // Route Groups and Layout Routes aren't being removed since -// // you may want to have an API route that starts with an underscore -// // or be wrapped in parentheses -// const routes = _routes.map((d) => { -// const routePath = removeTrailingSlash(d.routePath ?? '') -// return { ...d, routePath } -// }) - -// const conflictingFiles = checkUnique(routes, 'routePath') - -// if (conflictingFiles !== undefined) { -// const errorMessage = `Conflicting configuration paths were found for the following API route${conflictingFiles.length > 1 ? 's' : ''}: ${conflictingFiles -// .map((p) => `"${p}"`) -// .join(', ')}. -// Please ensure each API route has a unique route path. -// Conflicting files: \n ${conflictingFiles.map((d) => path.resolve(config.routesDirectory, d.filePath)).join('\n ')}\n` -// throw new Error(errorMessage) -// } - -// return routes -// } - -// export type StartAPIRoutePathSegment = { -// value: string -// type: 'path' | 'param' | 'splat' -// } - -// /** -// * This function takes in a path in the format accepted by TanStack Router -// * and returns an array of path segments that can be used to generate -// * the pathname of the TanStack Start API route. -// * -// * @param src -// * @returns -// */ -// export function startAPIRouteSegmentsFromTSRFilePath( -// src: string, -// config: Config, -// ): Array { -// const routePath = determineInitialRoutePath(src) - -// const parts = routePath -// .replaceAll('.', '/') -// .split('/') -// .filter((p) => !!p && p !== config.indexToken) -// const segments: Array = parts.map((part) => { -// if (part.startsWith('$')) { -// if (part === '$') { -// return { value: part, type: 'splat' } -// } - -// part.replaceAll('$', '') -// return { value: part, type: 'param' } -// } - -// return { value: part, type: 'path' } -// }) - -// return segments -// } - -// type TemplateTag = 'tsrImports' | 'tsrPath' | 'tsrExportStart' | 'tsrExportEnd' - -// function fillTemplate(template: string, values: Record) { -// return template.replace( -// /%%(\w+)%%/g, -// (_, key) => values[key as TemplateTag] || '', -// ) -// } - diff --git a/bin/workspace-create.ts b/bin/workspace-create.ts index 174ecbd8..e7f53891 100755 --- a/bin/workspace-create.ts +++ b/bin/workspace-create.ts @@ -22,25 +22,24 @@ const env = Prompt.select({ const visibility = Prompt.select({ message: `Initialize the package as private (will not auto-publish)?`, choices: [ - { title: "true", value: true }, { title: "false", value: false }, + { title: "true", value: true }, ] as const }) const localDeps = Prompt.list({ - message: `Which will your workspace depend on?\n\ncomma separated list containing any of: \n\n ${ - [...PACKAGES].sort().map(pkg => pkg.slice("packages/".length)).join(", ") - }\n` , + message: `Which will your workspace depend on?\n\ncomma separated list containing any of: \n\n ${[...PACKAGES].sort().map(pkg => pkg.slice("packages/".length)).join(", ") + }\n`, delimiter: ", " }) const command = Command.prompt( - "New workspace", + "New workspace", Prompt.all([pkgName, env, localDeps, visibility]), - ([ pkgName, env, localDeps, private_ ]) => + ([pkgName, env, localDeps, private_]) => Effect.sync(() => main({ pkgName, env, localDeps, private: private_, dryRun: false }) -)) + )) const cli = Command.run(command, { name: "Generate an empty package", diff --git a/bin/workspace.ts b/bin/workspace.ts index a8299aa4..d668b17c 100755 --- a/bin/workspace.ts +++ b/bin/workspace.ts @@ -3,14 +3,15 @@ import * as path from 'node:path' import { identity, pipe, Effect } from 'effect' import * as fs from './fs.js' -import { template } from './assets/index.js' +import { template, getReadmeTemplate } from './assets/index.js' import { Print, tap, Transform } from './util.js' import * as S from 'effect/Schema' -import { SCOPE } from 'bin/constants.js' +import { ALPHABET_MAP, TEMPLATE as Template, SCOPE } from 'bin/constants.js' const $$ = (command: string) => process.execSync(command, { stdio: 'inherit' }) let ix = 0 + const PATH = { packages: path.join(path.resolve(), 'packages'), vitestSharedConfig: path.join(path.resolve(), 'vite.config.ts'), @@ -19,6 +20,16 @@ const PATH = { rootTsConfigBuild: path.join(path.resolve(), 'tsconfig.build.json'), } as const +const PATTERN = { + PkgName: Template.new('pkgName'), // `${Template.Start}([^]*?)${Template.End}`, + PkgHeader: Template.new('pkgHeader'), +} as const + +const REG_EXP = { + PkgName: new RegExp(PATTERN.PkgName, 'g'), + PkgHeader: new RegExp(PATTERN.PkgHeader, 'g'), +} as const + const TEMPLATE = { RootKey: 'packages/', BuildKey: { pre: 'packages/', post: '/tsconfig.build.json' }, @@ -419,7 +430,6 @@ namespace write { ($) => pipe( [ `export * from './exports.js'`, - `export * as ${Transform.toCamelCase($.pkgName)} from './exports.js'`, ].join('\n'), $.dryRun ? tap(`\n\n[CREATE #10]: workspaceIndex\n`, globalThis.String) : fs.writeString(path.join(PATH.packages, $.pkgName, 'src', 'index.ts')), @@ -459,12 +469,14 @@ namespace write { : fs.rimraf(path.join(PATH.packages, $.pkgName, 'vite.config.ts')), ) - export const workspaceReadme = defineEffect( + let toPackageHeader = (pkgName: string) => [...pkgName].map((char) => char in ALPHABET_MAP ? ALPHABET_MAP[char as keyof typeof ALPHABET_MAP] : char).join('') + + export let workspaceReadme = defineEffect( ($) => pipe( - [ - `# ${SCOPE}/${$.pkgName}` - ].join('\n'), - $.dryRun ? tap(`\n\n[CREATE #13]: workspaceReadme\n`, globalThis.String) + getReadmeTemplate(), + (readme) => readme.replace(REG_EXP.PkgHeader, toPackageHeader($.pkgName)), + (readme) => readme.replace(REG_EXP.PkgName, $.pkgName), + $.dryRun ? tap(`\n\n[CREATE #13]: readmeTemplate\n`, globalThis.String) : fs.writeString(path.join(PATH.packages, $.pkgName, 'README.md')), ), ($) => @@ -473,12 +485,27 @@ namespace write { : fs.rimraf(path.join(PATH.packages, $.pkgName, 'README.md')), ) + // export const workspaceReadme = defineEffect( + // ($) => pipe( + // [ + // `# ${SCOPE}/${$.pkgName}` + // ].join('\n'), + // $.dryRun ? tap(`\n\n[CREATE #14]: workspaceReadme\n`, globalThis.String) + // : fs.writeString(path.join(PATH.packages, $.pkgName, 'README.md')), + // ), + // ($) => + // $.dryRun + // ? tap(`\n\n[CLEANUP #14]: workspaceReadme\n`, globalThis.String) + // : fs.rimraf(path.join(PATH.packages, $.pkgName, 'README.md')), + // ) + export const workspaceSrcVersion = defineEffect( ($) => pipe( [ `import pkg from './__generated__/__manifest__.js'`, `export const VERSION = \`\${pkg.name}@\${pkg.version}\` as const`, `export type VERSION = typeof VERSION`, + ``, ].join('\n'), $.dryRun ? tap(`\n\n[CREATE #14]: workspaceVersionSrc\n`, globalThis.String) : fs.writeString(path.join(PATH.packages, $.pkgName, 'src', 'version.ts')), @@ -494,14 +521,15 @@ namespace write { ([ `import * as vi from 'vitest'`, `import pkg from '../package.json' with { type: 'json' }`, - `import { ${Transform.toCamelCase($.pkgName)} } from '${SCOPE}/${$.pkgName}'`, + `import { VERSION } from '${SCOPE}/${$.pkgName}'`, ``, `vi.describe('〖⛳️〗‹‹‹ ❲${SCOPE}/${$.pkgName}❳', () => {`, - ` vi.it('〖⛳️〗› ❲${Transform.toCamelCase($.pkgName)}#VERSION❳', () => {`, + ` vi.it('〖⛳️〗› ❲VERSION❳', () => {`, ` const expected = \`\${pkg.name}@\${pkg.version}\``, - ` vi.assert.equal(${Transform.toCamelCase($.pkgName)}.VERSION, expected)`, + ` vi.assert.equal(VERSION, expected)`, ` })`, `})`, + ``, ]).join('\n'), $.dryRun ? tap(`\n\n[CREATE #15]: workspaceVersionTest\n`, globalThis.String) : fs.writeString(path.join(PATH.packages, $.pkgName, 'test', 'version.test.ts')), diff --git a/config/__generated__/package-list.ts b/config/__generated__/package-list.ts index b8600883..30420693 100644 --- a/config/__generated__/package-list.ts +++ b/config/__generated__/package-list.ts @@ -5,6 +5,10 @@ export const PACKAGES = [ "packages/json", "packages/registry", "packages/schema", + "packages/schema-arbitrary", + "packages/schema-core", + "packages/schema-generator", + "packages/schema-jit-compiler", "packages/schema-seed", "packages/schema-to-json-schema", "packages/schema-to-string", diff --git a/examples/sandbox/package.json b/examples/sandbox/package.json index b745f7ce..04b67838 100644 --- a/examples/sandbox/package.json +++ b/examples/sandbox/package.json @@ -19,7 +19,7 @@ "@traversable/derive-validators": "workspace:^", "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "@traversable/schema-seed": "workspace:^", "@traversable/schema-to-json-schema": "workspace:^", "@traversable/schema-to-string": "workspace:^", diff --git a/examples/sandbox/src/demo/advanced.tsx b/examples/sandbox/src/demo/advanced.tsx index 229af6f0..40257e46 100644 --- a/examples/sandbox/src/demo/advanced.tsx +++ b/examples/sandbox/src/demo/advanced.tsx @@ -9,7 +9,7 @@ import { spacemacs as theme } from '../lib/theme' /** * DEMO: advanced * - * How to take full advantage of the core primitive that `@traverable/schema` is + * How to take full advantage of the core primitive that `@traversable/schema-core` is * built on: recursion schemes. * * The point of recursion schemes is to "factor out recursion". diff --git a/examples/sandbox/src/demo/codec.ts b/examples/sandbox/src/demo/codec.ts index d41bdcd3..3ff55d9c 100644 --- a/examples/sandbox/src/demo/codec.ts +++ b/examples/sandbox/src/demo/codec.ts @@ -3,16 +3,15 @@ import { t } from '../lib' /** * DEMO: converting your schema into a bi-directional codec * - * Import from `@traversable/derive-codec` to install the '.codec' method to all schemas. + * Import from `@traversable/derive-codec` to install the `.pipe` and `.extend` methods to all schemas. * - * From there, you'll have access to `.pipe`, `.extend`, `.decode` and `.encode`. You can pipe and extend - * as many times as you want -- the transformations will be added to a queue (`.pipe` puts a transformation - * in the "after" queue, `.extend` puts a preprocessor in the "before" queue). + * You can pipe and extend as many times as you want -- the transformations will be added to a queue + * (`.pipe` puts a transformation in the "after" queue, `.extend` puts a preprocessor in the "before" queue). * * If you need to recover the original schema, you can access it via the `.schema` property on the codec. */ let User = t - .object({ name: t.optional(t.string), createdAt: t.string }).codec + .object({ name: t.optional(t.string), createdAt: t.string }) .pipe(({ name = '', ...user }) => ({ ...user, firstName: name.split(' ')[0], lastName: name.split(' ')[1] ?? '', createdAt: new Date(user.createdAt) })) .unpipe(({ firstName, lastName, ...user }) => ({ ...user, name: firstName + ' ' + lastName, createdAt: user.createdAt.toISOString() })) diff --git a/examples/sandbox/src/demo/inferredTypePredicates.ts b/examples/sandbox/src/demo/inferredTypePredicates.ts index a8e58a6e..0c5923d1 100644 --- a/examples/sandbox/src/demo/inferredTypePredicates.ts +++ b/examples/sandbox/src/demo/inferredTypePredicates.ts @@ -23,7 +23,7 @@ export let classes = t.object({ /** * DEMO: inferred type predicates * - * You can write inline type predicates, and `@traversable/schema` keep track of + * You can write inline type predicates, and `@traversable/schema-core` keep track of * that at the type-level, even when the predicate is used inside a larger schema: */ export let values = t.object({ diff --git a/examples/sandbox/src/global.d.ts b/examples/sandbox/src/global.d.ts index bede536d..52081912 100644 --- a/examples/sandbox/src/global.d.ts +++ b/examples/sandbox/src/global.d.ts @@ -1,4 +1,4 @@ -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' declare global { var t: typeof t } diff --git a/examples/sandbox/src/lib/functor.tsx b/examples/sandbox/src/lib/functor.tsx index 39ee8ed5..831bf320 100644 --- a/examples/sandbox/src/lib/functor.tsx +++ b/examples/sandbox/src/lib/functor.tsx @@ -1,8 +1,8 @@ import type * as T from '@traversable/registry' import { fn } from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' -import type { Free, Fixpoint } from './shared' +import type { Free } from './shared' import { MapSymbol, SetSymbol, URI } from './shared' import { set as Set } from './set' @@ -16,18 +16,17 @@ let map: T.Functor['map'] = (f) => (x) => { } } -export const Functor: T.Functor.Ix = { +export const Functor: T.Functor.Ix = { map, mapWithIndex(f) { return (x, ix) => { switch (true) { - default: return fn.exhaustive(x) case x.tag === URI.set: return Set(f(x.def, [...ix, SetSymbol])) case x.tag === URI.map: return Map(f(x.def[0], [...ix, MapSymbol, 0]), f(x.def[1], [...ix, MapSymbol, 1])) - case t.isCore(x): return t.IndexedFunctor.mapWithIndex(f)(x, ix) + default: return t.IndexedFunctor.mapWithIndex(f)(x, ix) } } - }, + } } export const fold = fn.cataIx(Functor) diff --git a/examples/sandbox/src/lib/index.ts b/examples/sandbox/src/lib/index.ts index dafb2bdc..edccf6be 100644 --- a/examples/sandbox/src/lib/index.ts +++ b/examples/sandbox/src/lib/index.ts @@ -1,34 +1,34 @@ export * as t from './namespace' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import '@traversable/derive-codec/install' import '@traversable/derive-equals/install' import '@traversable/derive-validators/install' import '@traversable/schema-to-json-schema/install' import '@traversable/schema-to-string/install' -import { prototype } from './prototype' +import { bindParse } from './prototype' export function bind() { - Object.assign(t.never, prototype) - Object.assign(t.unknown, prototype) - Object.assign(t.void, prototype) - Object.assign(t.null, prototype) - Object.assign(t.undefined, prototype) - Object.assign(t.boolean, prototype) - Object.assign(t.symbol, prototype) - Object.assign(t.integer, prototype) - Object.assign(t.bigint, prototype) - Object.assign(t.number, prototype) - Object.assign(t.string, prototype) - Object.assign(t.eq.prototype, prototype) - Object.assign(t.optional.prototype, prototype) - Object.assign(t.array.prototype, prototype) - Object.assign(t.record.prototype, prototype) - Object.assign(t.union.prototype, prototype) - Object.assign(t.intersect.prototype, prototype) - Object.assign(t.tuple.prototype, prototype) - Object.assign(t.object.prototype, prototype) - Object.assign(t.enum.prototype, prototype) + Object.assign(t.never, bindParse) + Object.assign(t.unknown, bindParse) + Object.assign(t.void, bindParse) + Object.assign(t.null, bindParse) + Object.assign(t.undefined, bindParse) + Object.assign(t.boolean, bindParse) + Object.assign(t.symbol, bindParse) + Object.assign(t.integer, bindParse) + Object.assign(t.bigint, bindParse) + Object.assign(t.number, bindParse) + Object.assign(t.string, bindParse) + Object.assign(t.eq.userDefinitions, bindParse) + Object.assign(t.optional.userDefinitions, bindParse) + Object.assign(t.array.userDefinitions, bindParse) + Object.assign(t.record.userDefinitions, bindParse) + Object.assign(t.union.userDefinitions, bindParse) + Object.assign(t.intersect.userDefinitions, bindParse) + Object.assign(t.tuple.userDefinitions, bindParse) + Object.assign(t.object.userDefinitions, bindParse) + Object.assign(t.enum.userDefinitions, bindParse) } @@ -39,8 +39,8 @@ export interface parse { parse(u: this['_type' & keyof this] | {} | null | undefined): this['_type' & keyof this] } -declare module '@traversable/schema' { - interface t_never extends parse { } +declare module '@traversable/schema-core' { + // interface t_never extends parse { } interface t_unknown extends parse { } interface t_any extends parse { } interface t_void extends parse { } diff --git a/examples/sandbox/src/lib/map.ts b/examples/sandbox/src/lib/map.ts index 2314a61c..573bc9bc 100644 --- a/examples/sandbox/src/lib/map.ts +++ b/examples/sandbox/src/lib/map.ts @@ -1,5 +1,5 @@ import * as T from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import type { ValidationError, Validate, @@ -53,8 +53,8 @@ export namespace map { export type type = never | Map export function def(k: K, v: V): map { type T = Map - let keyPredicate = t.isPredicate(k) ? k : (_?: any) => true - let valuePredicate = t.isPredicate(v) ? v : (_?: any) => true + let keyPredicate = T._isPredicate(k) ? k : (_?: any) => true + let valuePredicate = T._isPredicate(v) ? v : (_?: any) => true function MapSchema(u: map['_type'] | {} | null | undefined): u is T { if (!(u instanceof globalThis.Map)) return false else { diff --git a/examples/sandbox/src/lib/namespace.ts b/examples/sandbox/src/lib/namespace.ts index a627dd4c..8e779d2c 100644 --- a/examples/sandbox/src/lib/namespace.ts +++ b/examples/sandbox/src/lib/namespace.ts @@ -1,5 +1,5 @@ -export * from '@traversable/schema/namespace' -export { getConfig } from '@traversable/schema' +export * from '@traversable/schema-core/namespace' +export { getConfig } from '@traversable/schema-core' export type { F, diff --git a/examples/sandbox/src/lib/prototype.ts b/examples/sandbox/src/lib/prototype.ts index 11b75ada..6f43173d 100644 --- a/examples/sandbox/src/lib/prototype.ts +++ b/examples/sandbox/src/lib/prototype.ts @@ -1,8 +1,8 @@ -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' export function parse(this: S, u: unknown) { if (this(u)) return u else throw Error('invalid input') } -export const prototype = { parse } +export const bindParse = { parse } diff --git a/examples/sandbox/src/lib/react.ts b/examples/sandbox/src/lib/react.ts index 16e344fc..8618b235 100644 --- a/examples/sandbox/src/lib/react.ts +++ b/examples/sandbox/src/lib/react.ts @@ -1,5 +1,5 @@ import type * as React from 'react' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' export interface Key extends t.union<[t.null, t.string]> { } export const Key = t.union(t.null, t.string) satisfies Key @@ -18,7 +18,7 @@ export function ElementSchema

(propsSch props: t.object

key: Key }> -export function ElementSchema(propsSchema: { [x: string]: t.Schema } = {}) { +export function ElementSchema

(propsSchema: P = {} as never) { return t.object({ type: t.any, props: t.object(propsSchema), diff --git a/examples/sandbox/src/lib/set.ts b/examples/sandbox/src/lib/set.ts index 2106dc6e..94aea2c0 100644 --- a/examples/sandbox/src/lib/set.ts +++ b/examples/sandbox/src/lib/set.ts @@ -1,5 +1,5 @@ import * as T from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import type { ValidationError, ValidationFn, @@ -40,7 +40,7 @@ export namespace set { export function def(x: S): set { type T = Set - const predicate = t.isPredicate(x) ? x : (_?: any) => true + const predicate = T._isPredicate(x) ? x : (_?: any) => true function SetSchema(u: unknown): u is T { if (!(u instanceof globalThis.Set)) return false else { diff --git a/examples/sandbox/src/lib/shared.ts b/examples/sandbox/src/lib/shared.ts index 3d658d3a..eb4daf74 100644 --- a/examples/sandbox/src/lib/shared.ts +++ b/examples/sandbox/src/lib/shared.ts @@ -1,7 +1,7 @@ import type { HKT } from '@traversable/registry' import { NS, URI as URI_, symbol as symbol_ } from '@traversable/registry' import { has } from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import type { set } from './set' import type { map } from './map' diff --git a/examples/sandbox/src/lib/toHtml.tsx b/examples/sandbox/src/lib/toHtml.tsx index 1794002d..64546cff 100644 --- a/examples/sandbox/src/lib/toHtml.tsx +++ b/examples/sandbox/src/lib/toHtml.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import type * as T from '@traversable/registry' import { fn, parseKey } from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import { Json } from '@traversable/json' import * as isReact from './react' diff --git a/examples/sandbox/src/sandbox.tsx b/examples/sandbox/src/sandbox.tsx index cc90c307..a9c3f94b 100644 --- a/examples/sandbox/src/sandbox.tsx +++ b/examples/sandbox/src/sandbox.tsx @@ -1,15 +1,15 @@ import { t } from './lib' // to view the inferred type predicates demo, see './demo/inferredTypePredicates.ts' import './demo/inferredTypePredicates' -// to view the '.toString' demo, check your browser console + see './demo/toString.ts' +// to view the '.toString' demo, see './demo/toString.ts' + check your browser console import './demo/toString' -// to view the '.toJsonSchema' demo, check your browser console + see './demo/toJsonSchema.ts' +// to view the '.toJsonSchema' demo, see './demo/toJsonSchema.ts' + check your browser console import './demo/toJsonSchema' -// to view the '.validate' demo, check your browser console + see './demo/validate.ts' +// to view the '.validate' demo, see './demo/validate.ts' + check your browser console import './demo/validate' -// to view the '.pipe' and '.extend' demo, check your browser console + see './demo/codec.ts' +// to view the '.pipe' and '.extend' demo, see './demo/codec.ts' + check your browser console import './demo/codec' -// to see how to extend traversable schemas in userland, check your browser console + see './demo/userlandExtensions.ts' +// to see how to extend traversable schemas in userland, see './demo/userlandExtensions.ts' + check your browser console import './demo/userlandExtensions' // to see how traversable schemas support reflection + custom interpreters, run this example with 'pnpm dev' import { HardcodedSchemaExamples, RandomlyGeneratedSchemas } from './demo/advanced' diff --git a/examples/sandbox/tsconfig.app.json b/examples/sandbox/tsconfig.app.json index d962aa70..0b205611 100644 --- a/examples/sandbox/tsconfig.app.json +++ b/examples/sandbox/tsconfig.app.json @@ -32,8 +32,8 @@ "@traversable/json/*": ["../../packages/json/src/*"], "@traversable/registry": ["../../packages/registry/src"], "@traversable/registry/*": ["../../packages/registry/src/*"], - "@traversable/schema": ["../../packages/schema/src"], - "@traversable/schema/*": ["../../packages/schema/src/*"], + "@traversable/schema-core": ["../../packages/schema-core/src"], + "@traversable/schema-core/*": ["../../packages/schema-core/src/*"], "@traversable/schema-seed": ["../../packages/schema-seed/src"], "@traversable/schema-seed/*": ["../../packages/schema-seed/src/*"], "@traversable/schema-to-json-schema": ["../../packages/schema-to-json-schema/src"], diff --git a/examples/sandbox/vite.config.ts b/examples/sandbox/vite.config.ts index a38e2595..a1147956 100644 --- a/examples/sandbox/vite.config.ts +++ b/examples/sandbox/vite.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ '@traversable/derive-validators': fileURLToPath(new URL('../../packages/derive-validators/src', import.meta.url)), '@traversable/json': fileURLToPath(new URL('../../packages/json/src', import.meta.url)), '@traversable/registry': fileURLToPath(new URL('../../packages/registry/src', import.meta.url)), - '@traversable/schema': fileURLToPath(new URL('../../packages/schema/src', import.meta.url)), + '@traversable/schema-core': fileURLToPath(new URL('../../packages/schema-core/src', import.meta.url)), '@traversable/schema-seed': fileURLToPath(new URL('../../packages/schema-seed/src', import.meta.url)), '@traversable/schema-to-json-schema': fileURLToPath(new URL('../../packages/schema-to-json-schema/src', import.meta.url)), '@traversable/schema-to-string': fileURLToPath(new URL('../../packages/schema-to-string/src', import.meta.url)), diff --git a/package.json b/package.json index fe4b7943..c11df0eb 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,15 @@ "private": true, "license": "MIT", "scripts": { - "bench": "pnpm vitest bench --outputJson benchmarks/benchmark--$(date -Iseconds).json", + "bench": "pnpm bench:types && pnpm bench:runtime", + "bench:runtime": "./bin/bench.ts run", + "bench:types": "./bin/bench.ts prepareTypes && ./bin/bench.ts runTypes && ./bin/bench.ts cleanupTypes", "boot": "pnpm install && pnpm reboot", - "build": "pnpm build:root && pnpm run build:pre && pnpm --recursive --parallel run build && pnpm build:post", + "build": "pnpm build:root && pnpm run build:pre && pnpm --recursive --parallel run build && pnpm build:post && pnpm build:dist && cd packages/schema && pnpm build:schemas", + "build:docs": "pnpm dlx tsx ./bin/docs.ts", "build:pre": "pnpm dlx tsx ./bin/bump.ts", "build:pkgs": "pnpm --filter \"@traversable/*\" run \"/^build:.*/\"", - "build:root": "tsc -b tsconfig.build.json && pnpm run docs", + "build:root": "tsc -b tsconfig.build.json && pnpm run build:docs", "build:post": "pnpm circular", "build:dist": "pnpm dlx tsx ./bin/pack.ts", "changes": "changeset add", @@ -21,43 +24,39 @@ "clean:build": "pnpm -r --stream --parallel run clean:build", "clean:deps": "pnpm -r --stream --parallel run clean:deps && rm -rf node_modules", "clean:generated": "pnpm -r --stream --parallel run clean:generated", + "cli": "./packages/schema-generator/src/cli.ts", "describe": "pnpm run \"/^describe:.*/\"", "describe:project": "./bin/describe.ts", "dev": "pnpm --filter \"@traversable/sandbox\" run \"dev\"", - "docs": "pnpm dlx tsx ./bin/docs.ts", "reboot": "./bin/reboot.ts", - "test": "vitest run -- --skipTypes && tsc -b tsconfig.json", + "test": "vitest run -- --skipTypes --exclude packages/**/test/e2e* && tsc -b tsconfig.json", + "test_": "vitest run -- --skipTypes && tsc -b tsconfig.json", "test:cov": "pnpm vitest run --coverage", - "test:watch": "pnpm vitest --coverage", + "test:e2e": "pnpm vitest run --coverage", + "test:watch": "pnpm vitest run -- --exclude packages/schema-generator/test/e2e.test.ts", "test:watch:no-cov": "pnpm vitest", "workspace:new": "./bin/workspace-create.ts", "workspace:rm": "./bin/workspace-cleanup.ts" }, "devDependencies": { - "@ark/attest": "^0.44.8", - "@babel/cli": "^7.25.9", - "@babel/core": "^7.26.0", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@changesets/changelog-github": "^0.5.0", - "@changesets/cli": "^2.27.9", - "@fast-check/vitest": "^0.2.0", - "@types/madge": "^5.0.3", - "@types/node": "^22.9.0", - "@vitest/coverage-v8": "3.1.1", - "@vitest/ui": "3.1.1", - "babel-plugin-annotate-pure-calls": "^0.4.0", - "fast-check": "^4.0.1", - "madge": "^8.0.0", - "tinybench": "^3.0.4", - "typescript": "5.8.2", - "vitest": "^3.0.4" - }, - "overrides": { - "typescript": "5.8.2" - }, - "resolutions": { - "typescript": "5.8.2" + "@ark/attest": "catalog:", + "@babel/cli": "catalog:", + "@babel/core": "catalog:", + "@babel/plugin-transform-export-namespace-from": "catalog:", + "@babel/plugin-transform-modules-commonjs": "catalog:", + "@changesets/changelog-github": "catalog:", + "@changesets/cli": "catalog:", + "@fast-check/vitest": "catalog:", + "@types/madge": "catalog:", + "@types/node": "catalog:", + "@vitest/coverage-v8": "catalog:", + "@vitest/ui": "catalog:", + "babel-plugin-annotate-pure-calls": "catalog:", + "fast-check": "catalog:", + "madge": "catalog:", + "tinybench": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" }, "packageManager": "pnpm@10.2.1" } diff --git a/packages/derive-codec/package.json b/packages/derive-codec/package.json index f44676d5..dba79328 100644 --- a/packages/derive-codec/package.json +++ b/packages/derive-codec/package.json @@ -48,19 +48,19 @@ }, "peerDependencies": { "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^" + "@traversable/schema-core": "workspace:^" }, "peerDependenciesMeta": { "@traversable/registry": { "optional": false }, - "@traversable/schema": { + "@traversable/schema-core": { "optional": false } }, "devDependencies": { "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "@traversable/schema-seed": "workspace:^" } } diff --git a/packages/derive-codec/src/__generated__/__manifest__.ts b/packages/derive-codec/src/__generated__/__manifest__.ts index c7c375f5..1426835d 100644 --- a/packages/derive-codec/src/__generated__/__manifest__.ts +++ b/packages/derive-codec/src/__generated__/__manifest__.ts @@ -42,19 +42,19 @@ export default { }, "peerDependencies": { "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^" + "@traversable/schema-core": "workspace:^" }, "peerDependenciesMeta": { "@traversable/registry": { "optional": false }, - "@traversable/schema": { + "@traversable/schema-core": { "optional": false } }, "devDependencies": { "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "@traversable/schema-seed": "workspace:^" } } as const \ No newline at end of file diff --git a/packages/derive-codec/src/bind.ts b/packages/derive-codec/src/bind.ts deleted file mode 100644 index 3e74fb89..00000000 --- a/packages/derive-codec/src/bind.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { Parameters } from '@traversable/registry' - -import { - t, - t_never, - t_unknown, - t_any, - t_void, - t_null, - t_undefined, - t_symbol, - t_boolean, - t_integer, - t_bigint, - t_number, - t_string, - t_eq, - t_optional, - t_array, - t_record, - t_union, - t_intersect, - t_tuple, - t_object, - // t_of, - // def, -} from '@traversable/schema' -import { pipe } from './codec.js' - -const Object_assign = globalThis.Object.assign - -const def = { - never: t.never.def, - any: t.any.def, - unknown: t.unknown.def, - void: t.void.def, - null: t.null.def, - undefined: t.undefined.def, - symbol: t.symbol.def, - boolean: t.boolean.def, - integer: t.integer.def, - bigint: t.bigint.def, - number: t.number.def, - string: t.string.def, - eq: t.eq.def, - optional: t.optional.def, - array: t.array.def, - record: t.record.def, - union: t.union.def, - intersect: t.intersect.def, - tuple: t.tuple.def, - object: t.object.def, - of: t.of.def, -} - -export function bind() { - void Object_assign(t_never, pipe(t.never)); - void Object_assign(t_unknown, pipe(t.unknown)); - void Object_assign(t_any, pipe(t.any)); - void Object_assign(t_void, pipe(t.void)); - void Object_assign(t_null, pipe(t.null)); - void Object_assign(t_undefined, pipe(t.undefined)); - void Object_assign(t_boolean, pipe(t.boolean)); - void Object_assign(t_symbol, pipe(t.symbol)); - void Object_assign(t_integer, pipe(t.integer)); - void Object_assign(t_bigint, pipe(t.bigint)); - void Object_assign(t_number, pipe(t.number)); - void Object_assign(t_string, pipe(t.string)); - void ((t_eq.def as any) = (...args: Parameters) => pipe(def.eq(...args))); - void ((t_optional.def as any) = (...args: Parameters) => pipe(def.optional(...args))); - void ((t_record.def as any) = (...args: Parameters) => pipe(def.record(...args))); - void ((t_array.def as any) = (...args: Parameters) => pipe(def.array(...args))); - void ((t_union.def as any) = (...args: Parameters) => pipe(def.union(...args))); - void ((t_intersect.def as any) = (...args: Parameters) => pipe(def.intersect(...args))); - void ((t_tuple.def as any) = (...args: Parameters) => pipe(def.tuple(...args))); - void ((t_object.def as any) = (...args: Parameters) => pipe(def.object(...args))); -} diff --git a/packages/derive-codec/src/codec.ts b/packages/derive-codec/src/codec.ts index c7cf4378..b1a4ba54 100644 --- a/packages/derive-codec/src/codec.ts +++ b/packages/derive-codec/src/codec.ts @@ -1,4 +1,5 @@ -import type { t } from '@traversable/schema' +import type { Unknown } from '@traversable/registry' +import type { t } from '@traversable/schema-core' /** @internal */ interface Pipelines { @@ -8,19 +9,31 @@ interface Pipelines { } export const Invariant = { - DecodeError: (u: unknown) => globalThis.Error('DecodeError: could not decode invalid input, got: \n\r' + JSON.stringify(u, null, 2)) + DecodeError: (u: unknown) => + Error('DecodeError: could not decode invalid input, got: \n\r' + JSON.stringify(u, null, 2)) } as const export interface Pipe { unpipe(mapBack: (b: B) => T): Codec } - export interface Extend { unextend(mapBack: (s: S) => B): Codec } +export interface BindCodec { + pipe(map: (src: T['_type' & keyof T]) => B): + Pipe + extend(premap: (b: B) => T['_type' & keyof T]): + Extend +} + +export let bindCodec = { + pipe(this: S, mapfn: (src: S['_type']) => B) { return Codec.new(this).pipe(mapfn) }, + extend(this: S, mapfn: (src: S['_type']) => B) { return Codec.new(this).extend(mapfn) }, +} satisfies BindCodec + export class Codec { static new - : (schema: S) => S & { codec: Codec } - = (schema) => { const codec = new Codec(schema); Object.defineProperty(schema, 'codec', { value: codec, writable: true }); return schema as never } + : (schema: S) => Codec + = (schema) => new Codec(schema) - parse(u: S | {} | null | undefined): T | Error { + parse(u: S | Unknown): T | Error { if (typeof this.schema === 'function' && this.schema(u) === false) return Invariant.DecodeError(u) else return this.decode(u as S) @@ -29,7 +42,8 @@ export class Codec { decode(source: S): T decode(source: S) { this._.$ = source - for (let ix = 0, len = this._.to.length; ix < len; ix++) { + let len = this._.to.length + for (let ix = 0; ix < len; ix++) { const f = this._.to[ix] this._.$ = f(this._.$) } @@ -39,7 +53,8 @@ export class Codec { encode(target: T): S encode(target: T) { this._.$ = target - for (let ix = this._.from.length; ix-- !== 0;) { + let len = this._.from.length + for (let ix = len; ix-- !== 0;) { const f = this._.from[ix] this._.$ = f(this._.$) } @@ -68,21 +83,8 @@ export class Codec { } } - private constructor( + constructor( public schema: A, private _: Pipelines = { from: [], to: [], $: void 0 } ) { } } - -export interface pipe { - codec: { - pipe(map: (src: T['_type' & keyof T]) => B): - Pipe - extend(premap: (b: B) => T['_type' & keyof T]): - Extend - } -} - -export function pipe(schema: S): pipe { - return Codec.new(schema) -} diff --git a/packages/derive-codec/src/exports.ts b/packages/derive-codec/src/exports.ts index ff1a5fff..705f9570 100644 --- a/packages/derive-codec/src/exports.ts +++ b/packages/derive-codec/src/exports.ts @@ -5,5 +5,4 @@ export type { } from './codec.js' export { Codec, - pipe, } from './codec.js' diff --git a/packages/derive-codec/src/install.ts b/packages/derive-codec/src/install.ts index 8dd38fac..97f04836 100644 --- a/packages/derive-codec/src/install.ts +++ b/packages/derive-codec/src/install.ts @@ -1,30 +1,58 @@ -import type { t } from '@traversable/schema' -import type { pipe } from './codec.js' +import { Object_assign } from '@traversable/registry' +import { t } from '@traversable/schema-core' -import { bind } from './bind.js' -// SIDE-EFFECT -void bind() +import type { BindCodec } from './codec.js' +import { bindCodec } from './codec.js' -declare module '@traversable/schema' { - interface t_LowerBound extends pipe> { } - interface t_never extends pipe { } - interface t_unknown extends pipe { } - interface t_void extends pipe { } - interface t_any extends pipe { } - interface t_null extends pipe { } - interface t_undefined extends pipe { } - interface t_symbol extends pipe { } - interface t_boolean extends pipe { } - interface t_integer extends pipe { } - interface t_bigint extends pipe { } - interface t_number extends pipe { } - interface t_string extends pipe { } - interface t_eq extends pipe> { } - interface t_optional extends pipe> { } - interface t_array extends pipe> { } - interface t_record extends pipe> { } - interface t_union extends pipe> { } - interface t_intersect extends pipe> { } - interface t_tuple extends pipe> { } - interface t_object extends pipe> { } +declare module '@traversable/schema-core' { + interface t_LowerBound extends BindCodec> { } + // interface t_never extends BindCodec { } + interface t_unknown extends BindCodec { } + interface t_void extends BindCodec { } + interface t_any extends BindCodec { } + interface t_null extends BindCodec { } + interface t_undefined extends BindCodec { } + interface t_symbol extends BindCodec { } + interface t_boolean extends BindCodec { } + interface t_integer extends BindCodec { } + interface t_bigint extends BindCodec { } + interface t_number extends BindCodec { } + interface t_string extends BindCodec { } + interface t_eq extends BindCodec> { } + interface t_optional extends BindCodec> { } + interface t_array extends BindCodec> { } + interface t_record extends BindCodec> { } + interface t_union extends BindCodec> { } + interface t_intersect extends BindCodec> { } + interface t_tuple extends BindCodec> { } + interface t_object extends BindCodec> { } +} + +///////////////// +/// INSTALL /// +void bind() /// +/// INSTALL /// +///////////////// + +export function bind() { + Object_assign(t.never, bindCodec) + Object_assign(t.unknown, bindCodec) + Object_assign(t.any, bindCodec) + Object_assign(t.void, bindCodec) + Object_assign(t.null, bindCodec) + Object_assign(t.undefined, bindCodec) + Object_assign(t.boolean, bindCodec) + Object_assign(t.symbol, bindCodec) + Object_assign(t.integer, bindCodec) + Object_assign(t.bigint, bindCodec) + Object_assign(t.number, bindCodec) + Object_assign(t.string, bindCodec) + Object_assign(t.eq.userDefinitions, bindCodec) + Object_assign(t.optional.userDefinitions, bindCodec) + Object_assign(t.array.userDefinitions, bindCodec) + Object_assign(t.record.userDefinitions, bindCodec) + Object_assign(t.union.userDefinitions, bindCodec) + Object_assign(t.intersect.userDefinitions, bindCodec) + Object_assign(t.tuple.userDefinitions, bindCodec) + Object_assign(t.object.userDefinitions, bindCodec) } diff --git a/packages/derive-codec/test/codec.test.ts b/packages/derive-codec/test/codec.test.ts index 4f970c59..afd805b1 100644 --- a/packages/derive-codec/test/codec.test.ts +++ b/packages/derive-codec/test/codec.test.ts @@ -1,7 +1,7 @@ import * as vi from 'vitest' import { Codec } from '@traversable/derive-codec' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' vi.describe('〖⛳️〗‹‹‹ ❲@traverable/derive-codec❳', () => { vi.it('〖⛳️〗› ❲Codec❳', () => { @@ -35,7 +35,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/derive-codec❳', () => { }] satisfies [any] const codec_01 = Codec - .new(ServerUser).codec + .new(ServerUser) .extend(({ data }: { data: ServerUser }) => data) .unextend((_) => ({ data: _ })) .extend(([_]: [{ data: ServerUser }]) => _) @@ -51,4 +51,3 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/derive-codec❳', () => { vi.assert.deepEqual(codec_01.encode(codec_01.decode(codec_01.encode(codec_01.decode(serverResponse)))), serverResponse) }) }) - diff --git a/packages/derive-codec/test/install.test.ts b/packages/derive-codec/test/install.test.ts index 61b7c7b6..56fd6077 100644 --- a/packages/derive-codec/test/install.test.ts +++ b/packages/derive-codec/test/install.test.ts @@ -1,12 +1,41 @@ -import * as vi from 'vitest' -import { t } from '@traversable/schema' +import { assert, it, describe, vi } from 'vitest' -vi.describe('〖⛳️〗‹‹‹ ❲@traversable/derive-codec❳', () => { - vi.it('〖⛳️〗› ❲pre-install❳', () => vi.assert.isFalse(t.has('codec')(t.string))) +import { t } from '@traversable/schema-core' - vi.it('〖⛳️〗› ❲post-install❳', () => { - import('@traversable/derive-codec/install') - .then(() => vi.assert.isTrue(t.has('codec')(t.string))) - .catch((e) => vi.assert.fail(e.message)) +describe('〖⛳️〗‹‹‹ ❲@traversable/derive-codec❳', async () => { + it('〖⛳️〗› ❲pre-install❳', async () => { + assert.isFalse(t.has('pipe')(t.string)) + assert.isFalse(t.has('extend')(t.string)) + }) + + it('〖⛳️〗› ❲post-install❳', async () => { + assert.isFalse(t.has('pipe')(t.string)) + assert.isFalse(t.has('extend')(t.string)) + + await vi.waitFor(() => import('@traversable/derive-codec/install')) + + assert.isTrue(t.has('pipe')(t.string)) + assert.isTrue(t.has('extend')(t.string)) + + let codec_01 = t.array(t.string) + .pipe((ss) => ss.map(Number)) + .unpipe((xs) => xs.map(String)) + + assert.deepEqual(codec_01.decode(['1', '2', '3']), [1, 2, 3]) + assert.deepEqual(codec_01.encode([1, 2, 3]), ['1', '2', '3']) + + let codec_02 = t.array(t.array(t.integer)) + .pipe((xss) => xss.map((xs) => xs.map((x) => [x] satisfies [any]))) + .unpipe((yss) => yss.map((ys) => ys.map(([y]) => y))) + .pipe((xss) => xss.map((xs) => xs.map(([x]) => [x - 1] satisfies [any]))) + .unpipe((yss) => yss.map((ys) => ys.map(([y]) => [y + 1]))) + + assert.deepEqual(codec_02.decode( + [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + ), [[[-1], [0], [1]], [[2], [3], [4]], [[5], [6], [7]]]) + + assert.deepEqual(codec_02.encode( + [[[-1], [0], [1]], [[2], [3], [4]], [[5], [6], [7]]] + ), [[0, 1, 2], [3, 4, 5], [6, 7, 8]]) }) }) diff --git a/packages/derive-codec/tsconfig.build.json b/packages/derive-codec/tsconfig.build.json index cabdfce7..2972409b 100644 --- a/packages/derive-codec/tsconfig.build.json +++ b/packages/derive-codec/tsconfig.build.json @@ -9,6 +9,6 @@ }, "references": [ { "path": "../registry" }, - { "path": "../schema" } + { "path": "../schema-core" } ] } diff --git a/packages/derive-codec/tsconfig.src.json b/packages/derive-codec/tsconfig.src.json index fa90f525..9416bd78 100644 --- a/packages/derive-codec/tsconfig.src.json +++ b/packages/derive-codec/tsconfig.src.json @@ -8,7 +8,7 @@ }, "references": [ { "path": "../registry" }, - { "path": "../schema" } + { "path": "../schema-core" } ], "include": ["src"] } diff --git a/packages/derive-codec/tsconfig.test.json b/packages/derive-codec/tsconfig.test.json index 80aa9583..3ad0d988 100644 --- a/packages/derive-codec/tsconfig.test.json +++ b/packages/derive-codec/tsconfig.test.json @@ -9,7 +9,7 @@ "references": [ { "path": "tsconfig.src.json" }, { "path": "../registry" }, - { "path": "../schema" }, + { "path": "../schema-core" }, { "path": "../schema-seed" } ], "include": ["test"] diff --git a/packages/derive-equals/package.json b/packages/derive-equals/package.json index 070d2f37..342235ff 100644 --- a/packages/derive-equals/package.json +++ b/packages/derive-equals/package.json @@ -17,7 +17,8 @@ "@traversable": { "generateExports": { "include": [ - "**/*.ts" + "**/*.ts", + "schemas/*.ts" ] }, "generateIndex": { @@ -46,7 +47,7 @@ "peerDependencies": { "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^" + "@traversable/schema-core": "workspace:^" }, "peerDependenciesMeta": { "@traversable/json": { @@ -55,14 +56,14 @@ "@traversable/registry": { "optional": false }, - "@traversable/schema": { + "@traversable/schema-core": { "optional": false } }, "devDependencies": { "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "@traversable/schema-seed": "workspace:^", "@traversable/schema-to-string": "workspace:^" } diff --git a/packages/derive-equals/src/__generated__/__manifest__.ts b/packages/derive-equals/src/__generated__/__manifest__.ts index 19d271aa..3ac6d645 100644 --- a/packages/derive-equals/src/__generated__/__manifest__.ts +++ b/packages/derive-equals/src/__generated__/__manifest__.ts @@ -16,7 +16,7 @@ export default { }, "@traversable": { "generateExports": { - "include": ["**/*.ts"] + "include": ["**/*.ts", "schemas/*.ts"] }, "generateIndex": { "include": ["**/*.ts"] @@ -42,7 +42,7 @@ export default { "peerDependencies": { "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^" + "@traversable/schema-core": "workspace:^" }, "peerDependenciesMeta": { "@traversable/json": { @@ -51,14 +51,14 @@ export default { "@traversable/registry": { "optional": false }, - "@traversable/schema": { + "@traversable/schema-core": { "optional": false } }, "devDependencies": { "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "@traversable/schema-seed": "workspace:^", "@traversable/schema-to-string": "workspace:^" } diff --git a/packages/derive-equals/src/equals.ts b/packages/derive-equals/src/equals.ts index 2fc46842..646f757e 100644 --- a/packages/derive-equals/src/equals.ts +++ b/packages/derive-equals/src/equals.ts @@ -1,7 +1,7 @@ import type { Algebra, Kind } from '@traversable/registry' import { Equal, fn, URI } from '@traversable/registry' import type { Json } from '@traversable/json' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' /** @internal */ type FixUnknown = 0 extends T & 1 ? unknown : T diff --git a/packages/derive-equals/src/install.ts b/packages/derive-equals/src/install.ts index a4fc0eec..23c2cb55 100644 --- a/packages/derive-equals/src/install.ts +++ b/packages/derive-equals/src/install.ts @@ -1,15 +1,20 @@ -import { Equal, has } from '@traversable/registry' -import { t } from '@traversable/schema' - -import * as Eq from './equals.js' +import { + Array_isArray, + Equal, + Object_assign, + Object_hasOwn, + Object_is, + Object_keys, +} from '@traversable/registry' +import { t } from '@traversable/schema-core' export interface equals { equals: Equal } -declare module '@traversable/schema' { +declare module '@traversable/schema-core' { interface t_LowerBound extends equals { } - interface t_never extends equals { } + // interface t_never extends equals { } interface t_unknown extends equals { } interface t_void extends equals { } interface t_any extends equals { } @@ -31,33 +36,7 @@ declare module '@traversable/schema' { interface t_object extends equals { } } -/** @internal */ -const hasEquals - : (u: unknown) => u is { equals: Equal } - = has('equals', (u): u is Equal => typeof u === 'function' && u.length === 2) - -/** @internal */ -const getEquals - : (u: unknown) => Equal - = (u) => hasEquals(u) ? u.equals : Object_is - -/** @internal */ -const Object_assign = globalThis.Object.assign - -/** @internal */ -const Object_keys = globalThis.Object.keys - -/** @internal */ -const Array_isArray = globalThis.Array.isArray - -/** @internal */ -const Object_is = globalThis.Object.is - -/** @internal */ -const hasOwn = (u: unknown, k: K): u is Record => - !!u && typeof u === 'object' && globalThis.Object.prototype.hasOwnProperty.call(u, k) - -export function eqEquals(this: any, x: V, y: V): boolean { return t.eq.def(x)(y) } +export function eqEquals(this: t.eq, x: V, y: V): boolean { return t.eq.def(x)(y) } export function optionalEquals( this: t.optional<{ equals: Equal }>, @@ -99,19 +78,20 @@ export function recordEquals( if (len !== rhs.length) return false for (let ix = len; ix-- !== 0;) { k = lhs[ix] - if (!hasOwn(r, k)) return false + if (!Object_hasOwn(r, k)) return false if (!(this.def.equals(l[k], r[k]))) return false } len = rhs.length for (let ix = len; ix-- !== 0;) { k = rhs[ix] - if (!hasOwn(l, k)) return false + if (!Object_hasOwn(l, k)) return false if (!(this.def.equals(l[k], r[k]))) return false } - return Eq.record(this.def.equals)(l, r) + return true } -export function unionEquals(this: t.union<{ equals: Equal }[]>, +export function unionEquals( + this: t.union<{ equals: Equal }[]>, l: unknown, r: unknown ): boolean { @@ -141,10 +121,10 @@ export function tupleEquals( if (Array_isArray(l)) { if (!Array_isArray(r)) return false for (let ix = this.def.length; ix-- !== 0;) { - if (!hasOwn(l, ix) && !hasOwn(r, ix)) continue - if (hasOwn(l, ix) && !hasOwn(r, ix)) return false - if (!hasOwn(l, ix) && hasOwn(r, ix)) return false - if (hasOwn(l, ix) && hasOwn(r, ix)) { + if (!Object_hasOwn(l, ix) && !Object_hasOwn(r, ix)) continue + if (Object_hasOwn(l, ix) && !Object_hasOwn(r, ix)) return false + if (!Object_hasOwn(l, ix) && Object_hasOwn(r, ix)) return false + if (Object_hasOwn(l, ix) && Object_hasOwn(r, ix)) { if (!this.def[ix].equals(l[ix], r[ix])) return false } } @@ -162,8 +142,8 @@ export function objectEquals( if (!l || typeof l !== 'object' || Array_isArray(l)) return false if (!r || typeof r !== 'object' || Array_isArray(r)) return false for (const k in this.def) { - const lHas = hasOwn(l, k) - const rHas = hasOwn(r, k) + const lHas = Object_hasOwn(l, k) + const rHas = Object_hasOwn(r, k) if (lHas) { if (!rHas) return false if (!this.def[k].equals(l[k], r[k])) return false @@ -183,9 +163,6 @@ void bind() /// /// INSTALL /// ///////////////// - -function stringEquals(l: never, r: never) { return Object_is(l, r) } - export function bind() { Object_assign(t.never, { equals: function neverEquals(l: never, r: never) { return Object_is(l, r) } }) Object_assign(t.unknown, { equals: function unknownEquals(l: never, r: never) { return Object_is(l, r) } }) @@ -198,17 +175,13 @@ export function bind() { Object_assign(t.integer, { equals: function integerEquals(l: never, r: never) { return Object_is(l, r) } }) Object_assign(t.bigint, { equals: function bigintEquals(l: never, r: never) { return Object_is(l, r) } }) Object_assign(t.number, { equals: function numberEquals(l: never, r: never) { return Object_is(l, r) } }) - Object_assign(t.string, { equals: stringEquals.bind(t.string) }) - Object_assign(t.eq.prototype, { equals: eqEquals }) - - // let test = arrayEquals.bind(t.array.prototype) - t.array.prototype.equals = arrayEquals - t.record.prototype.equals = recordEquals - // Object_assign(t.array.prototype, { equals: arrayEquals.bind(t.array.prototype) }) - // Object_assign(t.record.prototype, { equals: recordEquals }) - Object_assign(t.optional.prototype, { equals: optionalEquals }) - Object_assign(t.union.prototype, { equals: unionEquals }) - Object_assign(t.intersect.prototype, { equals: intersectEquals }) - Object_assign(t.tuple.prototype, { equals: tupleEquals }) - Object_assign(t.object.prototype, { equals: objectEquals }) + Object_assign(t.string, { equals: function stringEquals(l: never, r: never) { return Object_is(l, r) } }) + Object_assign(t.eq.userDefinitions, { equals: eqEquals }) + Object_assign(t.array.userDefinitions, { equals: arrayEquals }) + Object_assign(t.record.userDefinitions, { equals: recordEquals }) + Object_assign(t.optional.userDefinitions, { equals: optionalEquals }) + Object_assign(t.union.userDefinitions, { equals: unionEquals }) + Object_assign(t.intersect.userDefinitions, { equals: intersectEquals }) + Object_assign(t.tuple.userDefinitions, { equals: tupleEquals }) + Object_assign(t.object.userDefinitions, { equals: objectEquals }) } diff --git a/packages/derive-equals/src/schemas/any.ts b/packages/derive-equals/src/schemas/any.ts new file mode 100644 index 00000000..09d8da5f --- /dev/null +++ b/packages/derive-equals/src/schemas/any.ts @@ -0,0 +1,7 @@ +import type { Equal } from "@traversable/registry" +import { Object_is } from "@traversable/registry" + +export type equals = Equal +export function equals(left: unknown, right: unknown): boolean { + return Object_is(left, right) +} diff --git a/packages/derive-equals/src/schemas/array.ts b/packages/derive-equals/src/schemas/array.ts new file mode 100644 index 00000000..aa06a7bf --- /dev/null +++ b/packages/derive-equals/src/schemas/array.ts @@ -0,0 +1,24 @@ +import type { Equal } from '@traversable/registry' +import { has, Array_isArray, Object_is } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = never | Equal + +export function equals(arraySchema: t.array): equals +export function equals(arraySchema: t.array): equals +export function equals({ def }: t.array<{ equals: Equal }>): Equal { + let equals = has('equals', (x): x is Equal => typeof x === 'function')(def) ? def.equals : Object_is + function arrayEquals(l: unknown[], r: unknown[]): boolean { + if (Object_is(l, r)) return true + if (Array_isArray(l)) { + if (!Array_isArray(r)) return false + let len = l.length + if (len !== r.length) return false + for (let ix = len; ix-- !== 0;) + if (!equals(l[ix], r[ix])) return false + return true + } else return false + } + return arrayEquals +} + diff --git a/packages/derive-equals/src/schemas/bigint.ts b/packages/derive-equals/src/schemas/bigint.ts new file mode 100644 index 00000000..7c98bbf1 --- /dev/null +++ b/packages/derive-equals/src/schemas/bigint.ts @@ -0,0 +1,7 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' + +export type equals = Equal +export function equals(left: bigint, right: bigint): boolean { + return Object_is(left, right) +} diff --git a/packages/derive-equals/src/schemas/boolean.ts b/packages/derive-equals/src/schemas/boolean.ts new file mode 100644 index 00000000..306bb12b --- /dev/null +++ b/packages/derive-equals/src/schemas/boolean.ts @@ -0,0 +1,7 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' + +export type equals = Equal +export function equals(left: boolean, right: boolean): boolean { + return Object_is(left, right) +} diff --git a/packages/derive-equals/src/schemas/eq.ts b/packages/derive-equals/src/schemas/eq.ts new file mode 100644 index 00000000..774127f6 --- /dev/null +++ b/packages/derive-equals/src/schemas/eq.ts @@ -0,0 +1,11 @@ +import type { Equal } from '@traversable/registry' +import { laxEquals } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = never | Equal +export function equals(eqSchema: t.eq): equals +export function equals(): Equal { + return function eqEquals(left: any, right: any) { + return laxEquals(left, right) + } +} diff --git a/packages/derive-equals/src/schemas/integer.ts b/packages/derive-equals/src/schemas/integer.ts new file mode 100644 index 00000000..29fcd602 --- /dev/null +++ b/packages/derive-equals/src/schemas/integer.ts @@ -0,0 +1,7 @@ +import type { Equal } from '@traversable/registry' +import { SameValueNumber } from '@traversable/registry' + +export type equals = Equal +export function equals(left: number, right: number): boolean { + return SameValueNumber(left, right) +} diff --git a/packages/derive-equals/src/schemas/intersect.ts b/packages/derive-equals/src/schemas/intersect.ts new file mode 100644 index 00000000..ce880aa1 --- /dev/null +++ b/packages/derive-equals/src/schemas/intersect.ts @@ -0,0 +1,16 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = Equal +export function equals(intersectSchema: t.intersect<[...S]>): equals +export function equals(intersectSchema: t.intersect<[...S]>): equals +export function equals({ def }: t.intersect<{ equals: Equal }[]>): Equal { + function intersectEquals(l: unknown, r: unknown): boolean { + if (Object_is(l, r)) return true + for (let ix = def.length; ix-- !== 0;) + if (!def[ix].equals(l, r)) return false + return true + } + return intersectEquals +} diff --git a/packages/derive-equals/src/schemas/never.ts b/packages/derive-equals/src/schemas/never.ts new file mode 100644 index 00000000..3ed89421 --- /dev/null +++ b/packages/derive-equals/src/schemas/never.ts @@ -0,0 +1,6 @@ +import type { Equal } from '@traversable/registry' + +export type equals = Equal +export function equals(left: never, right: never): boolean { + return false +} diff --git a/packages/derive-equals/src/schemas/null.ts b/packages/derive-equals/src/schemas/null.ts new file mode 100644 index 00000000..12c2f636 --- /dev/null +++ b/packages/derive-equals/src/schemas/null.ts @@ -0,0 +1,7 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' + +export type equals = Equal +export function equals(left: null, right: null): boolean { + return Object_is(left, right) +} diff --git a/packages/derive-equals/src/schemas/number.ts b/packages/derive-equals/src/schemas/number.ts new file mode 100644 index 00000000..29fcd602 --- /dev/null +++ b/packages/derive-equals/src/schemas/number.ts @@ -0,0 +1,7 @@ +import type { Equal } from '@traversable/registry' +import { SameValueNumber } from '@traversable/registry' + +export type equals = Equal +export function equals(left: number, right: number): boolean { + return SameValueNumber(left, right) +} diff --git a/packages/derive-equals/src/schemas/object.ts b/packages/derive-equals/src/schemas/object.ts new file mode 100644 index 00000000..ecda0b32 --- /dev/null +++ b/packages/derive-equals/src/schemas/object.ts @@ -0,0 +1,55 @@ +import type * as T from '@traversable/registry' +import { Array_isArray, Object_hasOwn, Object_is } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = never | T.Equal +export function equals(objectSchema: t.object): equals> +export function equals(objectSchema: t.object): equals> +export function equals({ def }: t.object): equals> { + function objectEquals(l: { [x: string]: unknown }, r: { [x: string]: unknown }) { + if (Object_is(l, r)) return true + if (!l || typeof l !== 'object' || Array_isArray(l)) return false + if (!r || typeof r !== 'object' || Array_isArray(r)) return false + for (const k in def) { + const lHas = Object_hasOwn(l, k) + const rHas = Object_hasOwn(r, k) + if (lHas) { + if (!rHas) return false + if (!def[k].equals(l[k], r[k])) return false + } + if (rHas) { + if (!lHas) return false + if (!def[k].equals(l[k], r[k])) return false + } + if (!def[k].equals(l[k], r[k])) return false + } + return true + } + return objectEquals +} + +// export type equals = never | T.Equal +// export function equals(objectSchema: t.object): equals> +// export function equals(objectSchema: t.object): equals> +// export function equals({ def }: t.object<{ [x: string]: { equals: T.Equal } }>): T.Equal<{ [x: string]: unknown }> { +// function objectEquals(l: { [x: string]: unknown }, r: { [x: string]: unknown }) { +// if (Object_is(l, r)) return true +// if (!l || typeof l !== 'object' || Array_isArray(l)) return false +// if (!r || typeof r !== 'object' || Array_isArray(r)) return false +// for (const k in def) { +// const lHas = Object_hasOwn(l, k) +// const rHas = Object_hasOwn(r, k) +// if (lHas) { +// if (!rHas) return false +// if (!def[k].equals(l[k], r[k])) return false +// } +// if (rHas) { +// if (!lHas) return false +// if (!def[k].equals(l[k], r[k])) return false +// } +// if (!def[k].equals(l[k], r[k])) return false +// } +// return true +// } +// return objectEquals +// } diff --git a/packages/derive-equals/src/schemas/of.ts b/packages/derive-equals/src/schemas/of.ts new file mode 100644 index 00000000..38809729 --- /dev/null +++ b/packages/derive-equals/src/schemas/of.ts @@ -0,0 +1,6 @@ +import { Equal } from '@traversable/registry' + +export type equals = Equal +export function equals(left: T, right: T): boolean { + return Equal.lax(left, right) +} diff --git a/packages/derive-equals/src/schemas/optional.ts b/packages/derive-equals/src/schemas/optional.ts new file mode 100644 index 00000000..25944376 --- /dev/null +++ b/packages/derive-equals/src/schemas/optional.ts @@ -0,0 +1,13 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = never | Equal +export function equals(optionalSchema: t.optional): equals +export function equals(optionalSchema: t.optional): equals +export function equals({ def }: t.optional<{ equals: Equal }>): Equal { + return function optionalEquals(l: unknown, r: unknown): boolean { + if (Object_is(l, r)) return true + return def.equals(l, r) + } +} diff --git a/packages/derive-equals/src/schemas/record.ts b/packages/derive-equals/src/schemas/record.ts new file mode 100644 index 00000000..07addff1 --- /dev/null +++ b/packages/derive-equals/src/schemas/record.ts @@ -0,0 +1,32 @@ +import type { Equal } from '@traversable/registry' +import { Array_isArray, Object_is, Object_keys, Object_hasOwn } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = never | Equal +export function equals(recordSchema: t.record): equals +export function equals(recordSchema: t.record): equals +export function equals({ def }: t.record<{ equals: Equal }>): Equal> { + function recordEquals(l: Record, r: Record): boolean { + if (Object_is(l, r)) return true + if (!l || typeof l !== 'object' || Array_isArray(l)) return false + if (!r || typeof r !== 'object' || Array_isArray(r)) return false + const lhs = Object_keys(l) + const rhs = Object_keys(r) + let len = lhs.length + let k: string + if (len !== rhs.length) return false + for (let ix = len; ix-- !== 0;) { + k = lhs[ix] + if (!Object_hasOwn(r, k)) return false + if (!(def.equals(l[k], r[k]))) return false + } + len = rhs.length + for (let ix = len; ix-- !== 0;) { + k = rhs[ix] + if (!Object_hasOwn(l, k)) return false + if (!(def.equals(l[k], r[k]))) return false + } + return true + } + return recordEquals +} diff --git a/packages/derive-equals/src/schemas/string.ts b/packages/derive-equals/src/schemas/string.ts new file mode 100644 index 00000000..b9444108 --- /dev/null +++ b/packages/derive-equals/src/schemas/string.ts @@ -0,0 +1,6 @@ +import type { Equal } from '@traversable/registry' + +export type equals = Equal +export function equals(left: string, right: string): boolean { + return left === right +} diff --git a/packages/derive-equals/src/schemas/symbol.ts b/packages/derive-equals/src/schemas/symbol.ts new file mode 100644 index 00000000..f3bb7486 --- /dev/null +++ b/packages/derive-equals/src/schemas/symbol.ts @@ -0,0 +1,7 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' + +export type equals = Equal +export function equals(left: symbol, right: symbol): boolean { + return Object_is(left, right) +} diff --git a/packages/derive-equals/src/schemas/tuple.ts b/packages/derive-equals/src/schemas/tuple.ts new file mode 100644 index 00000000..66adadaa --- /dev/null +++ b/packages/derive-equals/src/schemas/tuple.ts @@ -0,0 +1,27 @@ +import type { Equal } from '@traversable/registry' +import { Array_isArray, Object_hasOwn, Object_is } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = Equal + +export function equals(tupleSchema: t.tuple): equals +export function equals(tupleSchema: t.tuple): equals +export function equals(tupleSchema: t.tuple) { + function tupleEquals(l: typeof tupleSchema['_type'], r: typeof tupleSchema['_type']): boolean { + if (Object_is(l, r)) return true + if (Array_isArray(l)) { + if (!Array_isArray(r)) return false + for (let ix = tupleSchema.def.length; ix-- !== 0;) { + if (!Object_hasOwn(l, ix) && !Object_hasOwn(r, ix)) continue + if (Object_hasOwn(l, ix) && !Object_hasOwn(r, ix)) return false + if (!Object_hasOwn(l, ix) && Object_hasOwn(r, ix)) return false + if (Object_hasOwn(l, ix) && Object_hasOwn(r, ix)) { + if (!tupleSchema.def[ix].equals(l[ix], r[ix])) return false + } + } + return true + } + return false + } + return tupleEquals +} diff --git a/packages/derive-equals/src/schemas/undefined.ts b/packages/derive-equals/src/schemas/undefined.ts new file mode 100644 index 00000000..75156d56 --- /dev/null +++ b/packages/derive-equals/src/schemas/undefined.ts @@ -0,0 +1,7 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' + +export type equals = Equal +export function equals(left: undefined, right: undefined): boolean { + return Object_is(left, right) +} diff --git a/packages/derive-equals/src/schemas/union.ts b/packages/derive-equals/src/schemas/union.ts new file mode 100644 index 00000000..c0275e69 --- /dev/null +++ b/packages/derive-equals/src/schemas/union.ts @@ -0,0 +1,16 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = Equal +export function equals(unionSchema: t.union<[...S]>): equals +export function equals(unionSchema: t.union<[...S]>): equals +export function equals({ def }: t.union<{ equals: Equal }[]>): Equal { + function unionEquals(l: unknown, r: unknown): boolean { + if (Object_is(l, r)) return true + for (let ix = def.length; ix-- !== 0;) + if (def[ix].equals(l, r)) return true + return false + } + return unionEquals +} diff --git a/packages/derive-equals/src/schemas/unknown.ts b/packages/derive-equals/src/schemas/unknown.ts new file mode 100644 index 00000000..ccd2a780 --- /dev/null +++ b/packages/derive-equals/src/schemas/unknown.ts @@ -0,0 +1,7 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' + +export type equals = Equal +export function equals(left: any, right: any): boolean { + return Object_is(left, right) +} diff --git a/packages/derive-equals/src/schemas/void.ts b/packages/derive-equals/src/schemas/void.ts new file mode 100644 index 00000000..d11d89e3 --- /dev/null +++ b/packages/derive-equals/src/schemas/void.ts @@ -0,0 +1,7 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' + +export type equals = Equal +export function equals(left: void, right: void): boolean { + return Object_is(left, right) +} diff --git a/packages/derive-equals/test/equals.test.ts b/packages/derive-equals/test/equals.test.ts index a08e6d89..4a7cf067 100644 --- a/packages/derive-equals/test/equals.test.ts +++ b/packages/derive-equals/test/equals.test.ts @@ -4,7 +4,7 @@ import * as NodeJSUtil from 'node:util' import type { Algebra, Kind } from '@traversable/registry' import { Equal, fn, omitMethods, URI } from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import { Seed } from '@traversable/schema-seed' import { Eq } from '@traversable/derive-equals' diff --git a/packages/derive-equals/test/install.test.ts b/packages/derive-equals/test/install.test.ts index 02d25347..1a409bdd 100644 --- a/packages/derive-equals/test/install.test.ts +++ b/packages/derive-equals/test/install.test.ts @@ -1,12 +1,33 @@ -import * as vi from 'vitest' -import { t } from '@traversable/schema' +import { assert, describe, it, vi } from 'vitest' +import { t } from '@traversable/schema-core' -vi.describe('〖⛳️〗‹‹‹ ❲@traversable/derive-equals❳', () => { - vi.it('〖⛳️〗› ❲pre-install❳', () => vi.assert.isFalse(t.has('equals')(t.string))) +describe('〖⛳️〗‹‹‹ ❲@traversable/derive-equals❳', async () => { + it('〖⛳️〗› ❲pre-install❳', () => assert.isFalse(t.has('equals')(t.string))) - vi.it('〖⛳️〗› ❲post-install❳', () => { - import('@traversable/derive-equals/install') - .then(() => vi.assert.isTrue(t.has('equals')(t.string))) - .catch((e) => vi.assert.fail(e.message)) + it('〖⛳️〗› ❲post-install❳', async () => { + assert.isFalse(t.has('equals')(t.string)) + assert.isFalse(t.has('equals')(t.array(t.string))) + + await vi.waitFor(() => import('@traversable/derive-equals/install')) + + assert.isTrue(t.has('equals')(t.string)) + assert.isTrue(t.has('equals')(t.array(t.string))) + + assert.isTrue(t.string.equals('', '')) + assert.isFalse(t.string.equals('a', 'b')) + + assert.isTrue(t.array(t.string).equals([], [])) + assert.isTrue(t.array(t.string).equals([''], [''])) + assert.isFalse(t.array(t.string).equals(['a'], [])) + assert.isFalse(t.array(t.string).equals(['a'], ['b'])) + + assert.isTrue(t.array(t.array(t.string)).equals([], [])) + assert.isTrue(t.array(t.array(t.string)).equals([[]], [[]])) + assert.isTrue(t.array(t.array(t.string)).equals([['a']], [['a']])) + assert.isTrue(t.array(t.array(t.string)).equals([[], ['b']], [[], ['b']])) + + assert.isFalse(t.array(t.array(t.string)).equals([[]], [['c']])) + assert.isFalse(t.array(t.array(t.string)).equals([['d']], [['e']])) + assert.isFalse(t.array(t.array(t.string)).equals([['f']], [['f', 'g']])) }) }) diff --git a/packages/derive-equals/tsconfig.build.json b/packages/derive-equals/tsconfig.build.json index 30eb210a..5dac57a7 100644 --- a/packages/derive-equals/tsconfig.build.json +++ b/packages/derive-equals/tsconfig.build.json @@ -10,6 +10,6 @@ "references": [ { "path": "../json" }, { "path": "../registry" }, - { "path": "../schema" }, + { "path": "../schema-core" }, ] } diff --git a/packages/derive-equals/tsconfig.src.json b/packages/derive-equals/tsconfig.src.json index 0f1e77bf..acd96694 100644 --- a/packages/derive-equals/tsconfig.src.json +++ b/packages/derive-equals/tsconfig.src.json @@ -9,7 +9,7 @@ "references": [ { "path": "../json" }, { "path": "../registry" }, - { "path": "../schema" }, + { "path": "../schema-core" }, ], "include": ["src"] } diff --git a/packages/derive-equals/tsconfig.test.json b/packages/derive-equals/tsconfig.test.json index 77da0195..aa484eed 100644 --- a/packages/derive-equals/tsconfig.test.json +++ b/packages/derive-equals/tsconfig.test.json @@ -10,7 +10,7 @@ { "path": "tsconfig.src.json" }, { "path": "../json" }, { "path": "../registry" }, - { "path": "../schema" }, + { "path": "../schema-core" }, { "path": "../schema-seed" }, { "path": "../schema-to-string" }, ], diff --git a/packages/derive-validators/package.json b/packages/derive-validators/package.json index 952cfda2..f8301dac 100644 --- a/packages/derive-validators/package.json +++ b/packages/derive-validators/package.json @@ -49,12 +49,12 @@ "peerDependencies": { "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^" + "@traversable/schema-core": "workspace:^" }, "devDependencies": { "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "@traversable/schema-seed": "workspace:^" } } diff --git a/packages/derive-validators/src/__generated__/__manifest__.ts b/packages/derive-validators/src/__generated__/__manifest__.ts index e2451c33..05818dcd 100644 --- a/packages/derive-validators/src/__generated__/__manifest__.ts +++ b/packages/derive-validators/src/__generated__/__manifest__.ts @@ -43,12 +43,12 @@ export default { "peerDependencies": { "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^" + "@traversable/schema-core": "workspace:^" }, "devDependencies": { "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "@traversable/schema-seed": "workspace:^" } } as const \ No newline at end of file diff --git a/packages/derive-validators/src/errors.ts b/packages/derive-validators/src/errors.ts index c8738280..4330cccd 100644 --- a/packages/derive-validators/src/errors.ts +++ b/packages/derive-validators/src/errors.ts @@ -1,4 +1,4 @@ -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' export interface ValidationError { kind: string @@ -38,7 +38,14 @@ export const ErrorType = { OutOfBounds: 'OUT_OF_BOUNDS', } as const satisfies Record -function error(kind: T, path: (keyof any)[], got: unknown, msg: string | undefined, expected: unknown, schemaPath: (keyof any)[]): { +function error( + kind: T, + path: (keyof any)[], + got: unknown, + msg: string | undefined, + expected: unknown, + schemaPath: (keyof any)[] +): { kind: typeof kind path: typeof path got: typeof got @@ -46,25 +53,51 @@ function error(kind: T, path: (keyof any)[], got: unknown, msg expected: typeof expected schemaPath: typeof schemaPath } -function error(kind: T, path: (keyof any)[], got: unknown, msg: string | undefined, expected: unknown): { + +function error( + kind: T, + path: (keyof any)[], + got: unknown, + msg: string | undefined, + expected: unknown +): { kind: typeof kind path: typeof path got: typeof got msg: typeof msg expected: typeof expected } -function error(kind: T, path: (keyof any)[], got: unknown, msg: string): { + +function error( + kind: T, + path: (keyof any)[], + got: unknown, + msg: string +): { kind: typeof kind path: typeof path got: typeof got msg: typeof msg } -function error(kind: T, path: (keyof any)[], got: unknown): { + +function error( + kind: T, + path: (keyof any)[], + got: unknown +): { kind: typeof kind path: typeof path got: typeof got } -function error(kind: T, path: (keyof any)[], got: unknown, msg?: string, expected?: unknown, schemaPath?: (keyof any)[]): ValidationError { + +function error( + kind: T, + path: (keyof any)[], + got: unknown, + msg?: string, + expected?: unknown, + schemaPath?: (keyof any)[] +): ValidationError { return { kind, path: dataPath(path), @@ -92,6 +125,7 @@ export const NULLARY = { array: (got, path) => error(ErrorType.TypeMismatch, path, got, 'Expected array'), record: (got, path) => error(ErrorType.TypeMismatch, path, got, 'Expected object'), optional: (got, path) => error(ErrorType.TypeMismatch, path, got, 'Expected optional'), + inline: (got, path) => error(ErrorType.TypeMismatch, path, got, 'Expected input to satisfy inline predicate', 'value that satisfies the predicate'), } as const satisfies Record ValidationError> const gteErrorMessage = (type: string) => (x: number | bigint, got: unknown) => 'Expected ' + type + ' to be greater than or equal to ' + x + ', got: ' + globalThis.String(got) diff --git a/packages/derive-validators/src/exports.ts b/packages/derive-validators/src/exports.ts index 0a9ffc1d..96fb3b2b 100644 --- a/packages/derive-validators/src/exports.ts +++ b/packages/derive-validators/src/exports.ts @@ -1,20 +1,25 @@ -export { fromSchema, fromSchemaWithOptions } from './recursive.js' - export { VERSION } from './version.js' - -export type { - ValidationFn, - Validate, - Options, -} from './shared.js' -export { isOptional } from './shared.js' - export type { ValidationError, } from './errors.js' - export { + NULLARY as NullaryErrors, + UNARY as UnaryErrors, ERROR as Errors, ErrorType, dataPath as dataPathFromSchemaPath, } from './errors.js' +export { + fromSchema, + fromSchemaWithOptions, +} from './recursive.js' +export type { + ValidationFn, + Validate, + Options, +} from './shared.js' +export { + hasOptionalSymbol, + hasValidate, + callValidate, +} from './shared.js' diff --git a/packages/derive-validators/src/index.ts b/packages/derive-validators/src/index.ts index e91c4858..e2d2295d 100644 --- a/packages/derive-validators/src/index.ts +++ b/packages/derive-validators/src/index.ts @@ -1,3 +1,3 @@ export * from './exports.js' export * as Validator from './exports.js' -export type Validator = import('./shared.js').Validator +export type Validator = import('./shared.js').Validator diff --git a/packages/derive-validators/src/install.ts b/packages/derive-validators/src/install.ts index 2528bd7d..9251145b 100644 --- a/packages/derive-validators/src/install.ts +++ b/packages/derive-validators/src/install.ts @@ -1,31 +1,32 @@ -import { t } from '@traversable/schema' -import * as proto from './prototype.js' -import type { Validate } from './shared.js' +import { Object_assign } from '@traversable/registry' +import { t } from '@traversable/schema-core' +import * as validate from './validate.js' +import type { ValidationFn_ as ValidationFn } from './shared.js' -declare module '@traversable/schema' { - interface t_Lower { validate: Validate } - interface t_never { validate: Validate } - interface t_unknown { validate: Validate } - interface t_void { validate: Validate } - interface t_any { validate: Validate } - interface t_null { validate: Validate } - interface t_undefined { validate: Validate } - interface t_symbol { validate: Validate } - interface t_boolean { validate: Validate } - interface t_integer { validate: Validate } - interface t_bigint { validate: Validate } - interface t_number { validate: Validate } - interface t_string { validate: Validate } - interface t_eq { validate: Validate } - interface t_optional { validate: Validate> } - interface t_array { validate: Validate> } - interface t_record { validate: Validate> } - interface t_union { validate: Validate> } - interface t_intersect { validate: Validate> } - interface t_tuple { validate: Validate> } - interface t_object { validate: Validate> } - interface t_of { validate: Validate> } - interface t_enum { validate: Validate> } +declare module '@traversable/schema-core' { + interface t_Lower { validate: ValidationFn } + // interface t_never { validate: ValidationFn } + interface t_unknown { validate: ValidationFn } + interface t_void { validate: ValidationFn } + interface t_any { validate: ValidationFn } + interface t_null { validate: ValidationFn } + interface t_undefined { validate: ValidationFn } + interface t_symbol { validate: ValidationFn } + interface t_boolean { validate: ValidationFn } + interface t_integer { validate: ValidationFn } + interface t_bigint { validate: ValidationFn } + interface t_number { validate: ValidationFn } + interface t_string { validate: ValidationFn } + interface t_eq { validate: ValidationFn } + interface t_optional { validate: ValidationFn } + interface t_array { validate: ValidationFn } + interface t_record { validate: ValidationFn } + interface t_union { validate: ValidationFn } + interface t_intersect { validate: ValidationFn } + interface t_tuple { validate: ValidationFn } + interface t_object { validate: ValidationFn } + interface t_of { validate: ValidationFn } + interface t_enum { validate: ValidationFn } } ///////////////// @@ -34,29 +35,26 @@ void bind() /// /// INSTALL /// ///////////////// - export function bind() { - /** @internal */ - let Object_assign = globalThis.Object.assign - Object_assign(t.never, { validate: proto.never }) - Object_assign(t.unknown, { validate: proto.unknown }) - Object_assign(t.any, { validate: proto.any }) - Object_assign(t.void, { validate: proto.void }) - Object_assign(t.null, { validate: proto.null }) - Object_assign(t.undefined, { validate: proto.undefined }) - Object_assign(t.symbol, { validate: proto.symbol }) - Object_assign(t.boolean, { validate: proto.boolean }) - Object_assign(t.integer, { validate: proto.integer }) - Object_assign(t.bigint, { validate: proto.bigint }) - Object_assign(t.number, { validate: proto.number }) - Object_assign(t.string, { validate: proto.string }) - Object_assign(t.optional.prototype, { validate: proto.optional }) - Object_assign(t.eq.prototype, { validate: proto.eq }) - Object_assign(t.array.prototype, { validate: proto.array }) - Object_assign(t.record.prototype, { validate: proto.record }) - Object_assign(t.union.prototype, { validate: proto.union }) - Object_assign(t.intersect.prototype, { validate: proto.intersect }) - Object_assign(t.tuple.prototype, { validate: proto.tuple }) - Object_assign(t.object.prototype, { validate: proto.object }) - Object_assign(t.enum.prototype, { validate: proto.enum }) + Object_assign(t.never, { validate: validate.never }) + Object_assign(t.unknown, { validate: validate.unknown }) + Object_assign(t.any, { validate: validate.any }) + Object_assign(t.void, { validate: validate.void }) + Object_assign(t.null, { validate: validate.null }) + Object_assign(t.undefined, { validate: validate.undefined }) + Object_assign(t.symbol, { validate: validate.symbol }) + Object_assign(t.boolean, { validate: validate.boolean }) + Object_assign(t.integer, { validate: validate.integer }) + Object_assign(t.bigint, { validate: validate.bigint }) + Object_assign(t.number, { validate: validate.number }) + Object_assign(t.string, { validate: validate.string }) + Object_assign(t.eq.userDefinitions, { validate: validate.eq }) + Object_assign(t.optional.userDefinitions, { validate: validate.optional }) + Object_assign(t.array.userDefinitions, { validate: validate.array }) + Object_assign(t.record.userDefinitions, { validate: validate.record }) + Object_assign(t.union.userDefinitions, { validate: validate.union }) + Object_assign(t.intersect.userDefinitions, { validate: validate.intersect }) + Object_assign(t.tuple.userDefinitions, { validate: validate.tuple }) + Object_assign(t.object.userDefinitions, { validate: validate.object }) + Object_assign(t.enum.userDefinitions, { validate: validate.enum }) } diff --git a/packages/derive-validators/src/recursive.ts b/packages/derive-validators/src/recursive.ts index f47d4e76..10ade016 100644 --- a/packages/derive-validators/src/recursive.ts +++ b/packages/derive-validators/src/recursive.ts @@ -1,11 +1,11 @@ import type { IndexedAlgebra } from '@traversable/registry' import { Equal, fn, symbol, typeName, URI } from '@traversable/registry' -import { t, getConfig } from '@traversable/schema' +import { t, getConfig } from '@traversable/schema-core' import type { ValidationError } from './errors.js' import { BOUNDS, ERROR, UNARY } from './errors.js' import type { Options, ValidationFn } from './shared.js' -import { isOptional } from './shared.js' +import { hasOptionalSymbol } from './shared.js' /** @internal */ const Array_isArray = globalThis.Array.isArray @@ -282,7 +282,7 @@ const union } return errors.length > 0 ? errors : true } - if (validationFns.every(isOptional)) validateUnion[symbol.optional] = true + if (validationFns.every(hasOptionalSymbol)) validateUnion[symbol.optional] = true validateUnion.tag = URI.union validateUnion.ctx = Array.of() return validateUnion diff --git a/packages/derive-validators/src/schemas/any.ts b/packages/derive-validators/src/schemas/any.ts new file mode 100644 index 00000000..d286c4b5 --- /dev/null +++ b/packages/derive-validators/src/schemas/any.ts @@ -0,0 +1,10 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(_?: t.any): validate { + validateAny.tag = URI.any + function validateAny() { return true as const } + return validateAny +} diff --git a/packages/derive-validators/src/schemas/array.ts b/packages/derive-validators/src/schemas/array.ts new file mode 100644 index 00000000..4d8e7958 --- /dev/null +++ b/packages/derive-validators/src/schemas/array.ts @@ -0,0 +1,27 @@ +import { URI } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { ValidationError, ValidationFn, Validator } from '@traversable/derive-validators' +import { Errors, NullaryErrors } from '@traversable/derive-validators' + +export type validate = never | ValidationFn +export function validate(arraySchema: t.array): validate +export function validate(arraySchema: t.array): validate +export function validate( + { def: { validate = () => true }, minLength, maxLength }: t.array +) { + validateArray.tag = URI.array + function validateArray(u: unknown, path = Array.of()) { + if (!Array.isArray(u)) return [NullaryErrors.array(u, path)] + let errors = Array.of() + if (typeof minLength === 'number' && u.length < minLength) errors.push(Errors.arrayMinLength(u, path, minLength)) + if (typeof maxLength === 'number' && u.length > maxLength) errors.push(Errors.arrayMaxLength(u, path, maxLength)) + for (let i = 0, len = u.length; i < len; i++) { + let y = u[i] + let results = validate(y, [...path, i]) + if (results === true) continue + else errors.push(...results) + } + return errors.length === 0 || errors + } + return validateArray +} diff --git a/packages/derive-validators/src/schemas/bigint.ts b/packages/derive-validators/src/schemas/bigint.ts new file mode 100644 index 00000000..7e3284da --- /dev/null +++ b/packages/derive-validators/src/schemas/bigint.ts @@ -0,0 +1,13 @@ +import type { ValidationError, ValidationFn } from '@traversable/derive-validators' +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(bigIntSchema: S): validate { + validateBigInt.tag = URI.bigint + function validateBigInt(u: unknown, path = Array.of()): true | ValidationError[] { + return bigIntSchema(u) || [NullaryErrors.bigint(u, path)] + } + return validateBigInt +} diff --git a/packages/derive-validators/src/schemas/boolean.ts b/packages/derive-validators/src/schemas/boolean.ts new file mode 100644 index 00000000..76cee261 --- /dev/null +++ b/packages/derive-validators/src/schemas/boolean.ts @@ -0,0 +1,12 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import { NullaryErrors, type ValidationFn } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(booleanSchema: t.boolean): validate { + validateBoolean.tag = URI.boolean + function validateBoolean(u: unknown, path = Array.of()) { + return booleanSchema(true as const) || [NullaryErrors.null(u, path)] + } + return validateBoolean +} diff --git a/packages/derive-validators/src/schemas/eq.ts b/packages/derive-validators/src/schemas/eq.ts new file mode 100644 index 00000000..2f02ba7d --- /dev/null +++ b/packages/derive-validators/src/schemas/eq.ts @@ -0,0 +1,17 @@ +import { Equal, getConfig, URI } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { Validate } from '@traversable/derive-validators' +import { Errors } from '@traversable/derive-validators' + +export type validate = Validate +export function validate(eqSchema: t.eq): validate +export function validate({ def }: t.eq): validate { + validateEq.tag = URI.eq + function validateEq(u: unknown, path = Array.of()) { + let options = getConfig().schema + let equals = options?.eq?.equalsFn || Equal.lax + if (equals(def, u)) return true + else return [Errors.eq(u, path, def)] + } + return validateEq +} diff --git a/packages/derive-validators/src/schemas/integer.ts b/packages/derive-validators/src/schemas/integer.ts new file mode 100644 index 00000000..5f044c5d --- /dev/null +++ b/packages/derive-validators/src/schemas/integer.ts @@ -0,0 +1,13 @@ +import type { ValidationError, ValidationFn } from '@traversable/derive-validators' +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(integerSchema: S): validate { + validateInteger.tag = URI.integer + function validateInteger(u: unknown, path = Array.of()): true | ValidationError[] { + return integerSchema(u) || [NullaryErrors.integer(u, path)] + } + return validateInteger +} diff --git a/packages/derive-validators/src/schemas/intersect.ts b/packages/derive-validators/src/schemas/intersect.ts new file mode 100644 index 00000000..8a586df2 --- /dev/null +++ b/packages/derive-validators/src/schemas/intersect.ts @@ -0,0 +1,21 @@ +import { URI } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { ValidationError, Validate, Validator } from '@traversable/derive-validators' + +export type validate = Validate + +export function validate(intersectSchema: t.intersect): validate +export function validate(intersectSchema: t.intersect): validate +export function validate({ def }: t.intersect) { + validateIntersect.tag = URI.intersect + function validateIntersect(u: unknown, path = Array.of()): true | ValidationError[] { + let errors = Array.of() + for (let i = 0; i < def.length; i++) { + let results = def[i].validate(u, path) + if (results !== true) + for (let j = 0; j < results.length; j++) errors.push(results[j]) + } + return errors.length === 0 || errors + } + return validateIntersect +} diff --git a/packages/derive-validators/src/schemas/never.ts b/packages/derive-validators/src/schemas/never.ts new file mode 100644 index 00000000..52ce9ee5 --- /dev/null +++ b/packages/derive-validators/src/schemas/never.ts @@ -0,0 +1,10 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import { NullaryErrors, type ValidationFn } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(_?: t.never): validate { + validateNever.tag = URI.never + function validateNever(u: unknown, path = Array.of()) { return [NullaryErrors.never(u, path)] } + return validateNever +} diff --git a/packages/derive-validators/src/schemas/null.ts b/packages/derive-validators/src/schemas/null.ts new file mode 100644 index 00000000..b2abe36b --- /dev/null +++ b/packages/derive-validators/src/schemas/null.ts @@ -0,0 +1,13 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(nullSchema: t.null): validate { + validateNull.tag = URI.null + function validateNull(u: unknown, path = Array.of()) { + return nullSchema(u) || [NullaryErrors.null(u, path)] + } + return validateNull +} diff --git a/packages/derive-validators/src/schemas/number.ts b/packages/derive-validators/src/schemas/number.ts new file mode 100644 index 00000000..416f639b --- /dev/null +++ b/packages/derive-validators/src/schemas/number.ts @@ -0,0 +1,13 @@ +import type { ValidationError, ValidationFn } from '@traversable/derive-validators' +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(numberSchema: S): validate { + validateNumber.tag = URI.number + function validateNumber(u: unknown, path: (keyof any)[] = []): true | ValidationError[] { + return numberSchema(u) || [NullaryErrors.number(u, path)] + } + return validateNumber +} diff --git a/packages/derive-validators/src/schemas/object.ts b/packages/derive-validators/src/schemas/object.ts new file mode 100644 index 00000000..eb06d116 --- /dev/null +++ b/packages/derive-validators/src/schemas/object.ts @@ -0,0 +1,110 @@ +import { + Array_isArray, + has, + Object_keys, + Object_hasOwn, + typeName, + URI, +} from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { getConfig } from '@traversable/schema-core' +import type { ValidationError, Validator, ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors, Errors, UnaryErrors } from '@traversable/derive-validators' + +/** @internal */ +let isObject = (u: unknown): u is { [x: string]: unknown } => + !!u && typeof u === 'object' && !Array_isArray(u) + +/** @internal */ +let isKeyOf = (k: keyof any, u: T): k is keyof T => + !!u && (typeof u === 'function' || typeof u === 'object') && k in u + +/** @internal */ +let isOptional = has('tag', (tag) => tag === URI.optional) + + +export type validate = never | ValidationFn + +export function validate(objectSchema: t.object): validate +export function validate(objectSchema: t.object): validate +export function validate(objectSchema: t.object): validate<{ [x: string]: unknown }> { + validateObject.tag = URI.object + function validateObject(u: unknown, path_ = Array.of()) { + // if (objectSchema(u)) return true + if (!isObject(u)) return [Errors.object(u, path_)] + let errors = Array.of() + let { schema: { optionalTreatment } } = getConfig() + let keys = Object_keys(objectSchema.def) + if (optionalTreatment === 'exactOptional') { + for (let i = 0, len = keys.length; i < len; i++) { + let k = keys[i] + let path = [...path_, k] + if (Object_hasOwn(u, k) && u[k] === undefined) { + if (isOptional(objectSchema.def[k].validate)) { + let tag = typeName(objectSchema.def[k].validate) + if (isKeyOf(tag, NullaryErrors)) { + let args = [u[k], path, tag] as never as [unknown, (keyof any)[]] + errors.push(NullaryErrors[tag](...args)) + } + else if (isKeyOf(tag, UnaryErrors)) { + errors.push(UnaryErrors[tag as keyof typeof UnaryErrors].invalid(u[k], path)) + } + } + let results = objectSchema.def[k].validate(u[k], path) + if (results === true) continue + let tag = typeName(objectSchema.def[k].validate) + if (isKeyOf(tag, NullaryErrors)) { + errors.push(NullaryErrors[tag](u[k], path, tag)) + } + else if (isKeyOf(tag, UnaryErrors)) { + errors.push(UnaryErrors[tag].invalid(u[k], path)) + } + errors.push(...results) + } + else if (Object_hasOwn(u, k)) { + let results = objectSchema.def[k].validate(u[k], path) + if (results === true) continue + errors.push(...results) + continue + } else { + errors.push(UnaryErrors.object.missing(u, path)) + continue + } + } + } + else { + for (let i = 0, len = keys.length; i < len; i++) { + let k = keys[i] + let path = [...path_, k] + if (!Object_hasOwn(u, k)) { + if (!isOptional(objectSchema.def[k].validate)) { + errors.push(UnaryErrors.object.missing(u, path)) + continue + } + else { + if (!Object_hasOwn(u, k)) continue + if (isOptional(objectSchema.def[k].validate) && Object_hasOwn(u, k)) { + if (u[k] === undefined) continue + let results = objectSchema.def[k].validate(u[k], path) + if (results === true) continue + for (let j = 0; j < results.length; j++) { + let result = results[j] + errors.push(result) + continue + } + } + } + } + let results = objectSchema.def[k].validate(u[k], path) + if (results === true) continue + for (let l = 0; l < results.length; l++) { + let result = results[l] + errors.push(result) + } + } + } + return errors.length === 0 || errors + } + + return validateObject +} diff --git a/packages/derive-validators/src/schemas/of.ts b/packages/derive-validators/src/schemas/of.ts new file mode 100644 index 00000000..c6557c41 --- /dev/null +++ b/packages/derive-validators/src/schemas/of.ts @@ -0,0 +1,14 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(inlineSchema: t.of): validate { + validateInline.tag = URI.inline + function validateInline(u: unknown, path = Array.of()) { + return inlineSchema(u) || [NullaryErrors.inline(u, path)] + } + return validateInline +} + diff --git a/packages/derive-validators/src/schemas/optional.ts b/packages/derive-validators/src/schemas/optional.ts new file mode 100644 index 00000000..70cd1e9d --- /dev/null +++ b/packages/derive-validators/src/schemas/optional.ts @@ -0,0 +1,17 @@ +import { URI } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { Validate, Validator, ValidationFn } from '@traversable/derive-validators' + +export type validate = Validate + +export function validate(optionalSchema: t.optional): validate +export function validate(optionalSchema: t.optional): validate +export function validate({ def }: t.optional): ValidationFn { + validateOptional.tag = URI.optional + validateOptional.optional = 1 + function validateOptional(u: unknown, path = Array.of()) { + if (u === void 0) return true + return def.validate(u, path) + } + return validateOptional +} diff --git a/packages/derive-validators/src/schemas/record.ts b/packages/derive-validators/src/schemas/record.ts new file mode 100644 index 00000000..6d958004 --- /dev/null +++ b/packages/derive-validators/src/schemas/record.ts @@ -0,0 +1,24 @@ +import type { t } from '@traversable/schema-core' +import { Array_isArray, Object_keys, URI } from '@traversable/registry' +import type { ValidationError, ValidationFn, Validator } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = never | ValidationFn +export function validate(recordSchema: t.record): validate +export function validate(recordSchema: t.record): validate +export function validate({ def: { validate = () => true } }: t.record) { + validateRecord.tag = URI.record + function validateRecord(u: unknown, path = Array.of()) { + if (!u || typeof u !== 'object' || Array_isArray(u)) return [NullaryErrors.record(u, path)] + let errors = Array.of() + let keys = Object_keys(u) + for (let k of keys) { + let y = u[k] + let results = validate(y, [...path, k]) + if (results === true) continue + else errors.push(...results) + } + return errors.length === 0 || errors + } + return validateRecord +} diff --git a/packages/derive-validators/src/schemas/string.ts b/packages/derive-validators/src/schemas/string.ts new file mode 100644 index 00000000..650ce686 --- /dev/null +++ b/packages/derive-validators/src/schemas/string.ts @@ -0,0 +1,13 @@ +import type { ValidationError, ValidationFn } from '@traversable/derive-validators' +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(stringSchema: S): validate { + validateString.tag = URI.string + function validateString(u: unknown, path = Array.of()): true | ValidationError[] { + return stringSchema(u) || [NullaryErrors.number(u, path)] + } + return validateString +} diff --git a/packages/derive-validators/src/schemas/symbol.ts b/packages/derive-validators/src/schemas/symbol.ts new file mode 100644 index 00000000..302e11f2 --- /dev/null +++ b/packages/derive-validators/src/schemas/symbol.ts @@ -0,0 +1,13 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(symbolSchema: t.symbol): validate { + validateSymbol.tag = URI.symbol + function validateSymbol(u: unknown, path = Array.of()) { + return symbolSchema(true as const) || [NullaryErrors.symbol(u, path)] + } + return validateSymbol +} diff --git a/packages/derive-validators/src/schemas/tuple.ts b/packages/derive-validators/src/schemas/tuple.ts new file mode 100644 index 00000000..5b065453 --- /dev/null +++ b/packages/derive-validators/src/schemas/tuple.ts @@ -0,0 +1,35 @@ +import { Array_isArray, has, URI } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { ValidationError, Validate, Validator } from '@traversable/derive-validators' +import { Errors } from '@traversable/derive-validators' + +export type validate = Validate +export function validate(tupleSchema: t.tuple<[...S]>): validate +export function validate(tupleSchema: t.tuple<[...S]>): validate +export function validate(tupleSchema: t.tuple<[...S]>): Validate { + validateTuple.tag = URI.tuple + let isOptional = has('tag', (tag) => tag === URI.optional) + function validateTuple(u: unknown, path = Array.of()) { + let errors = Array.of() + if (!Array_isArray(u)) return [Errors.array(u, path)] + for (let i = 0; i < tupleSchema.def.length; i++) { + if (!(i in u) && !(isOptional(tupleSchema.def[i].validate))) { + errors.push(Errors.missingIndex(u, [...path, i])) + continue + } + let results = tupleSchema.def[i].validate(u[i], [...path, i]) + if (results !== true) { + for (let j = 0; j < results.length; j++) errors.push(results[j]) + results.push(Errors.arrayElement(u[i], [...path, i])) + } + } + if (u.length > tupleSchema.def.length) { + for (let k = tupleSchema.def.length; k < u.length; k++) { + let excess = u[k] + errors.push(Errors.excessItems(excess, [...path, k])) + } + } + return errors.length === 0 || errors + } + return validateTuple +} diff --git a/packages/derive-validators/src/schemas/undefined.ts b/packages/derive-validators/src/schemas/undefined.ts new file mode 100644 index 00000000..d69c9d9e --- /dev/null +++ b/packages/derive-validators/src/schemas/undefined.ts @@ -0,0 +1,13 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(undefinedSchema: t.undefined): validate { + validateUndefined.tag = URI.undefined + function validateUndefined(u: unknown, path = Array.of()) { + return undefinedSchema(u) || [NullaryErrors.undefined(u, path)] + } + return validateUndefined +} diff --git a/packages/derive-validators/src/schemas/union.ts b/packages/derive-validators/src/schemas/union.ts new file mode 100644 index 00000000..95b1f0f0 --- /dev/null +++ b/packages/derive-validators/src/schemas/union.ts @@ -0,0 +1,26 @@ +import { URI } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { ValidationError, Validate, Validator } from '@traversable/derive-validators' + +export type validate = Validate + +export function validate(unionSchema: t.union): validate +export function validate(unionSchema: t.union): validate +export function validate({ def }: t.union) { + validateUnion.tag = URI.union + function validateUnion(u: unknown, path = Array.of()): true | ValidationError[] { + // if (this.def.every((x) => t.optional.is(x.validate))) validateUnion.optional = 1; + let errors = Array.of() + for (let i = 0; i < def.length; i++) { + let results = def[i].validate(u, path) + if (results === true) { + // validateUnion.optional = 0 + return true + } + for (let j = 0; j < results.length; j++) errors.push(results[j]) + } + // validateUnion.optional = 0 + return errors.length === 0 || errors + } + return validateUnion +} diff --git a/packages/derive-validators/src/schemas/unknown.ts b/packages/derive-validators/src/schemas/unknown.ts new file mode 100644 index 00000000..9c4298c0 --- /dev/null +++ b/packages/derive-validators/src/schemas/unknown.ts @@ -0,0 +1,10 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(_?: t.unknown): validate { + validateUnknown.tag = URI.unknown + function validateUnknown() { return true as const } + return validateUnknown +} diff --git a/packages/derive-validators/src/schemas/void.ts b/packages/derive-validators/src/schemas/void.ts new file mode 100644 index 00000000..a67fc4e4 --- /dev/null +++ b/packages/derive-validators/src/schemas/void.ts @@ -0,0 +1,13 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(voidSchema: t.void): validate { + validateVoid.tag = URI.void + function validateVoid(u: unknown, path = Array.of()) { + return voidSchema(u) || [NullaryErrors.void(u, path)] + } + return validateVoid +} diff --git a/packages/derive-validators/src/shared.ts b/packages/derive-validators/src/shared.ts index 57e8e6b7..71960540 100644 --- a/packages/derive-validators/src/shared.ts +++ b/packages/derive-validators/src/shared.ts @@ -1,23 +1,26 @@ -import { symbol } from '@traversable/registry' +import type { Unknown } from '@traversable/registry' +import { has, symbol } from '@traversable/registry' -import type { t, SchemaOptions } from '@traversable/schema' +import type { t, SchemaOptions } from '@traversable/schema-core' import type { ValidationError } from './errors.js' export interface Options extends SchemaOptions { path: (keyof any)[] } -export type Validate = never | { (u: T | {} | null | undefined): true | ValidationError[] } +export type Validate = never | { (u: T | Unknown): true | ValidationError[] } +export type ValidationFn = never | { (u: T | Unknown, path?: (keyof any)[]): true | ValidationError[] } +export interface Validator { validate: ValidationFn } -export type ValidationFn = never | { - (u: unknown, path?: t.Functor.Index): true | ValidationError[]; - tag: t.Tag - def?: unknown - ctx: (keyof any)[] -} +export type ValidationFn_ = never | { (u: S['_type' & keyof S] | Unknown, path?: (keyof any)[]): true | ValidationError[] } -export interface Validator { validate: ValidationFn } +export let hasOptionalSymbol = (u: unknown): u is t.optional => + !!u && typeof u === 'function' && symbol.optional in u && typeof u[symbol.optional] === 'number' +export let hasValidate + : (u: unknown) => u is Validator + = has('validate', (u) => typeof u === 'function') as never -export const isOptional = (u: unknown): u is t.optional => - !!u && typeof u === 'function' && symbol.optional in u && typeof u[symbol.optional] === 'number' \ No newline at end of file +export let callValidate + : (schema: { validate?: unknown }, u: unknown, path?: (keyof any)[]) => true | ValidationError[] + = (schema, u, path = Array.of()) => hasValidate(schema) ? schema.validate(u, path) : true diff --git a/packages/derive-validators/src/prototype.ts b/packages/derive-validators/src/validate.ts similarity index 62% rename from packages/derive-validators/src/prototype.ts rename to packages/derive-validators/src/validate.ts index 3b2aaf38..42aae406 100644 --- a/packages/derive-validators/src/prototype.ts +++ b/packages/derive-validators/src/validate.ts @@ -1,10 +1,21 @@ -import { Equal, omitMethods, Primitive, typeName, URI } from '@traversable/registry' -import { t, getConfig } from '@traversable/schema' +import type { + Primitive, + Unknown +} from '@traversable/registry' +import { + Array_isArray, + Equal, + Object_hasOwn, + Object_keys, + Object_values, + typeName, + URI, +} from '@traversable/registry' +import { t, getConfig } from '@traversable/schema-core' import type { ValidationError } from './errors.js' import { NULLARY, UNARY, ERROR } from './errors.js' import type { Validator } from './shared.js' -// import { isOptional } from './shared.js' export { validateNever as never, @@ -30,41 +41,30 @@ export { validateObject as object, } -/** @internal */ -let Array_isArray = globalThis.Array.isArray - -/** @internal */ -let Object_keys = globalThis.Object.keys - -/** @internal */ -let hasOwn = (u: unknown, k: K): u is Record => - !!u && typeof u === 'object' && globalThis.Object.prototype.hasOwnProperty.call(u, k) - /** @internal */ let isObject = (u: unknown): u is { [x: string]: unknown } => !!u && typeof u === 'object' && !Array_isArray(u) /** @internal */ -let isKeyOf = (k: keyof any, u: T): k is keyof T => !!u && (typeof u === 'function' || typeof u === 'object') && k in u +let isKeyOf = (k: keyof any, u: T): k is keyof T => + !!u && (typeof u === 'function' || typeof u === 'object') && k in u -const isOptional = t.has('tag', t.eq(URI.optional)) +function validateAny(this: t.any, _u: unknown, _path?: (keyof any)[]) { return true } +function validateUnknown(this: t.unknown, _u: unknown, _path?: (keyof any)[]) { return true } +function validateNever(this: t.never, u: unknown, path = Array.of()) { return [NULLARY.never(u, path)] } +function validateVoid(this: t.void, u: unknown, path = Array.of()) { return this(u) || [NULLARY.void(u, path)] } +function validateNull(this: t.null, u: unknown, path = Array.of()) { return this(u) || [NULLARY.null(u, path)] } +function validateUndefined(this: t.undefined, u: unknown, path = Array.of()) { return this(u) || [NULLARY.undefined(u, path)] } +function validateSymbol(this: t.symbol, u: unknown, path = Array.of()) { return this(u) || [NULLARY.symbol(u, path)] } +function validateBoolean(this: t.boolean, u: unknown, path = Array.of()) { return this(u) || [NULLARY.boolean(u, path)] } +function validateBigInt(this: t.bigint, u: unknown, path = Array.of()) { return this(u) || [NULLARY.bigint(u, path)] } +function validateInteger(this: t.integer, u: unknown, path = Array.of()) { return this(u) || [NULLARY.integer(u, path)] } +function validateNumber(this: t.number, u: unknown, path = Array.of()) { return this(u) || [NULLARY.number(u, path)] } +function validateString(this: t.string, u: unknown, path = Array.of()) { return this(u) || [NULLARY.string(u, path)] } -function validateNever(this: t.never, u: unknown, path: (keyof any)[] = []) { return this(u) || [NULLARY.never(u, path)] } -function validateUnknown(this: t.unknown, u: unknown, path: (keyof any)[] = []) { return true } -function validateAny(this: t.any, u: unknown, path: (keyof any)[] = []) { return true } -function validateVoid(this: t.void, u: unknown, path: (keyof any)[] = []) { return this(u) || [NULLARY.void(u, path)] } -function validateNull(this: t.null, u: unknown, path: (keyof any)[] = []) { return this(u) || [NULLARY.null(u, path)] } -function validateUndefined(this: t.undefined, u: unknown, path: (keyof any)[] = []) { return this(u) || [NULLARY.undefined(u, path)] } -function validateSymbol(this: t.symbol, u: unknown, path: (keyof any)[] = []) { return this(u) || [NULLARY.symbol(u, path)] } -function validateBoolean(this: t.boolean, u: unknown, path: (keyof any)[] = []) { return this(u) || [NULLARY.boolean(u, path)] } -function validateBigInt(this: t.bigint, u: unknown, path: (keyof any)[] = []) { return this(u) || [NULLARY.bigint(u, path)] } -function validateInteger(this: t.integer, u: unknown, path: (keyof any)[] = []) { return this(u) || [NULLARY.integer(u, path)] } -function validateNumber(this: t.number, u: unknown, path: (keyof any)[] = []) { return this(u) || [NULLARY.number(u, path)] } -function validateString(this: t.string, u: unknown, path: (keyof any)[] = []) { return this(u) || [NULLARY.string(u, path)] } - -validateNever.tag = URI.never validateAny.tag = URI.any validateUnknown.tag = URI.unknown +validateNever.tag = URI.never validateVoid.tag = URI.void validateNull.tag = URI.null validateUndefined.tag = URI.undefined @@ -74,31 +74,52 @@ validateBigInt.tag = URI.bigint validateInteger.tag = URI.integer validateNumber.tag = URI.number validateString.tag = URI.string - validateEnum.tag = URI.enum -function validateEnum(this: t.enum>, u: unknown, path: (keyof any)[] = []) { - let values = Object.values(this.def) +validateEq.tag = URI.eq +validateArray.tag = URI.array +validateRecord.tag = URI.record +validateUnion.tag = URI.union +validateIntersect.tag = URI.intersect +validateTuple.tag = URI.tuple +validateObject.tag = URI.object +validateOptional.tag = URI.optional +validateOptional.optional = 1 + +function validateEnum( + this: t.enum>, + u: unknown, + path: (keyof any)[] = [], +): true | ValidationError[] { + let values = Object_values(this.def) return values.includes(u as never) || [ERROR.enum(u, path, values.join(', '))] } -validateOptional.optional = 1 -validateOptional.tag = URI.optional -function validateOptional(this: t.optional, u: unknown, path: (keyof any)[] = []) { +function validateOptional( + this: t.optional>, + u: T | Unknown, + path = Array.of(), +): true | ValidationError[] { if (u === void 0) return true return this.def.validate(u, path) } -validateEq.tag = URI.eq -function validateEq(this: t.eq, u: unknown, path: (keyof any)[] = []) { +function validateEq( + this: t.eq, + u: V | Unknown, + path = Array.of(), +): true | ValidationError[] { let options = getConfig().schema let equals = options?.eq?.equalsFn || Equal.lax if (equals(this.def, u)) return true else return [ERROR.eq(u, path, this.def)] } -validateArray.tag = URI.array -function validateArray(this: t.array, u: unknown, path: (keyof any)[] = []) { - if (!Array.isArray(u)) return [NULLARY.array(u, path)] +function validateArray( + this: t.array>, + u: T | Unknown, + path = Array.of(), +): true | ValidationError[] { + if (!Array_isArray(u)) return [NULLARY.array(u, path)] let errors = Array.of() if (t.integer(this.minLength) && u.length < this.minLength) errors.push(ERROR.arrayMinLength(u, path, this.minLength)) if (t.integer(this.maxLength) && u.length > this.maxLength) errors.push(ERROR.arrayMaxLength(u, path, this.maxLength)) @@ -111,8 +132,11 @@ function validateArray(this: t.array, u: unknown, path: return errors.length === 0 || errors } -validateRecord.tag = URI.record -function validateRecord(this: t.record, u: unknown, path: (keyof any)[] = []): true | ValidationError[] { +function validateRecord( + this: t.record>, + u: T | Unknown, + path = Array.of(), +): true | ValidationError[] { if (!isObject(u)) return [NULLARY.record(u, path)] let errors = Array.of() let keys = Object_keys(u) @@ -125,10 +149,13 @@ function validateRecord(this: t.record, u: unknown, path return errors.length === 0 || errors } -// validateUnion.optional = 0 -validateUnion.tag = URI.union -function validateUnion(this: t.union, u: unknown, path: (keyof any)[] = []) { - // if (this.def.every((x) => isOptional(x.validate))) validateUnion.optional = 1; +function validateUnion( + this: t.union<{ [I in keyof T]: Validator }>, + u: T[number] | Unknown, + path = Array.of(), +): true | ValidationError[] { + // validateUnion.optional = 0 + // if (this.def.every((x) => t.optional.is(x.validate))) validateUnion.optional = 1; let errors = Array.of() for (let i = 0; i < this.def.length; i++) { let results = this.def[i].validate(u, path) @@ -142,8 +169,11 @@ function validateUnion(this: t.union, u: unknown, path: (keyof any) return errors.length === 0 || errors } -validateIntersect.tag = URI.intersect -function validateIntersect(this: t.intersect, u: unknown, path: (keyof any)[] = []) { +function validateIntersect( + this: t.intersect, + u: unknown, + path = Array.of(), +): true | ValidationError[] { let errors = Array.of() for (let i = 0; i < this.def.length; i++) { let results = this.def[i].validate(u, path) @@ -153,12 +183,16 @@ function validateIntersect(this: t.intersect, u: unknown, return errors.length === 0 || errors } -validateTuple.tag = URI.tuple -function validateTuple(this: t.tuple, u: unknown, path: (keyof any)[] = []): true | ValidationError[] { + +function validateTuple( + this: t.tuple, + u: unknown, + path = Array.of(), +): true | ValidationError[] { let errors = Array.of() if (!Array_isArray(u)) return [ERROR.array(u, path)] for (let i = 0; i < this.def.length; i++) { - if (!(i in u) && !(isOptional(this.def[i].validate))) { + if (!(i in u) && !(t.optional.is(this.def[i].validate))) { errors.push(ERROR.missingIndex(u, [...path, i])) continue } @@ -177,8 +211,11 @@ function validateTuple(this: t.tuple, u: unknown, path: (k return errors.length === 0 || errors } -validateObject.tag = URI.object -function validateObject(this: t.object<{ [x: string]: Validator }>, u: unknown, path: (keyof any)[] = []): true | ValidationError[] { +function validateObject( + this: t.object<{ [x: string]: Validator }>, + u: unknown, + path = Array.of(), +): true | ValidationError[] { if (!isObject(u)) return [ERROR.object(u, path)] let errors = Array.of() let { schema: { optionalTreatment } } = getConfig() @@ -187,8 +224,8 @@ function validateObject(this: t.object<{ [x: string]: Validator }>, u: unknown, for (let i = 0, len = keys.length; i < len; i++) { let k = keys[i] let path_ = [...path, k] - if (hasOwn(u, k) && u[k] === undefined) { - if (isOptional(this.def[k].validate)) { + if (Object_hasOwn(u, k) && u[k] === undefined) { + if (t.optional.is(this.def[k].validate)) { let tag = typeName(this.def[k].validate) if (isKeyOf(tag, NULLARY)) { let args = [u[k], path_, tag] as never as [unknown, (keyof any)[]] @@ -209,7 +246,7 @@ function validateObject(this: t.object<{ [x: string]: Validator }>, u: unknown, } errors.push(...results) } - else if (hasOwn(u, k)) { + else if (Object_hasOwn(u, k)) { let results = this.def[k].validate(u[k], path_) if (results === true) continue errors.push(...results) @@ -221,18 +258,17 @@ function validateObject(this: t.object<{ [x: string]: Validator }>, u: unknown, } } else { - // else if (optionalTreatment === 'presentButUndefinedIsOK') { for (let i = 0, len = keys.length; i < len; i++) { let k = keys[i] let path_ = [...path, k] - if (!hasOwn(u, k)) { - if (!isOptional(this.def[k].validate)) { + if (!Object_hasOwn(u, k)) { + if (!t.optional.is(this.def[k].validate)) { errors.push(UNARY.object.missing(u, path_)) continue } else { - if (!hasOwn(u, k)) continue - if (isOptional(this.def[k].validate) && hasOwn(u, k)) { + if (!Object_hasOwn(u, k)) continue + if (t.optional.is(this.def[k].validate) && Object_hasOwn(u, k)) { if (u[k] === undefined) continue let results = this.def[k].validate(u[k], path_) if (results === true) continue diff --git a/packages/derive-validators/test/bind.test.ts b/packages/derive-validators/test/bind.test.ts index a6c6b9ed..40ed2263 100644 --- a/packages/derive-validators/test/bind.test.ts +++ b/packages/derive-validators/test/bind.test.ts @@ -1,5 +1,5 @@ import * as vi from 'vitest' -import { t, configure } from '@traversable/schema' +import { t, configure } from '@traversable/schema-core' import '@traversable/derive-validators/install' vi.describe('〖⛳️〗‹‹‹ ❲@traversable/validation❳', () => { diff --git a/packages/derive-validators/test/install.test.ts b/packages/derive-validators/test/install.test.ts index 1d1b607c..aba12084 100644 --- a/packages/derive-validators/test/install.test.ts +++ b/packages/derive-validators/test/install.test.ts @@ -1,5 +1,5 @@ import * as vi from 'vitest' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' vi.describe('〖⛳️〗‹‹‹ ❲@traversable/derive-validators❳', () => { vi.it('〖⛳️〗› ❲pre-install❳', () => vi.assert.isFalse(t.has('validate')(t.string))) diff --git a/packages/derive-validators/test/validators.test.ts b/packages/derive-validators/test/validators.test.ts index 1da9b7a5..5ad54046 100644 --- a/packages/derive-validators/test/validators.test.ts +++ b/packages/derive-validators/test/validators.test.ts @@ -3,7 +3,7 @@ import { fc, test } from '@fast-check/vitest' import { Seed } from '@traversable/schema-seed' import { symbol } from '@traversable/registry' -import { t, configure } from '@traversable/schema' +import { t, configure } from '@traversable/schema-core' import { dataPathFromSchemaPath as dataPath, fromSchema } from '@traversable/derive-validators' import '@traversable/derive-validators/install' diff --git a/packages/derive-validators/tsconfig.build.json b/packages/derive-validators/tsconfig.build.json index e36720e1..7af09c34 100644 --- a/packages/derive-validators/tsconfig.build.json +++ b/packages/derive-validators/tsconfig.build.json @@ -10,6 +10,6 @@ "references": [ { "path": "../json" }, { "path": "../registry" }, - { "path": "../schema" } + { "path": "../schema-core" } ] } diff --git a/packages/derive-validators/tsconfig.src.json b/packages/derive-validators/tsconfig.src.json index 076cde65..a021e790 100644 --- a/packages/derive-validators/tsconfig.src.json +++ b/packages/derive-validators/tsconfig.src.json @@ -9,7 +9,7 @@ "references": [ { "path": "../json" }, { "path": "../registry" }, - { "path": "../schema" } + { "path": "../schema-core" } ], "include": ["src"] } diff --git a/packages/derive-validators/tsconfig.test.json b/packages/derive-validators/tsconfig.test.json index 3cfc5619..e91761f8 100644 --- a/packages/derive-validators/tsconfig.test.json +++ b/packages/derive-validators/tsconfig.test.json @@ -10,7 +10,7 @@ { "path": "tsconfig.src.json" }, { "path": "../json" }, { "path": "../registry" }, - { "path": "../schema" }, + { "path": "../schema-core" }, { "path": "../schema-seed" }, ], "include": ["test"] diff --git a/packages/json/src/exports.ts b/packages/json/src/exports.ts index 3c58e482..7b041c78 100644 --- a/packages/json/src/exports.ts +++ b/packages/json/src/exports.ts @@ -5,6 +5,7 @@ export { VERSION } from './version.js' export { Cache } from './cache.js' export * as Json from './json.js' +export type { Free, Unary } from './json.js' /** * ## {@link Json `Json`} diff --git a/packages/registry/src/bindUserExtensions.ts b/packages/registry/src/bindUserExtensions.ts new file mode 100644 index 00000000..cd1e5d2a --- /dev/null +++ b/packages/registry/src/bindUserExtensions.ts @@ -0,0 +1,10 @@ +export function bindUserExtensions(schema: T, userDefinitions: Record): T & { _type: any } +export function bindUserExtensions(schema: T, userDefinitions: Record) { + for (let k in userDefinitions) { + userDefinitions[k] = + typeof userDefinitions[k] === 'function' + ? userDefinitions[k](schema) + : userDefinitions[k] + } + return userDefinitions +} diff --git a/packages/schema/src/bounded.ts b/packages/registry/src/bounded.ts similarity index 61% rename from packages/schema/src/bounded.ts rename to packages/registry/src/bounded.ts index 33fd1828..d9f247b9 100644 --- a/packages/schema/src/bounded.ts +++ b/packages/registry/src/bounded.ts @@ -1,17 +1,7 @@ -import { fn } from '@traversable/registry' +import { isNullable, isNumeric } from './predicate.js' +import { assertIsNotCalled } from './function.js' -export interface Bounds { - gte?: T - lte?: T - gt?: T - lt?: T -} - -/** @internal */ -const isNumeric = (u: unknown) => typeof u === 'number' || typeof u === 'bigint' - -/** @internal */ -const isNullable = (u: unknown) => u == null +export interface Bounds { gte?: T, lte?: T, gt?: T, lt?: T } export function within({ gte = Number.MIN_SAFE_INTEGER, lte = Number.MAX_SAFE_INTEGER, gt, lt }: Bounds) { return (x: number): boolean => { @@ -19,7 +9,7 @@ export function within({ gte = Number.MIN_SAFE_INTEGER, lte = Number.MAX_SAFE_IN case isNullable(gt) && isNullable(lt): return gte <= x && x <= lte case isNumeric(gt): return isNumeric(lt) ? gt < x && x < lt : gt < x && x <= lte case isNumeric(lt): return gte <= x && x < lt - default: return fn.assertIsNotCalled(lt, gt) + default: return assertIsNotCalled(lt, gt) } } } @@ -32,7 +22,17 @@ export function withinBig({ gte = void 0, lte = void 0, gt, lt }: Bounds(x: T, ...ignoreKeys: (keyof T)[]) { + let keys = Object.keys(x).filter((k) => !ignoreKeys.includes(k as never) && x[k as keyof typeof x] != null) + if (keys.length === 0) return {} + else { + let out: { [x: string]: unknown } = {} + for (let k of keys) out[k] = x[k as keyof typeof x] + return out + } +} diff --git a/packages/registry/src/equals.ts b/packages/registry/src/equals.ts index 7a668c79..77cbf231 100644 --- a/packages/registry/src/equals.ts +++ b/packages/registry/src/equals.ts @@ -39,7 +39,7 @@ export const IsStrictlyEqual = (l: T, r: T): boolean => l === r /** * ## {@link SameValueNumber `Equal.SameValueNumber`} * - * Specified by TC39's + * TC39-compliant implementation of * [`Number::sameValue`](https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-sameValue) */ export const SameValueNumber = ( @@ -53,7 +53,7 @@ export const SameValueNumber = ( /** * ## {@link SameValue `Equal.SameValue`} * - * Specified by TC39's + * TC39-compliant implementation of * [`SameValue`](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-samevalue) */ export const SameValue diff --git a/packages/registry/src/exports.ts b/packages/registry/src/exports.ts index e3c700d8..187567aa 100644 --- a/packages/registry/src/exports.ts +++ b/packages/registry/src/exports.ts @@ -1,10 +1,3 @@ -export type * from './satisfies.js' -export type * from './types.js' -export { Match } from './types.js' - -export * as fn from './function.js' -export * as Print from './print.js' - import * as symbol_ from './symbol.js' type symbol_ = typeof symbol_[keyof typeof symbol_] export { symbol_ as symbol } @@ -12,26 +5,40 @@ export { symbol_ as symbol } import * as URI_ from './uri.js' type URI_ = typeof URI_[keyof typeof URI_] export { URI_ as URI } - export { NS, SCOPE } from './uri.js' -export { VERSION } from './version.js' -export type { TypeName } from './typeName.js' -export { typeName } from './typeName.js' -export { has } from './has.js' -export { parseArgs } from './parseArgs.js' -export { - escape, - isQuoted, - isValidIdentifier, - parseKey, -} from './parse.js' + export * as Equal from './equals.js' export type Equal = import('./types.js').Equal +export type * from './satisfies.js' +export type * from './types.js' + +export * from './globalThis.js' +export * as fn from './function.js' +export * as Print from './print.js' +export * from './predicate.js' + +export { VERSION } from './version.js' + export type { GlobalOptions, OptionalTreatment, SchemaOptions } from './options.js' export type { GlobalConfig, SchemaConfig } from './config.js' export { applyOptions, configure, defaults, eqDefaults, getConfig } from './config.js' - +export type { TypeName } from './typeName.js' +export { typeName } from './typeName.js' +export type { Bounds } from './bounded.js' +export { carryover, within, withinBig } from './bounded.js' +export { join } from './join.js' +export { has } from './has.js' +export { parseArgs } from './parseArgs.js' +export { ESC_CHAR, PATTERN, escape, isQuoted, isValidIdentifier, parseKey } from './parse.js' +export { IsStrictlyEqual, SameType, SameValue, SameValueNumber, deep as deepEquals, lax as laxEquals } from './equals.js' +export { unsafeCompact } from './compact.js' +export { bindUserExtensions } from './bindUserExtensions.js' +export { safeCoerce } from './safeCoerce.js' +export { map } from './mapObject.js' +export { objectFromKeys, omit, omit_, omitWhere, omitMethods, pick, pick_, pickWhere } from './pick.js' +export { merge, mut } from './merge.js' +export { ValueSet } from './set.js' export { /** @internal */ fromPath as __fromPath, @@ -40,17 +47,3 @@ export { /** @internal */ parsePath as __parsePath, } from './has.js' - -export { unsafeCompact } from './compact.js' - -export { - omit, - omit_, - omitWhere, - omitMethods, - pick, - pick_, - pickWhere, -} from './pick.js' - -export { merge, mut } from './merge.js' diff --git a/packages/registry/src/function.ts b/packages/registry/src/function.ts index 8051d2f5..926ba536 100644 --- a/packages/registry/src/function.ts +++ b/packages/registry/src/function.ts @@ -199,3 +199,303 @@ export function flow( case args.length === 3: return function (this: unknown) { return args[2](args[1](args[0].apply(this, arguments))) } } } + +type fn = globalThis.Function +type _ = unknown + +export function pipe(): void +export function pipe(a: a): a +export function pipe(a: a, ab: (a: a) => b): b +export function pipe(a: a, ab: (a: a) => b, bc: (b: b) => c): c +export function pipe(a: a, ab: (a: a) => b, bc: (b: b) => c, cd: (c: c) => d): d +export function pipe(a: a, ab: (a: a) => b, bc: (b: b) => c, cd: (c: c) => d, de: (d: d) => e,): e +export function pipe(a: a, ab: (a: a) => b, bc: (b: b) => c, cd: (c: c) => d, de: (d: d) => e, ef: (e: e) => f): f +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, +): g +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, +): h +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, + hi: (h: h) => i, +): i +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, + hi: (h: h) => i, + ij: (i: i) => j, +): j +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, + hi: (h: h) => i, + ij: (i: i) => j, + jk: (j: j) => k, +): k +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, + hi: (h: h) => i, + ij: (i: i) => j, + jk: (j: j) => k, + kl: (k: k) => l, +): l +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, + hi: (h: h) => i, + ij: (i: i) => j, + jk: (j: j) => k, + kl: (k: k) => l, + lm: (l: l) => m, +): m +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, + hi: (h: h) => i, + ij: (i: i) => j, + jk: (j: j) => k, + kl: (k: k) => l, + lm: (l: l) => m, + mn: (m: m) => n, +): n +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, + hi: (h: h) => i, + ij: (i: i) => j, + jk: (j: j) => k, + kl: (k: k) => l, + lm: (l: l) => m, + mn: (m: m) => n, + no: (n: n) => o, +): o +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, + hi: (h: h) => i, + ij: (i: i) => j, + jk: (j: j) => k, + kl: (k: k) => l, + lm: (l: l) => m, + mn: (m: m) => n, + no: (n: n) => o, + op: (o: o) => p, +): p +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, + hi: (h: h) => i, + ij: (i: i) => j, + jk: (j: j) => k, + kl: (k: k) => l, + lm: (l: l) => m, + mn: (m: m) => n, + no: (n: n) => o, + op: (o: o) => p, + pq: (p: p) => q, +): q +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, + hi: (h: h) => i, + ij: (i: i) => j, + jk: (j: j) => k, + kl: (k: k) => l, + lm: (l: l) => m, + mn: (m: m) => n, + no: (n: n) => o, + op: (o: o) => p, + pq: (p: p) => q, + qr: (q: q) => r, +): r +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, + hi: (h: h) => i, + ij: (i: i) => j, + jk: (j: j) => k, + kl: (k: k) => l, + lm: (l: l) => m, + mn: (m: m) => n, + no: (n: n) => o, + op: (o: o) => p, + pq: (p: p) => q, + qr: (q: q) => r, + rs: (r: r) => s, +): s +export function pipe( + a: a, + ab: (a: a) => b, + bc: (b: b) => c, + cd: (c: c) => d, + de: (d: d) => e, + ef: (e: e) => f, + fg: (f: f) => g, + gh: (g: g) => h, + hi: (h: h) => i, + ij: (i: i) => j, + jk: (j: j) => k, + kl: (k: k) => l, + lm: (l: l) => m, + mn: (m: m) => n, + no: (n: n) => o, + op: (o: o) => p, + pq: (p: p) => q, + qr: (q: q) => r, + rs: (r: r) => s, + st: (s: s) => t, +): t +export function pipe( + ...a: + | [_] + | [_, fn] + | [_, fn, fn] + | [_, fn, fn, fn] + | [_, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn] + | [_, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn, fn] +): unknown { + switch (true) { + case a.length === 1: + return a[0] + case a.length === 2: + return a[1](a[0]) + case a.length === 3: + return a[2](a[1](a[0])) + case a.length === 4: + return a[3](a[2](a[1](a[0]))) + case a.length === 5: + return a[4](a[3](a[2](a[1](a[0])))) + case a.length === 6: + return a[5](a[4](a[3](a[2](a[1](a[0]))))) + case a.length === 7: + return a[6](a[5](a[4](a[3](a[2](a[1](a[0])))))) + case a.length === 8: + return a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0]))))))) + case a.length === 9: + return a[8](a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0])))))))) + case a.length === 10: + return a[9](a[8](a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0]))))))))) + case a.length === 11: + return a[10](a[9](a[8](a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0])))))))))) + case a.length === 12: + return a[11](a[10](a[9](a[8](a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0]))))))))))) + case a.length === 13: + return a[12](a[11](a[10](a[9](a[8](a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0])))))))))))) + case a.length === 14: + return a[13](a[12](a[11](a[10](a[9](a[8](a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0]))))))))))))) + case a.length === 15: + return a[14](a[13](a[12](a[11](a[10](a[9](a[8](a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0])))))))))))))) + case a.length === 16: + return a[15](a[14](a[13](a[12](a[11](a[10](a[9](a[8](a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0]))))))))))))))) + case a.length === 17: + return a[16](a[15](a[14](a[13](a[12](a[11](a[10](a[9](a[8](a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0])))))))))))))))) + case a.length === 18: + return a[17](a[16](a[15](a[14](a[13](a[12](a[11](a[10](a[9](a[8](a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0]))))))))))))))))) + case a.length === 19: + return a[18](a[17](a[16](a[15](a[14](a[13](a[12](a[11](a[10](a[9](a[8](a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0])))))))))))))))))) + case a.length === 20: + return a[19](a[18](a[17](a[16](a[15](a[14](a[13](a[12](a[11](a[10](a[9](a[8](a[7](a[6](a[5](a[4](a[3](a[2](a[1](a[0]))))))))))))))))))) + default: { + const args: fn[] = a + let ret: unknown = args[0] + for (let ix = 1, len = args.length; ix < len; ix++) ret = args[ix](ret) + return ret + } + } +} diff --git a/packages/registry/src/globalThis.ts b/packages/registry/src/globalThis.ts new file mode 100644 index 00000000..1c124700 --- /dev/null +++ b/packages/registry/src/globalThis.ts @@ -0,0 +1,82 @@ +import type { + EmptyObject, + FiniteArray, + FiniteObject, + NonFiniteArray, + NonFiniteRecord, + StringIndexed, + NumberIndexed, + PlainObject, + SymbolIndexed, + ObjectAsTypeAlias, + MixedNonFinite, + Entry, +} from './satisfies.js' +import { Force, newtype } from './types.js' + +export const Array_isArray + : (u: unknown) => u is T[] + = globalThis.Array.isArray + +export const Math_max = globalThis.Math.max +export const Math_min = globalThis.Math.min + +export const Number_isInteger + : (x: unknown) => x is number + = globalThis.Number.isInteger + +export const Number_isSafeInteger + : (x: unknown) => x is number + = globalThis.Number.isSafeInteger + +export const Object_assign = globalThis.Object.assign +export const Object_defineProperty = globalThis.Object.defineProperty +export const Object_is = globalThis.Object.is +export const Object_values = globalThis.Object.values + +export const Object_hasOwn + : (u: unknown, k: K) => u is Record + = (u, k): u is never => !!u + && (typeof u === 'object' || typeof u === 'function') + && globalThis.Object.prototype.hasOwnProperty.call(u, k) + +export const Object_keys + : (x: T) => (K)[] + = globalThis.Object.keys + +export const Object_getOwnPropertySymbols: { + (x: T): (K)[] + (x: {}): symbol[] + (x: T): (K)[] +} = globalThis.Object.getOwnPropertySymbols + +export type Object_fromEntries = never | Force< + & { [E in Entry.Optional as E[0]]+?: E[1] } + & { [E in Entry.Required as E[0]]-?: E[1] } +> + +export const Object_fromEntries: { + >(entries: T[]): Object_fromEntries + >(entries: readonly T[]): Object_fromEntries +} = globalThis.Object.fromEntries + +export type Object_entries = never | (K extends K ? [k: K, v: T[K & keyof T]] : never)[] +export const Object_entries: { + >(x: T): MixedNonFiniteEntries + >(x: T): [k: string, v?: unknown][] + >(x: T): [k: string, v: unknown][] + , K extends Extract>(x: T): Object_entries + , K extends keyof T>(x: T): Object_entries + >(x: T): [k: keyof T, v: T[keyof T]][] + >(x: T): [k: keyof T, v: T[keyof T]][] + , K extends Extract>(x: T): [k: `${number}`, v: T[number]][] + >(x: T): [k: string | number, v: T[keyof T]][] + >(x: T): [k: symbol, v: T[keyof T]][] + >(x: T): [k: string, v: T[keyof T]][] +} = globalThis.Object.entries as never + +export type MixedNonFiniteEntries< + T, + O = [T] extends [T[number & keyof T][] & infer U] ? U : never, + A = [T] extends [O & infer U] ? U : never +> = never | ([k: keyof O, v: O[keyof O]] | [k: `${number}`, v: A[number & keyof A]])[] diff --git a/packages/registry/src/has.ts b/packages/registry/src/has.ts index 14a52eb3..d7cd5f8c 100644 --- a/packages/registry/src/has.ts +++ b/packages/registry/src/has.ts @@ -1,3 +1,4 @@ +import type { Unknown } from './types.js' import * as symbol from './symbol.js' /** @internal */ @@ -46,7 +47,7 @@ export function parsePath(xs: readonly (keyof any)[] | readonly [...(keyof any)[ : [xs.slice(0, -1), xs[xs.length - 1]] } -export type has = has.loop +export type has = has.loop export declare namespace has { export type loop @@ -56,9 +57,9 @@ export declare namespace has { } /** - * ## {@link has `tree.has`} + * ## {@link has `has`} * - * The {@link has `tree.has`} utility accepts a path + * The {@link has `has`} utility accepts a path * into a tree and an optional type-guard, and returns * a predicate that returns true if its argument * 'has' the specified path. diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index 704f5088..410a4bcb 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -1,2 +1 @@ export * from './exports.js' -export * as registry from './exports.js' diff --git a/packages/registry/src/join.ts b/packages/registry/src/join.ts new file mode 100644 index 00000000..4cd9d94d --- /dev/null +++ b/packages/registry/src/join.ts @@ -0,0 +1,6 @@ +import type { Join } from '@traversable/registry/types' + +export function join(left: readonly [...L], right: readonly [...R]): [...L, ...R] +export function join(left: readonly [...L], right: readonly [...R]) { + return [...left, ...right] +} diff --git a/packages/registry/src/mapObject.ts b/packages/registry/src/mapObject.ts new file mode 100644 index 00000000..d33ea6e9 --- /dev/null +++ b/packages/registry/src/mapObject.ts @@ -0,0 +1,19 @@ + +/** @internal */ +let Array_isArray = globalThis.Array.isArray + +/** @internal */ +let Object_keys = globalThis.Object.keys + +export function map(src: S, mapfn: (x: S[keyof S], k: string, xs: S) => T): { [x: string]: T } +export function map(src: Record, mapfn: (x: S, k: string, xs: Record) => T) { + if (Array_isArray(src)) return src.map(mapfn as never) + const keys = Object_keys(src) + let out: { [x: string]: T } = {} + for (let ix = 0, len = keys.length; ix < len; ix++) { + const k = keys[ix] + out[k] = mapfn(src[k], k, src) + } + return out +} + diff --git a/packages/registry/src/merge.ts b/packages/registry/src/merge.ts index 19095b21..f309957b 100644 --- a/packages/registry/src/merge.ts +++ b/packages/registry/src/merge.ts @@ -68,7 +68,9 @@ export function merge, R extends FiniteObject>(l export function merge, R extends NonFiniteObject>(l: L, r: R): Force export function merge, R extends NonFiniteArray>(l: L, r: R): Force export function merge, R extends NonFiniteObject>(l: L, r: R): Force> + export function merge, R extends NonFiniteArray>(l: L, r: R): Force> + export function merge, R extends NonFiniteObject>(l: L, r: R): Force export function merge(l: {}, r: {}): unknown { if (Array.isArray(l) && Array.isArray(r)) return [...l, ...r] @@ -77,8 +79,3 @@ export function merge(l: {}, r: {}): unknown { return Object.assign(l_, r) } } - - -// type FiniteArrayToFiniteObject = never | { [I in keyof T as I extends `${number}` ? I : never]: T[I] } -// type NonFiniteArrayToFiniteObject = never | { [x: number]: T[number] } -// type MixedToObject = [T] extends [Record & infer U] ? U : [fail: Record] \ No newline at end of file diff --git a/packages/registry/src/parse.ts b/packages/registry/src/parse.ts index d599500a..464f6094 100644 --- a/packages/registry/src/parse.ts +++ b/packages/registry/src/parse.ts @@ -1,14 +1,14 @@ -const PATTERN = { +export let PATTERN = { singleQuoted: /(?<=^').+?(?='$)/, doubleQuoted: /(?<=^").+?(?="$)/, graveQuoted: /(?<=^`).+?(?=`$)/, identifier: /^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u, } as const satisfies Record -export const isQuoted +export let isQuoted : (text: string | number) => boolean = (text) => { - const string = `${text}` + let string = `${text}` return ( PATTERN.singleQuoted.test(string) || PATTERN.doubleQuoted.test(string) || @@ -16,13 +16,12 @@ export const isQuoted ) } -export const isValidIdentifier = (name: keyof any): boolean => +export let isValidIdentifier = (name: keyof any): boolean => typeof name === "symbol" ? true : isQuoted(name) || PATTERN.identifier.test(`${name}`) - -const ESC_CHAR = [ - /** 0- 9 */ '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', '\\u0005', '\\u0006', '\\u0007', '\\b', '\\t', +export let ESC_CHAR = [ + /** 00-09 */ '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', '\\u0005', '\\u0006', '\\u0007', '\\b', '\\t', /** 10-19 */ '\\n', '\\u000b', '\\f', '\\r', '\\u000e', '\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013', /** 20-29 */ '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', '\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d', /** 30-39 */ '\\u001e', '\\u001f', '', '', '\\"', '', '', '', '', '', @@ -67,7 +66,7 @@ export function escape(x: string): string { let pt: number for (let ix = 0, len = x.length; ix < len; ix++) { void (pt = x.charCodeAt(ix)) - if (pt === 34 || pt === 92 || pt < 32) { + if (pt === 34 /* " */ || pt === 92 /* \\ */ || pt < 32) { void (out += x.slice(prev, ix) + ESC_CHAR[pt]) void (prev = ix + 1) } else if (0xdfff <= pt && pt <= 0xdfff) { @@ -79,7 +78,7 @@ export function escape(x: string): string { } } void (out += x.slice(prev, ix) + "\\u" + pt.toString(16)) - void (prev = ix + 1) + void (prev = ix + 2) } } void (out += x.slice(prev)) @@ -87,10 +86,31 @@ export function escape(x: string): string { } export declare namespace parseKey { - type Options = Partial<{ parseAsJson: boolean }> + type Options = Partial<{ + parseAsJson: boolean + /** + * ## {@link defaultOptions.quotationMarks `Options.quotationMarks`} + * + * The character to use as a quotation mark if the interpolated key + * is not a valid identifier. + * + * @example + * import { parseKey } from '@traversable/registry' + * + * parseKey('a') + * // => 'a' + * + * parseKey(100) + * // => "100" + * + * parseKey(100, { quotationMarks: '`' }) + * // => `100` + */ + quotationMarks: '`' | '"' | `'` + }> } -/** +/** * ## {@link parseKey `parseKey`} */ export function parseKey( @@ -101,19 +121,24 @@ export function parseKey( export function parseKey( k: keyof any, { parseAsJson = parseKey.defaults.parseAsJson, + quotationMarks: quote = parseKey.defaults.quotationMarks, }: parseKey.Options = parseKey.defaults, _str = globalThis.String(k) ) { return ( typeof k === "symbol" ? _str : isQuoted(k) ? escape(_str) - : parseAsJson ? `"` + escape(_str) + `"` + : parseAsJson ? quote + escape(_str) + quote : isValidIdentifier(k) ? escape(_str) - : `"` + escape(_str) + `"` + : quote + escape(_str) + quote ) } -parseKey.defaults = { +let defaultOptions = { + /** Type defined in {@link parseKey.Options} */ parseAsJson: false, + /** Type defined in {@link parseKey.Options} */ + quotationMarks: '"', } satisfies Required +parseKey.defaults = defaultOptions diff --git a/packages/registry/src/parseArgs.ts b/packages/registry/src/parseArgs.ts index f9eeae8c..509041fe 100644 --- a/packages/registry/src/parseArgs.ts +++ b/packages/registry/src/parseArgs.ts @@ -1,6 +1,6 @@ /** * TODO: Currently this is hardcoded to avoid creating a dependency on - * `@traversable/schema`. Figure out a better way to handle this. + * `@traversable/schema-core`. Figure out a better way to handle this. */ import type { Equal } from "./types.js" diff --git a/packages/registry/src/pick.ts b/packages/registry/src/pick.ts index 19968f8b..78e0a4b7 100644 --- a/packages/registry/src/pick.ts +++ b/packages/registry/src/pick.ts @@ -12,16 +12,26 @@ export type IndexOf< I extends keyof T = keyof T > = [T] extends [readonly any[]] ? Exclude : I + +export function objectFromKeys(...keys: [...T[]]): { [K in T]: K } +export function objectFromKeys(...keys: [...T[]]) { + let out: { [x: keyof any]: keyof any } = {} + for (let k of keys) out[k] = k + return out +} + export type pick = never | { [P in K]: T[P] } export declare namespace pick { type Lax = never | { [P in K as P extends keyof T ? P : never]: T[P & keyof T] } type Where = never | { [K in keyof T as T[K] extends S | undefined ? K : never]: T[K] } } +export type Omit = keyof T extends K ? T : { [P in keyof T as P extends K ? never : P]: T[P] } export type omit = never | { [P in keyof T as P extends K ? never : P]: T[P] } export declare namespace omit { type Lax = never | { [P in keyof T as P extends K ? never : P]: T[P] } type Where = never | { [K in keyof T as T[K] extends S ? never : K]: T[K] } + type When = never | { [K in keyof T as T[K] extends S | undefined ? never : K]: T[K] } type List = never | { [I in keyof T as I extends keyof [] | K | Key ? never : I]: T[I] } type Any = [T] extends [readonly unknown[]] ? omit.List : omit type NonFiniteObject = [string] extends [K] ? T : omit.Lax> @@ -29,6 +39,7 @@ export declare namespace omit { } export function pick(x: T, ks: K[]): pick +export function pick(x: T, ks: K[]): pick.Lax export function pick(x: { [x: keyof any]: unknown }, ks: (keyof any)[]) { if (!x || typeof x !== 'object') return x let allKeys = Object.keys(x) @@ -54,7 +65,7 @@ export function pick(x: { [x: keyof any]: unknown }, ks: (keyof any)[]) { } } -export function omit(x: T, ks: K[]): omit +export function omit(x: T, ks: K[]): Omit export function omit(x: { [x: keyof any]: unknown }, ks: (keyof any)[]) { if (!x || typeof x !== 'object') return x if (ks.length === 0) return x diff --git a/packages/registry/src/predicate.ts b/packages/registry/src/predicate.ts new file mode 100644 index 00000000..15c346dd --- /dev/null +++ b/packages/registry/src/predicate.ts @@ -0,0 +1,220 @@ +import type { Intersect, Unknown } from './types.js' +import type { SchemaOptions } from './options.js' +import * as URI from './uri.js' +import * as symbol from './symbol.js' +import { + Array_isArray, + Number_isInteger, + Object_hasOwn, + Object_keys, + Object_values, +} from './globalThis.js' + +export function isNonNullable(x: {} | Unknown) { return x != null } + +/** @internal */ +function coerce(f: (x: S) => T): ((x: S) => T) +function coerce(f: (x: never) => unknown): {} { + return f === globalThis.Boolean ? isNonNullable : f +} + +/** @internal */ +let apply + : (x: S) => (f: (x: S) => T) => T + = (x) => ((f: Function) => { + if ((f) === globalThis.Boolean) return (x != null) + else return f(x) + }) + +/** @internal */ +let bind + : (f: (x: S) => T) => ((x: S) => T) + = (f) => { + let g = coerce(f) + return (x) => g(x) + } + +/** @internal */ +export let _isPredicate + : (x: unknown) => x is { (): boolean; (x: S): x is T } + = ((x: any) => typeof x === 'function') as never + +/** Nullary */ +export function isNever(_: unknown): _ is never { return false } +export function isAny(_: unknown): _ is any { return true } +export function isUnknown(_: unknown): _ is unknown { return true } +export function isVoid(x: void | Unknown): x is void { return x === void 0 } +export function isNull(x: null | Unknown) { return x === null } +export function isUndefined(x: undefined | Unknown) { return x === undefined } +export function isSymbol(x: symbol | Unknown) { return typeof x === 'symbol' } +export function isBoolean(x: boolean | Unknown) { return typeof x === 'boolean' } +export function isInteger(x: number | Unknown) { return Number_isInteger(x) } +export function isNumber(x: number | Unknown) { return typeof x === 'number' } +export function isBigInt(x: bigint | Unknown) { return typeof x === 'bigint' } +export function isString(x: string | Unknown) { return typeof x === 'string' } +export function isAnyArray(x: unknown[] | Unknown) { return Array_isArray(x) } +export function isAnyObject(x: { [x: string]: unknown } | Unknown): x is { [x: string]: unknown } { + return !!x && typeof x === 'object' && !Array_isArray(x) +} + +export function isFunction(x: Function | unknown) { return typeof x === 'function' } +export function isComposite(x: { [x: string]: T } | T[] | Unknown): x is { [x: string]: T } | T[] { return !!x && typeof x === 'object' } + +/** Unary+ */ +export let array + : (fn: (x: unknown) => x is T) => ((x: T[] | Unknown) => x is T[]) + = (fn) => function isArrayOf(x): x is never { return Array_isArray(x) && x.every(bind(fn)) } + +export let record + : (fn: (x: unknown) => x is T) => ((x: Record | Unknown) => x is Record) + = (fn) => function isRecordOf(x): x is never { return isAnyObject(x) && Object_values(x).every(bind(fn)) } + +export let union + : (fns: { [I in keyof T]: (x: unknown) => x is T[I] }) => ((x: T[number] | Unknown) => x is T[number]) + = (fns) => function isAnyOf(x): x is never { return fns.some(apply(x)) } + +export let intersect + : (fns: { [I in keyof T]: (x: unknown) => x is T[I] }) => ((x: Intersect | Unknown) => x is Intersect) + = (fns) => function isAllOf(x): x is never { return fns.every(apply(x)) } + +export let optional + : (fn: (x: unknown) => x is T) => (x: undefined | T | Unknown) => x is undefined | T + = (fn) => function isOptionally(u): u is never { return u === void 0 || coerce(fn)(u) } + +/** Composites */ +export function isNumeric(x: number | bigint | Unknown) { return typeof x === 'number' || typeof x === 'bigint' } +export function isNullable(x: null | undefined | Unknown) { return x == null } + + +type Target = S extends { (_: any): _ is infer T } ? T : S extends { (x: infer T): boolean } ? T : never +type Object$ = (x: unknown) => x is { [K in keyof T]: Target } + +function isOptionalSchema(x: unknown): x is ((x: unknown) => x is unknown) & { [symbol.tag]: URI.optional, def: (x: unknown) => x is unknown } { + return !!x && (typeof x === 'object' || typeof x === 'function') && 'tag' in x && x.tag === URI.optional && 'def' in x && typeof x.def === 'function' +} +function isRequiredSchema(x: unknown): x is (_: unknown) => _ is T { + return !!x && !isOptionalSchema(x) +} +function isOptionalNotUndefinedSchema(x: unknown): x is {} { + return !!x && isOptionalSchema(x) && x.def(undefined) === false +} +function isUndefinedSchema(x: unknown): x is { tag: URI.undefined } { + return !!x && typeof x === 'function' && 'tag' in x && x.tag === URI.undefined +} + +function hasOwn(x: unknown, key: K): x is { [P in K]: unknown } +function hasOwn(x: unknown, key: keyof any): x is { [x: string]: unknown } { + return typeof x === 'function' ? Object_hasOwn(x, key) + : typeof key === "symbol" + ? isComposite(x) && key in x + : Object_hasOwn(x, key) +} + +export function exactOptional( + fns: Record boolean>, + xs: Record +): boolean { + for (const k in fns) { + const fn = coerce(fns[k]) + switch (true) { + // case q === (globalThis.Boolean as never): { if (Object_hasOwn(x, k)) return x[k] != null; else return false } + case isUndefinedSchema(fn) && !Object_hasOwn(xs, k): return false + case isOptionalNotUndefinedSchema(fn) && Object_hasOwn(xs, k) && xs[k] === undefined: return false + case isOptionalSchema(fn) && !Object_hasOwn(xs, k): continue + case isRequiredSchema(fn) && !Object_hasOwn(xs, k): return false + case !fn(xs[k]): return false + default: continue + } + } + return true +} + +export function presentButUndefinedIsOK( + fns: Record boolean>, + x: Record +): boolean { + for (const k in fns) { + const fn = coerce(fns[k]) + switch (true) { + // case fn === (globalThis.Boolean as never): { if (hasOwn(x, k)) return x[k] != null; else return false } + case isOptionalSchema(fn) && !hasOwn(x, k): continue + case isOptionalSchema(fn) && hasOwn(x, k) && x[k] === undefined: continue + case isOptionalSchema(fn) && hasOwn(x, k) && fn(x[k]): continue + case isOptionalSchema(fn) && hasOwn(x, k) && !fn(x[k]): return false + case isRequiredSchema(fn) && !hasOwn(x, k): return false + case isRequiredSchema(fn) && hasOwn(x, k) && fn(x[k]) === true: continue + default: return false + } + } + return true +} + +export function treatUndefinedAndOptionalAsTheSame( + fns: Record boolean>, + x: Record +) { + const ks = Object_keys(fns) + for (const k of ks) { + const fn = coerce(fns[k]) + const y = x[k] + if (!fn(y)) return false + } + return true +} + +export function object( + fns: { [K in keyof T]: (x: unknown) => x is T[K] }, + config?: Required, +): (x: T | Unknown) => x is T { + return function isFiniteObject(x: unknown): x is never { + switch (true) { + case !x: return false + case !isAnyObject(x): return false + case !config?.treatArraysAsObjects && Array_isArray(x): return false + case config?.optionalTreatment === 'exactOptional': return exactOptional(fns, x) + case config?.optionalTreatment === 'presentButUndefinedIsOK': return presentButUndefinedIsOK(fns, x) + case config?.optionalTreatment === 'treatUndefinedAndOptionalAsTheSame': return treatUndefinedAndOptionalAsTheSame(fns, x) + default: throw globalThis.Error( + + '(["@traversable/schema-core/predicates/object$"] \ + \ + Expected "optionalTreatment" to be one of: \ + \ + - "exactOptional" \ + - "presentButUndefinedIsOK" \ + - "treatUndefinedAndOptionalAsTheSame" \ + \ + Got: ' + globalThis.JSON.stringify(config?.optionalTreatment) + ) + } + } +} + +export type TupleOptions = { minLength?: number; } & SchemaOptions + +export let tuple + : < + T extends readonly unknown[], + Opts extends TupleOptions + >( + fns: { [I in keyof T]: (u: unknown) => u is T[I] }, + options: Opts + ) => ((u: unknown) => u is T) + + = (fns, options) => { + const checkLength = (xs: readonly unknown[]) => + options?.minLength === void 0 + ? (xs.length === fns.length) + : options.minLength === -1 + ? (xs.length === fns.length) + : (xs.length >= options.minLength && fns.length >= xs.length) + + function isFiniteArray(u: unknown): u is never { + return Array_isArray(u) + && checkLength(u) + && fns.every((fn, ix) => coerce(fn)(u[ix])) + } + + return isFiniteArray + } + diff --git a/packages/registry/src/safeCoerce.ts b/packages/registry/src/safeCoerce.ts new file mode 100644 index 00000000..c6520894 --- /dev/null +++ b/packages/registry/src/safeCoerce.ts @@ -0,0 +1,10 @@ +/** @internal */ +let isBooleanConstructor = (u: unknown): u is globalThis.BooleanConstructor => u === globalThis.Boolean + +/** @internal */ +let isNonNullable = (u: unknown) => u != null + +export function safeCoerce(fn: T): T +export function safeCoerce(fn: T): T { + return isBooleanConstructor(fn) ? isNonNullable as never : fn +} diff --git a/packages/registry/src/satisfies.ts b/packages/registry/src/satisfies.ts index e035cc31..65af329a 100644 --- a/packages/registry/src/satisfies.ts +++ b/packages/registry/src/satisfies.ts @@ -41,6 +41,18 @@ export type Mut : [T] extends [infer U extends Atom] ? U : { -readonly [ix in keyof T]: Mut } +export type Entry = Mut> = [M] extends [Entry.Any] ? M : never +export declare namespace Entry { + type Any = { 0: keyof any, 1?: unknown } + type Required = T extends { 0: keyof any, 1: unknown } ? T : never + type Optional = T extends { 0: keyof any, 1: unknown } ? never : T +} + +export type Entries = Mut> = [M] extends [Entries.Any] ? M : never +export declare namespace Entries { + type Any = readonly Entry.Any[] +} + export type Mutable = never | { -readonly [K in keyof T]: T[K] } export type NonUnion< @@ -50,20 +62,39 @@ export type NonUnion< = ([T] extends [infer _] ? _ : never) > = _ extends _ ? [T] extends [_] ? _ : never : never +export type OnlyUnion< + T, + _ extends + | ([T] extends [infer _] ? _ : never) + = ([T] extends [infer _] ? _ : never) +> = _ extends _ ? [T] extends [_] ? never : _ : never + export type NonFiniteArray - = [T] extends [readonly any[]] + = [T] extends [readonly unknown[]] ? number extends T['length'] ? readonly unknown[] : never : never export type NonFiniteObject - = string extends keyof T ? Record - : number extends keyof T ? Record - : never + = string extends keyof T ? { [x: string]: unknown } + : number extends keyof T ? { [x: number]: unknown } : never + +export type NonFiniteRecord = number extends keyof T ? never : symbol extends keyof T ? never : string extends keyof T ? { [x: string]: unknown } : never + +export type MixedNonFinite = [T] extends [readonly unknown[]] ? [T] extends [Record] ? {} : never : never +export type FiniteArrayNonFiniteObject = [FiniteArray] extends [never] ? never : [NonFiniteObject] extends [never] ? never : {}; + +/** @internal */ +interface EmptyStringInterface { [x: string]: unknown } export type FiniteArray = [T] extends [readonly any[]] ? number extends T['length'] ? never : Mut : never export type FiniteObject = [T] extends [Record] ? string extends keyof T ? never : number extends keyof T ? never : Mut : never - +export type PlainObject = [keyof T] extends [never] ? string extends T ? never : object : never +export type EmptyObject = [keyof T] extends [never] ? string extends T ? {} : never : never +export type SymbolIndexed = string extends keyof T ? never : number extends keyof T ? never : symbol extends keyof T ? { [x: symbol]: unknown } : never +export type ObjectAsTypeAlias = string extends keyof T ? EmptyStringInterface : never +export type StringIndexed = number extends keyof T ? never : symbol extends keyof T ? never : string extends keyof T ? { [x: string]: unknown } : never +export type NumberIndexed = string extends keyof T ? never : symbol extends keyof T ? never : number extends keyof T ? { [x: number]: unknown } : never export type FiniteIndex = string extends keyof T ? never : Record export type FiniteIndices = [T] extends [readonly any[]] ? number extends T['length'] ? never : readonly unknown[] : never diff --git a/packages/registry/src/set.ts b/packages/registry/src/set.ts new file mode 100644 index 00000000..81c9a056 --- /dev/null +++ b/packages/registry/src/set.ts @@ -0,0 +1,188 @@ +import type { Equal } from './types.js' + +export interface ValueSet { + [Symbol.iterator](): ValueSet.Iterator + /** + * ### {@link add `ValueSet.add`} + * + * Appends a new element with a specified value to the end of the {@link ValueSet `ValueSet`} + * if the value is not already a member. + * + * Set membership is determined by {@link equalsFn `ValueSet.equalsFn`} + */ + add(value: T): ValueSet + /** + * ### {@link clear `ValueSet.clear`} + * + * Clears all the values from the {@link ValueSet `ValueSet`}. + */ + clear(): void + /** + * ### {@link delete `ValueSet.delete`} + * + * Removes a specified value from the {@link ValueSet `ValueSet`}. + * + * Returns true if an element in the {@link ValueSet `ValueSet`} existed and has been removed, + * or false if the element does not exist. + */ + delete(value: T): boolean + /** + * ### {@link entries `ValueSet.entries`} + * + * Returns an iterable of `[v, v]` pairs for every value `v` + * in the {@link ValueSet `ValueSet`}. + */ + entries(): ValueSet.Iterator<[T, T]> + /** + * ### {@link equalsFn `ValueSet.equalsFn`} + * + * Retrieves the {@link Equal `equals function`} that was provided when + * the {@link ValueSet `ValueSet`} was constructed. + * + * This is the function that {@link ValueSet `ValueSet`} uses to determine + * whether a value is a member of the set it contains. + */ + equalsFn: Equal + /** + * ### {@link forEach `ValueSet.forEach`} + * + * Executes a provided function once per each value in the {@link ValueSet `ValueSet`}, + * in insertion order. + */ + forEach(callback: (value: T, set: ValueSet) => void, thisArg?: any): void + /** + * ### {@link has `ValueSet.has`} + * + * Returns boolean indicating whether an element with the specified value + * exists in the {@link ValueSet `ValueSet`} or not, as determined by + * {@link equalsFn `ValueSet.equalsFn`}. + */ + has(value: T): boolean + /** + * ### {@link keys `ValueSet.keys`} + * + * Despite its name, returns an iterable of the values in the {@link ValueSet `ValueSet`}. + */ + keys(): ValueSet.Iterator + /** + * ### {@link values `ValueSet.values`} + * + * Returns an iterable of values in the {@link ValueSet `ValueSet`}. + */ + values(): ValueSet.Iterator +} + +export declare namespace ValueSet { + interface Constructor { + new: (equalsFn: Equal) => ValueSet + } + interface Iterator extends globalThis.SetIterator { } +} + +export class ValueSet implements ValueSet { + [Symbol.iterator] = this.values; + private _data = Array.of(); + private constructor(public equalsFn: Equal) { } + static new + : (equalsFn: Equal) => ValueSet + = (equalsFn) => new ValueSet(equalsFn); + add(value: T): this { + let data = this._data + if (data.find((v) => v === value || this.equalsFn(v, value))) { + return this + } else { + data.push(value) + return this + } + } + clear(): void { + return void (this._data = Array.of()) + } + delete(value: T): boolean { + let data = this._data + let ix = data.findIndex((v) => v === value || this.equalsFn(v, value)) + if (ix === -1) return false + else { + data.splice(ix, 1) + return true + } + } + + entries(): ValueSet.Iterator<[T, T]> { + let data = this._data + let size = this.size + return { + *[Symbol.iterator](): globalThis.Generator<[T, T], undefined, never> { + let ix = 0 + while (ix < size) { + let d = data[ix++] + yield [d, d] satisfies [any, any] + } + }, + next() { + let ix = 0 + if (ix < size) { + let d = data[ix++] + return { value: [d, d] satisfies [any, any], done: false } + } + else + return { value: void 0, done: true } + } + } + } + + forEach(callback: (v: T, self: this) => void): void { + return [...this.values()].forEach((v) => callback(v, this)) + } + + has(value: T): boolean { + return this._data.find((v) => this.equalsFn(v, value)) !== undefined + } + + keys(): ValueSet.Iterator { + let data = this._data + let size = this.size + return { + *[Symbol.iterator](): globalThis.Generator { + let ix = 0 + while (ix < size) { + yield data[ix++] + } + }, + next() { + let ix = 0 + if (ix < size) { + return { value: data[ix++], done: false } + } + else + return { value: void 0, done: true } + } + } + } + + values(): ValueSet.Iterator { + let data = this._data + let size = this.size + return { + *[Symbol.iterator](): globalThis.Generator { + let ix = 0 + while (ix < size) { + yield data[ix++] + } + }, + next() { + let ix = 0 + if (ix < size) { + return { value: data[ix++], done: false } + } + else + return { value: void 0, done: true } + } + } + } + /** + * ### {@link size `ValueSet.size`} + * Returns the number of (unique) elements in the {@link ValueSet `ValueSet`} + */ + get size(): number { return this._data.length } +} diff --git a/packages/registry/src/symbol.ts b/packages/registry/src/symbol.ts index 99600f5c..5e137c13 100644 --- a/packages/registry/src/symbol.ts +++ b/packages/registry/src/symbol.ts @@ -20,6 +20,7 @@ export { symbol_number as number, symbol_object as object, symbol_optional as optional, + symbol_order as order, symbol_top as top, symbol_record as record, symbol_string as string, @@ -33,6 +34,8 @@ export { symbol_union as union, symbol_void as void, symbol_symbol as symbol, + symbol_any_array as any_array, + symbol_any_object as any_object, } import * as URI from './uri.js' @@ -63,6 +66,7 @@ const symbol_record = Symbol.for(URI.record) const symbol_string = Symbol.for(URI.string) const symbol_symbol = Symbol.for(URI.symbol) const symbol_tag = Symbol.for(URI.tag) +const symbol_order = Symbol.for(URI.order) const symbol_tuple = Symbol.for(URI.tuple) const symbol_type = Symbol.for(URI.type) const symbol_type_error = Symbol.for(URI.type_error) @@ -71,8 +75,12 @@ const symbol_unknown = Symbol.for(URI.unknown) const symbol_undefined = Symbol.for(URI.undefined) const symbol_union = Symbol.for(URI.union) const symbol_void = Symbol.for(URI.void) +const symbol_any_array = Symbol.for(URI.any_array) +const symbol_any_object = Symbol.for(URI.any_object) type symbol_any = typeof symbol_any +type symbol_any_array = typeof symbol_any_array +type symbol_any_object = typeof symbol_any_object type symbol_array = typeof symbol_array type symbol_bad_data = typeof symbol_bad_data type symbol_bigint = typeof symbol_bigint @@ -92,6 +100,7 @@ type symbol_null = typeof symbol_null type symbol_number = typeof symbol_number type symbol_object = typeof symbol_object type symbol_optional = typeof symbol_optional +type symbol_order = typeof symbol_order type symbol_top = typeof symbol_top type symbol_record = typeof symbol_record type symbol_string = typeof symbol_string diff --git a/packages/registry/src/typeName.ts b/packages/registry/src/typeName.ts index 51146d04..fc9a1782 100644 --- a/packages/registry/src/typeName.ts +++ b/packages/registry/src/typeName.ts @@ -2,4 +2,5 @@ import { NS } from './uri.js' export type TypeName = never | T extends `${NS}${infer S}` ? S : never export function typeName(x: T): TypeName +export function typeName(x: T): string export function typeName(x: { tag: string }) { return x.tag.substring(NS.length) } diff --git a/packages/registry/src/types.ts b/packages/registry/src/types.ts index 1eeb9e1f..b7a56a52 100644 --- a/packages/registry/src/types.ts +++ b/packages/registry/src/types.ts @@ -1,3 +1,5 @@ +import type { newtype } from '@traversable/registry/newtype' + export type * from './functor.js' export type * from './hkt.js' export type * from './newtype.js' @@ -6,42 +8,73 @@ export { Match } from './satisfies.js' // data types export type Primitive = null | undefined | symbol | boolean | bigint | number | string export type Showable = null | undefined | boolean | bigint | number | string -export type Entry = readonly [k: string, v: T] -export type Entries = readonly Entry[] +// export type Entry = readonly [k: string, v: T] +// export type Entries = readonly Entry[] export type Unknown = {} | null | undefined +/* @ts-expect-error */ +export type Key = `${T}` +export type Autocomplete = T | (string & {}) +export interface Etc { [x: string]: T[keyof T] } + +export interface Record extends newtype<{ [P in K]+?: V } & { [x in string]+?: V }> {} + +export interface Array extends newtype {} +export interface ReadonlyArray extends newtype {} +export type Integer> = [T] extends [number] + ? [Z] extends [`${number}.${string}`] ? never + : number : never + // transforms export type Force = never | { -readonly [K in keyof T]: T[K] } -export type Intersect = X extends readonly [infer H, ...infer T] ? Intersect : _ +export type Intersect = T extends readonly [infer Head, ...infer Tail] ? Intersect : Out +export type Require = [K] extends [never] + ? never | { [K in keyof T]-?: T[K] } + : never | ( + & { [P in keyof T as P extends K ? P : never]-?: T[P] } + & { [P in keyof T as P extends K ? never : P]+?: T[P] } + ) + +export type Partial = [K] extends [never] + ? never | { [K in keyof T]+?: T[K] } + : never | ( + & { [P in keyof T as P extends K ? P : never]-?: T[P] } + & { [P in keyof T as P extends K ? never : P]+?: T[P] } + ) export type PickIfDefined< T, K extends keyof any, - _ extends keyof T = K extends keyof T ? undefined extends T[K] ? never : K : never + _ extends + | K extends keyof T ? undefined extends T[K] ? never : K : never + = K extends keyof T ? undefined extends T[K] ? never : K : never > = never | { [K in _]: T[K] } // infererence export type Param = T extends (_: infer I) => unknown ? I : never export type Parameters = T extends (..._: infer I) => unknown ? I : never export type Returns = T extends (_: never) => infer O ? O : never +export type IfUnaryReturns = T extends () => infer O ? O : T +export type IfReturns = T extends (_: never) => infer O ? O : T export type Conform = Extract> = [_] extends [never] ? Extract : _ export type Target = S extends (_: any) => _ is infer T ? T : never export type UnionToIntersection< - T, - U = (T extends T ? (contra: T) => void : never) extends (contra: infer U) => void ? U : never, -> = U + U, + Out = (U extends U ? (contra: U) => void : never) extends (contra: infer T) => void ? T : never, +> = Out -export type UnionToTuple extends () => infer X ? X : never> = UnionToTuple.loop<[], U, _> +export type UnionToTuple extends () => infer _ ? _ : never> = UnionToTuple.Loop export declare namespace UnionToTuple { - type loop extends () => infer X ? X : never> = [ + type Loop< U, - ] extends [never] - ? Todo - : loop<[_, ...Todo], Exclude> + Todo extends readonly unknown[], + _ = Contra extends () => infer X ? X : never + > = [U] extends [never] ? Todo : Loop, [_, ...Todo]> } -type Thunk = (U extends U ? (_: () => U) => void : never) extends (_: infer _) => void ? _ : never +/** @internal */ +type Contra = (U extends U ? (_: () => U) => void : never) extends (_: infer T) => void ? T : never export type Join< T, diff --git a/packages/registry/src/uri.ts b/packages/registry/src/uri.ts index a4dee856..12147d99 100644 --- a/packages/registry/src/uri.ts +++ b/packages/registry/src/uri.ts @@ -23,6 +23,8 @@ export { URI_void as void, URI_enum as enum, // misc. + URI_any_array as any_array, + URI_any_object as any_object, URI_bad_data as bad_data, URI_bottom as bottom, URI_cache_hit as cache_hit, @@ -30,6 +32,7 @@ export { URI_has as has, URI_nonnullable as nonnullable, URI_notfound as notfound, + URI_order as order, URI_tag as tag, URI_top as top, URI_type as type, @@ -37,7 +40,7 @@ export { URI_typeclass as typeclass, } -export const SCOPE = '@traversable/schema/URI' +export const SCOPE = '@traversable/schema-core/URI' export const NS = `${SCOPE}::` export type NS = typeof NS @@ -89,7 +92,11 @@ const URI_void = `${NS}void` as const type URI_void = typeof URI_void // misc. -const URI_bad_data = `${NS}bad_data` +const URI_any_array = `${NS}any_array` as const +type URI_any_array = typeof URI_any_array +const URI_any_object = `${NS}any_object` as const +type URI_any_object = typeof URI_any_object +const URI_bad_data = `${NS}bad_data` as const type URI_bad_data = typeof URI_bad_data const URI_bottom = `${NS}bottom` as const type URI_bottom = typeof URI_bottom @@ -105,6 +112,8 @@ const URI_notfound = `${NS}notfound` as const type URI_notfound = typeof URI_notfound const URI_tag = `${NS}tag` as const type URI_tag = typeof URI_tag +const URI_order = `${NS}order` as const +type URI_order = typeof URI_order const URI_top = `${NS}top` as const type URI_top = typeof URI_top const URI_type = `${NS}type` as const diff --git a/packages/registry/test/bounded.test.ts b/packages/registry/test/bounded.test.ts new file mode 100644 index 00000000..7099fccf --- /dev/null +++ b/packages/registry/test/bounded.test.ts @@ -0,0 +1,60 @@ +import * as vi from 'vitest' +import { carryover, within, withinBig } from '@traversable/registry' + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core/bounded❳', () => { + vi.it('〖⛳️〗‹ ❲within❳', () => { + // SUCCESS + vi.assert.isTrue(within({ gt: 0 })(1)) + vi.assert.isTrue(within({ gt: 0, lt: 2 })(1)) + vi.assert.isTrue(within({ lt: 2 })(1)) + // FAILURE + vi.assert.isFalse(within({ gt: 0 })(0)) + vi.assert.isFalse(within({ gt: 0, lt: 0 })(0)) + vi.assert.isFalse(within({ lt: 0 })(0)) + /* @ts-expect-error */ + vi.assert.throws(() => within({ gt: '' })(0.5)) + }) + + vi.it('〖⛳️〗‹ ❲withinBig❳', () => { + // SUCCESS + vi.assert.isTrue(withinBig({})(1)) + vi.assert.isTrue(withinBig({ lte: 1 })(1)) + vi.assert.isTrue(withinBig({ gte: 1 })(1)) + vi.assert.isTrue(withinBig({ lt: 2 })(1)) + vi.assert.isTrue(withinBig({ gt: 0 })(1)) + vi.assert.isTrue(withinBig({ gt: 0, lt: 2 })(1)) + vi.assert.isTrue(withinBig({ gt: 0, lte: 1 })(1)) + vi.assert.isTrue(withinBig({ gte: 0, lt: 2 })(1)) + vi.assert.isTrue(withinBig({ gte: 0, lte: 1 })(1)) + vi.assert.isTrue(withinBig({ gt: 0, lt: 2, gte: 1 })(1)) + vi.assert.isTrue(withinBig({ gt: 0, lte: 1, gte: 1 })(1)) + vi.assert.isTrue(withinBig({ gte: 1, lt: 2, lte: 1 })(1)) + vi.assert.isTrue(withinBig({ gte: 1, lte: 1, lt: 2 })(1)) + vi.assert.isTrue(withinBig({ gte: 1, lte: 1, gt: 0 })(1)) + vi.assert.isTrue(withinBig({ gte: 1, lt: 2, gt: 0 })(1)) + vi.assert.isTrue(withinBig({ gte: 1, lte: 1, lt: 2, gt: 0 })(1)) + // FAILURE + vi.assert.isFalse(withinBig({ lte: 0 })(1)) + vi.assert.isFalse(withinBig({ gte: 1 })(-1)) + vi.assert.isFalse(withinBig({ lt: 0 })(1)) + vi.assert.isFalse(withinBig({ gt: 2 })(-1)) + vi.assert.isFalse(withinBig({ gt: 0, lt: 2 })(-1)) + vi.assert.isFalse(withinBig({ gt: 0, lte: 1 })(-1)) + vi.assert.isFalse(withinBig({ gte: 0, lt: 2 })(-1)) + vi.assert.isFalse(withinBig({ gte: 0, lte: 1 })(-1)) + vi.assert.isFalse(withinBig({ gt: 0, lt: 2, gte: 1 })(-1)) + vi.assert.isFalse(withinBig({ gt: 0, lte: 1, gte: 1 })(-1)) + vi.assert.isFalse(withinBig({ gte: 1, lt: 2, lte: 1 })(-1)) + vi.assert.isFalse(withinBig({ gte: 1, lte: 1, lt: 2 })(-1)) + vi.assert.isFalse(withinBig({ gte: 1, lte: 1, gt: 0 })(-1)) + vi.assert.isFalse(withinBig({ gte: 1, lt: 2, gt: 0 })(-1)) + vi.assert.isFalse(withinBig({ gte: 1, lte: 1, lt: 2, gt: 0 })(-1)) + /* @ts-expect-error */ + vi.assert.throws(() => withinBig({ gt: '' })(0.5)) + }) + + vi.it('〖⛳️〗› ❲~carryover❳', () => { + vi.assert.deepEqual(carryover({}), {}) + }) +}) + diff --git a/packages/registry/test/globalThis.test.ts b/packages/registry/test/globalThis.test.ts new file mode 100644 index 00000000..31bac57b --- /dev/null +++ b/packages/registry/test/globalThis.test.ts @@ -0,0 +1,26 @@ +import * as vi from 'vitest' + +import { Object_entries } from '@traversable/registry' + +let x: any = {} + +vi.describe('〖⛳️〗‹‹‹ ❲@traverable/registry❳: globalThis', () => { + vi.it('〖⛳️〗‹‹‹ ❲Object_entries❳: typelevel tests', () => { + let unknownEntry = [String(), Array.of()[0]] as const satisfies [any, any] + let optionalEntry: [string, _?: unknown] = unknownEntry + + vi.expectTypeOf(Object_entries(x as object)).toEqualTypeOf(Array.of(unknownEntry)) + vi.expectTypeOf(Object_entries({})).toEqualTypeOf(Array.of(optionalEntry)) + vi.expectTypeOf(Object_entries({ [String()]: 1 as const })).toEqualTypeOf(Array.of([String(), 1 as const] satisfies [any, any])) + vi.expectTypeOf(Object_entries({ a: 3, b: 4 })).toEqualTypeOf(Array.of<[k: "a", v: 3] | [k: "b", v: 4]>()) + vi.expectTypeOf(Object_entries([[1, 2], [3, 4]])).toEqualTypeOf(Array.of<[k: "0", v: [1, 2]] | [k: "1", v: [3, 4]]>()) + vi.expectTypeOf(Object_entries(Array.of(4 as const))).toEqualTypeOf(Array.of<[k: `${number}`, v: 4]>()) + vi.expectTypeOf(Object_entries({ [String()]: 5 as const })).toEqualTypeOf(Array.of<[k: string, v: 5]>()) + vi.expectTypeOf(Object_entries({ [Number()]: 6 as const })).toEqualTypeOf(Array.of<[k: string | number, v: 6]>()) + vi.expectTypeOf(Object_entries({ [Symbol()]: 7 })).toEqualTypeOf(Array.of<[k: symbol, v: 7]>()) + vi.expectTypeOf(Object_entries(Object.assign({} as Record, Array.of(Boolean())))) + .toEqualTypeOf(Array.of<[k: string, v: 9000] | [k: `${number}`, v: boolean]>()) + vi.expectTypeOf(Object_entries(Object.assign([1, 2, 3] as const, { a: 4, b: 5, c: 6 } as const))) + .toEqualTypeOf(Array.of<[k: "0", v: 1] | [k: "1", v: 2] | [k: "2", v: 3]>()) + }) +}) diff --git a/packages/registry/test/merge.test.ts b/packages/registry/test/merge.test.ts index 132baf82..786675d2 100644 --- a/packages/registry/test/merge.test.ts +++ b/packages/registry/test/merge.test.ts @@ -1,6 +1,6 @@ import * as vi from 'vitest' -import { merge, mut } from '@traversable/registry' +import { merge, mut, NonFiniteArray, NonFiniteObject, NonFiniteRecord, NumberIndexed } from '@traversable/registry' vi.describe('〖⛳️〗‹‹‹ ❲@traverable/registry❳: merge', () => { vi.it('〖⛳️〗‹‹‹ ❲merge❳: typelevel tests', () => { @@ -33,11 +33,5 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/registry❳: merge', () => { .toEqualTypeOf(mut({ [Number()]: Boolean() || null }, { [String()]: Number() || String() },)) vi.expectTypeOf(merge(Array(Boolean() || null), { [String() || Number()]: Number() || String() })) .toEqualTypeOf(mut({ [Number()]: Boolean() || null }, { [String()]: Number() || String() },)) - - // vi.expectTypeOf(merge({ [Symbol()]: Number() || String() }, Array(Boolean() || Symbol()))) - // .toEqualTypeOf(mut({ [Symbol()]: Number() || String() }, { [Number()]: Boolean() || Symbol() })) - // vi.expectTypeOf(merge(Array(Boolean() || null), { [Symbol()]: Number() || String() })) - // .toEqualTypeOf(mut({ [Number()]: Boolean() || null }, { [Symbol()]: Number() || String() },)) - }) }) diff --git a/packages/registry/test/satisfies.test.ts b/packages/registry/test/satisfies.test.ts index df6ba660..f82a0f00 100644 --- a/packages/registry/test/satisfies.test.ts +++ b/packages/registry/test/satisfies.test.ts @@ -1,5 +1,5 @@ import type { NonUnion } from '@traversable/registry' -import { Match } from '@traversable/registry' +import { Match } from '@traversable/registry/satisfies' import * as vi from 'vitest' vi.describe('〖⛳️〗‹‹‹ ❲@traversable/registry❳', () => { diff --git a/packages/schema-arbitrary/README.md b/packages/schema-arbitrary/README.md new file mode 100644 index 00000000..173c4050 --- /dev/null +++ b/packages/schema-arbitrary/README.md @@ -0,0 +1,60 @@ +
+

ᯓ𝘁𝗿𝗮𝘃𝗲𝗿𝘀𝗮𝗯𝗹𝗲/𝘀𝗰𝗵𝗲𝗺𝗮-𝗮𝗿𝗯𝗶𝘁𝗿𝗮𝗿𝘆

+
+ +

+ Derive a [fast-check](https://github.com/dubzzz/fast-check) arbitrary from a schema from `@traversable/schema-core`. +

+ +
+ NPM Version +   + TypeScript +   + Static Badge +   + npm +   +
+ +
+ npm bundle size (scoped) +   + Static Badge +   + Static Badge +   +
+ +
+ Demo (StackBlitz) +   •   + TypeScript Playground +   •   + npm +
+
+
+
+ +## Installation + +`@traversable/schema-arbitrary` has a peer depenency on [fast-check](https://github.com/dubzzz/fast-check). It has +been tested with version 3 and version 4. + +## Getting Started + +```typescript +import { t } from '@traversable/schema' +import { Arbitrary } from '@traversable/schema-arbitrary' +import * as fc from 'fast-check' + +let schema = t.object({ + a: t.optional(t.string), + b: t.integer.between(0, 100), +}) + +let arbitrary = Arbitrary.fromSchema(schema) + +let giveMe100Mocks = fc.sample(arbitrary, 100) +``` diff --git a/packages/schema-arbitrary/package.json b/packages/schema-arbitrary/package.json new file mode 100644 index 00000000..d1c0079b --- /dev/null +++ b/packages/schema-arbitrary/package.json @@ -0,0 +1,52 @@ +{ + "name": "@traversable/schema-arbitrary", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/schema-arbitrary" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { "include": ["**/*.ts"] }, + "generateIndex": { "include": ["**/*.ts"] } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "bench": "echo NOTHING TO BENCH", + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "test": "vitest" + }, + "peerDependencies": { + "fast-check": "3 - 4", + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^" + }, + "peerDependenciesMeta": { + "fast-check": { "optional": false }, + "@traversable/registry": { "optional": false }, + "@traversable/schema-core": { "optional": false } + }, + "devDependencies": { + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^" + } +} diff --git a/packages/schema-arbitrary/src/__generated__/__manifest__.ts b/packages/schema-arbitrary/src/__generated__/__manifest__.ts new file mode 100644 index 00000000..9d7b0d48 --- /dev/null +++ b/packages/schema-arbitrary/src/__generated__/__manifest__.ts @@ -0,0 +1,52 @@ +export default { + "name": "@traversable/schema-arbitrary", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/schema-arbitrary" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { "include": ["**/*.ts"] }, + "generateIndex": { "include": ["**/*.ts"] } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "bench": "echo NOTHING TO BENCH", + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "test": "vitest" + }, + "peerDependencies": { + "fast-check": "3 - 4", + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^" + }, + "peerDependenciesMeta": { + "fast-check": { "optional": false }, + "@traversable/registry": { "optional": false }, + "@traversable/schema-core": { "optional": false } + }, + "devDependencies": { + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^" + } +} as const \ No newline at end of file diff --git a/packages/schema-arbitrary/src/arbitrary.ts b/packages/schema-arbitrary/src/arbitrary.ts new file mode 100644 index 00000000..c3a7b5f8 --- /dev/null +++ b/packages/schema-arbitrary/src/arbitrary.ts @@ -0,0 +1,50 @@ +import { fc } from '@fast-check/vitest' +import { fn, URI } from '@traversable/registry' +import { t } from '@traversable/schema-core' + +export let defaultOptions = {} satisfies Required +declare namespace fromSchema { type Options = {} } + +/** + * ## {@link fromSchema `Arbitrary.fromSchema`} + */ +export let fromSchema + : (schema: S, options?: fromSchema.Options) => fc.Arbitrary + = t.fold>( + (x) => { + switch (true) { + default: return fn.exhaustive(x) + case x.tag === URI.never: return fc.constant(void 0) + case x.tag === URI.any: return fc.anything() + case x.tag === URI.unknown: return fc.anything() + case x.tag === URI.void: return fc.constant(void 0) + case x.tag === URI.null: return fc.constant(null) + case x.tag === URI.undefined: return fc.constant(undefined) + case x.tag === URI.symbol: return fc.string().map((_) => Symbol(_)) + case x.tag === URI.boolean: return fc.boolean() + case x.tag === URI.integer: return fc.integer({ min: x.minimum, max: x.maximum }) + case x.tag === URI.bigint: return fc.bigInt({ min: x.minimum, max: x.maximum }) + case x.tag === URI.string: return fc.string({ minLength: x.minLength, maxLength: x.maxLength }) + case x.tag === URI.number: return fc.float({ + min: t.number(x.exclusiveMinimum) ? x.exclusiveMinimum : x.minimum, + max: t.number(x.exclusiveMaximum) ? x.exclusiveMaximum : x.minimum, + minExcluded: t.number(x.exclusiveMinimum), + maxExcluded: t.number(x.exclusiveMaximum), + }) + case x.tag === URI.eq: return fc.constant(x.def) + case x.tag === URI.optional: return fc.option(x.def, { nil: void 0 }) + case x.tag === URI.array: return fc.array(x.def) + case x.tag === URI.record: return fc.dictionary(fc.string(), x.def) + case x.tag === URI.union: return fc.oneof(...x.def) + case x.tag === URI.tuple: return fc.tuple(...x.def) + case x.tag === URI.intersect: return fc.tuple(...x.def).map((xs) => xs.reduce<{}>( + (acc, cur) => cur == null ? acc : Object.assign(acc, cur), {} + )) + case x.tag === URI.object: { + let requiredKeys = Array.of().concat(x.req) + let optionalKeys = Array.of().concat(x.opt) + return fc.record(x.def, { ...optionalKeys.length > 0 && { requiredKeys } }) + } + } + } + ) diff --git a/packages/schema-arbitrary/src/exports.ts b/packages/schema-arbitrary/src/exports.ts new file mode 100644 index 00000000..faef772f --- /dev/null +++ b/packages/schema-arbitrary/src/exports.ts @@ -0,0 +1,2 @@ +export * from './version.js' +export * as Arbitrary from './arbitrary.js' diff --git a/packages/schema-arbitrary/src/index.ts b/packages/schema-arbitrary/src/index.ts new file mode 100644 index 00000000..9fd152fb --- /dev/null +++ b/packages/schema-arbitrary/src/index.ts @@ -0,0 +1 @@ +export * from './exports.js' \ No newline at end of file diff --git a/packages/schema-arbitrary/src/version.ts b/packages/schema-arbitrary/src/version.ts new file mode 100644 index 00000000..660ff1ca --- /dev/null +++ b/packages/schema-arbitrary/src/version.ts @@ -0,0 +1,3 @@ +import pkg from './__generated__/__manifest__.js' +export const VERSION = `${pkg.name}@${pkg.version}` as const +export type VERSION = typeof VERSION diff --git a/packages/schema-arbitrary/test/version.test.ts b/packages/schema-arbitrary/test/version.test.ts new file mode 100644 index 00000000..5ec1baa3 --- /dev/null +++ b/packages/schema-arbitrary/test/version.test.ts @@ -0,0 +1,10 @@ +import * as vi from 'vitest' +import pkg from '../package.json' with { type: 'json' } +import { VERSION } from '@traversable/schema-arbitrary' + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-arbitrary❳', () => { + vi.it('〖⛳️〗› ❲VERSION❳', () => { + const expected = `${pkg.name}@${pkg.version}` + vi.assert.equal(VERSION, expected) + }) +}) diff --git a/packages/schema-arbitrary/tsconfig.build.json b/packages/schema-arbitrary/tsconfig.build.json new file mode 100644 index 00000000..be49007b --- /dev/null +++ b/packages/schema-arbitrary/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "types": ["node"], + "declarationDir": "build/dts", + "outDir": "build/esm", + "stripInternal": true + }, + "references": [{ "path": "../registry" }, { "path": "../schema-core" }] +} diff --git a/packages/schema-arbitrary/tsconfig.json b/packages/schema-arbitrary/tsconfig.json new file mode 100644 index 00000000..2c291d21 --- /dev/null +++ b/packages/schema-arbitrary/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/packages/schema-arbitrary/tsconfig.src.json b/packages/schema-arbitrary/tsconfig.src.json new file mode 100644 index 00000000..702668d2 --- /dev/null +++ b/packages/schema-arbitrary/tsconfig.src.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "types": ["node"], + "outDir": "build/src" + }, + "references": [{ "path": "../registry" }, { "path": "../schema-core" }], + "include": ["src"] +} diff --git a/packages/schema-arbitrary/tsconfig.test.json b/packages/schema-arbitrary/tsconfig.test.json new file mode 100644 index 00000000..58314227 --- /dev/null +++ b/packages/schema-arbitrary/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "types": ["node"], + "noEmit": true + }, + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../registry" }, + { "path": "../schema-core" } + ], + "include": ["test"] +} diff --git a/packages/schema/vitest.config.ts b/packages/schema-arbitrary/vite.config.ts similarity index 100% rename from packages/schema/vitest.config.ts rename to packages/schema-arbitrary/vite.config.ts diff --git a/packages/schema/CHANGELOG.md b/packages/schema-core/CHANGELOG.md similarity index 100% rename from packages/schema/CHANGELOG.md rename to packages/schema-core/CHANGELOG.md diff --git a/packages/schema-core/README.md b/packages/schema-core/README.md new file mode 100644 index 00000000..837ae714 --- /dev/null +++ b/packages/schema-core/README.md @@ -0,0 +1,379 @@ +
+

ᯓ𝘁𝗿𝗮𝘃𝗲𝗿𝘀𝗮𝗯𝗹𝗲/𝘀𝗰𝗵𝗲𝗺𝗮

+
+ +

+ A lightweight, modular schema library with opt-in power tools. + Extensible in userland via + side-effect imports + + module augmentation. +

+ +
+ NPM Version +   + TypeScript +   + Static Badge +   + npm +   +
+ +
+ npm bundle size (scoped) +   + Static Badge +   + Static Badge +   +
+ +
+ Demo (StackBlitz) +   •   + TypeScript Playground +   •   + npm +
+
+
+ +
+ +`@traversable/schema-core` exploits a TypeScript feature called +[inferred type predicates](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#inferred-type-predicates) +to do what libaries like `zod` do, without the additional runtime overhead or abstraction. + +> **Note:** +> +> These docs are a W.I.P. +> +> We recommend jumping straight to the [demo](https://stackblitz.com/edit/traversable?file=src%2Fsandbox.tsx) +> or [playground](https://tsplay.dev/w2y29W). + +## Requirements + +The only hard requirement is [TypeScript 5.5](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/). +Since the core primitive that `@traversable/schema-core` is built on top of is +[inferred type predicates](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#inferred-type-predicates), +we do not have plans to backport to previous versions. + +## Quick start + +```typescript +import { t } from '@traversable/schema-core' + +declare let ex_01: unknown + +if (t.bigint(ex_01)) { + ex_01 + // ^? let ex_01: bigint +} + +const schema_01 = t.object({ + abc: t.optional(t.string), + def: t.tuple( + t.eq(1), + t.optional(t.eq(2)), // `t.eq` can be used to match any literal JSON value + t.optional(t.eq(3)), + ) +}) + +if (schema_01(ex_01)) { + ex_01 + // ^? let ex_01: { abc?: string, def: [ᵃ: 1, ᵇ?: 2, ᶜ?: 3] } + // ^ tuples are labeled to support optionality +} +``` + + +## Features + +`@traversable/schema-core` is modular by schema (like valibot), but takes it a step further by making its feature set opt-in by default. + +The ability to add features like this is a knock-on effect of traversable's extensible core. + +### First-class support for inferred type predicates + +> **Note:** This is the only feature on this list that is built into the core library. + +The motivation for creating another schema library was to add native support for inferred type predicates, +which no other schema library currently does (although please file an issue if that has changed!). + +This is possible because the traversable schemas are themselves just type predicates with a few additional properties +that allow them to also be used for reflection. + +- **Instructions:** To use this feature, define a predicate inline and `@traversable/schema-core` will figure out the rest. + +#### Example + +You can play with this example in the TypeScript Playground. + +```typescript +import { t } from '@traversable/schema-core' + +export let Classes = t.object({ + promise: (v) => v instanceof Promise, + set: (v) => v instanceof Set, + map: (v) => v instanceof Map, + weakMap: (v) => v instanceof WeakMap, + date: (v) => v instanceof Date, + regex: (v) => v instanceof RegExp, + error: (v) => v instanceof Error, + typeError: (v) => v instanceof TypeError, + syntaxError: (v) => v instanceof SyntaxError, + buffer: (v) => v instanceof ArrayBuffer, + readableStream: (v) => v instanceof ReadableStream, +}) + +type Classes = t.typeof +// ^? type Classes = { +// promise: Promise +// set: Set +// map: Map +// weakMap: WeakMap +// date: Date +// regex: RegExp +// error: Error +// typeError: TypeError +// syntaxError: SyntaxError +// buffer: ArrayBuffer +// readableStream: ReadableStream +// } + +let Values = t.object({ + function: (v) => typeof v === 'function', + successStatus: (v) => v === 200 || v === 201 || v === 202 || v === 204, + clientErrorStatus: (v) => v === 400 || v === 401 || v === 403 || v === 404, + serverErrorStatus: (v) => v === 500 || v === 502 || v === 503, + teapot: (v) => v === 418, + true: (v) => v === true, + false: (v) => v === false, + mixed: (v) => Array.isArray(v) || v === true, + startsWith: (v): v is `bill${string}` => typeof v === 'string' && v.startsWith('bill'), + endsWith: (v): v is `${string}murray` => typeof v === 'string' && v.endsWith('murral'), +}) + +type Values = t.typeof +// ^? type Values = { +// function: Function +// successStatus: 200 | 201 | 202 | 204 +// clientErrorStatus: 400 | 401 | 403 | 404 +// serverErrorStatus: 500 | 502 | 503 +// teapot: 418 +// true: true +// false: false +// mixed: true | any[] +// startsWith: `bill${string}` +// endsWith: `${string}murray` +// } + +let Shorthand = t.object({ + nonnullable: Boolean, + unknown: () => true, + never: () => false, +}) + +type Shorthand = t.typeof +// ^? type Shorthand = { +// nonnullable: {} +// unknown: unknown +// never?: never +// } +``` + +### `.validate` + +`.validate` is similar to `z.safeParse`, except more than an order of magnitude faster*. + +- **Instructions:** To install the `.validate` method to all schemas, simply import `@traversable/derive-validators/install`. +- [ ] TODO: add benchmarks + write-up + +#### Example + +Play with this example in the [TypeScript playground](https://tsplay.dev/NaBEPm). + +```typescript +import { t } from '@traversable/schema-core' +import '@traversable/derive-validators/install' +// ↑↑ importing `@traversable/derive-validators/install` adds `.validate` to all schemas + +let schema_01 = t.object({ + product: t.object({ + x: t.integer, + y: t.integer + }), + sum: t.union( + t.tuple(t.eq(0), t.integer), + t.tuple(t.eq(1), t.integer), + ), +}) + +let result = schema_01.validate({ product: { x: null }, sum: [2, 3.141592]}) +// ↑↑ .validate is available + +console.log(result) +// => +// [ +// { "kind": "TYPE_MISMATCH", "path": [ "product", "x" ], "expected": "number", "got": null }, +// { "kind": "REQUIRED", "path": [ "product" ], "msg": "Missing key 'y'" }, +// { "kind": "TYPE_MISMATCH", "path": [ "sum", 0 ], "expected": 0, "got": 2 }, +// { "kind": "TYPE_MISMATCH", "path": [ "sum", 1 ], "expected": "number", "got": 3.141592 }, +// { "kind": "TYPE_MISMATCH", "path": [ "sum", 0 ], "expected": 1, "got": 2 }, +// { "kind": "TYPE_MISMATCH", "path": [ "sum", 1 ], "expected": "number", "got": 3.141592 }, +// ] +``` + +### `.toString` + +The `.toString` method prints a stringified version of the type that the schema represents. + +Works on both the term- and type-level. + +- **Instructions:** To install the `.toString` method on all schemas, simply import `@traversable/schema-to-string/install`. + +- Caveat: type-level functionality is provided as a heuristic only; since object keys are unordered in the TS type system, the order that the +keys are printed at runtime might differ from the order they appear on the type-level. + +#### Example + +Play with this example in the [TypeScript playground](https://tsplay.dev/W49jew) + +```typescript +import { t } from '@traversable/schema-core' +import '@traversable/schema-to-string/install' +// ↑↑ importing `@traversable/schema-to-string/install` adds the upgraded `.toString` method on all schemas + +const schema_02 = t.intersect( + t.object({ + bool: t.optional(t.boolean), + nested: t.object({ + int: t.integer, + union: t.union(t.tuple(t.string), t.null), + }), + key: t.union(t.string, t.symbol, t.number), + }), + t.object({ + record: t.record(t.string), + maybeArray: t.optional(t.array(t.string)), + enum: t.enum('x', 'y', 1, 2, null), + }), +) + +let ex_02 = schema_02.toString() +// ^? let ex_02: "({ +// 'bool'?: (boolean | undefined), +// 'nested': { 'int': number, 'union': ([string] | null) }, +// 'key': (string | symbol | number) } +// & { +// 'record': Record, +// 'maybeArray'?: ((string)[] | undefined), +// 'enum': 'x' | 'y' | 1 | 2 | null +// })" +``` + +### `.toJsonSchema` + +- **Instructions:** To install the `.toJsonSchema` method on all schemas, simply import `@traversable/schema-to-json-schema/install`. + +#### Example + +Play with this example in the [TypeScript playground](https://tsplay.dev/NB98Vw). + +```typescript +import * as vi from 'vitest' + +import { t } from '@traversable/schema-core' +import '@traversable/schema-to-json-schema/install' +// ↑↑ importing `@traversable/schema-to-json-schema/install` adds `.toJsonSchema` on all schemas + +const schema_02 = t.intersect( + t.object({ + stringWithMaxExample: t.optional(t.string.max(255)), + nestedObjectExample: t.object({ + integerExample: t.integer, + tupleExample: t.tuple( + t.eq(1), + t.optional(t.eq(2)), + t.optional(t.eq(3)), + ), + }), + stringOrNumberExample: t.union(t.string, t.number), + }), + t.object({ + recordExample: t.record(t.string), + arrayExample: t.optional(t.array(t.string)), + enumExample: t.enum('x', 'y', 1, 2, null), + }), +) + +vi.assertType<{ + allOf: [ + { + type: "object" + required: ("nestedObjectExample" | "stringOrNumberExample")[] + properties: { + stringWithMaxExample: { type: "string", minLength: 3 } + stringOrNumberExample: { anyOf: [{ type: "string" }, { type: "number" }] } + nestedObjectExample: { + type: "object" + required: ("integerExample" | "tupleExample")[] + properties: { + integerExample: { type: "integer" } + tupleExample: { + type: "array" + minItems: 1 + maxItems: 3 + items: [{ const: 1 }, { const: 2 }, { const: 3 }] + additionalItems: false + } + } + } + } + }, + { + type: "object" + required: ("recordExample" | "enumExample")[] + properties: { + recordExample: { type: "object", additionalProperties: { type: "string" } } + arrayExample: { type: "array", items: { type: "string" } } + enumExample: { enum: ["x", "y", 1, 2, null] } + } + } + ] +}>(schema_02.toJsonSchema()) +// ↑↑ importing `@traversable/schema-to-json-schema` installs `.toJsonSchema` +``` + +### Codec (`.pipe`, `.extend`, `.parse`, `.decode` & `.encode`) + +- **Instructions:** to install the `.pipe` and `.extend` methods on all schemas, simply `@traversable/derive-codec/install`. + - To create a covariant codec (similar to zod's `.transform`), use `.pipe` + - To create a contravariant codec (similar to zod's `.preprocess`), use `.extend` (WIP) + +#### Example + +Play with this example in the [TypeScript playground](https://tsplay.dev/mbbv3m). + +```typescript +import { t } from '@traversable/schema-core' +import '@traversable/derive-codec/install' +// ↑↑ importing `@traversable/derive-codec/install` adds `.pipe` and `.extend` on all schemas + +let User = t + .object({ name: t.optional(t.string), createdAt: t.string }) + .pipe((user) => ({ ...user, createdAt: new Date(user.createdAt) })) + .unpipe((user) => ({ ...user, createdAt: user.createdAt.toISOString() })) + +let fromAPI = User.parse({ name: 'Bill Murray', createdAt: new Date().toISOString() }) +// ^? let fromAPI: Error | { name?: string, createdAt: Date} + +if (fromAPI instanceof Error) throw fromAPI +fromAPI +// ^? { name?: string, createdAt: Date } + +let toAPI = User.encode(fromAPI) +// ^? let toAPI: { name?: string, createdAt: string } +``` + diff --git a/packages/schema-core/package.json b/packages/schema-core/package.json new file mode 100644 index 00000000..32405daf --- /dev/null +++ b/packages/schema-core/package.json @@ -0,0 +1,61 @@ +{ + "name": "@traversable/schema-core", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/schema" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { + "include": [ + "**/*.ts", + "schemas/*.ts" + ] + }, + "generateIndex": { + "include": [ + "**/*.ts" + ] + } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "test": "vitest" + }, + "devDependencies": { + "@sinclair/typebox": "^0.34.33", + "@traversable/schema-zod-adapter": "workspace:^", + "@types/lodash.isequal": "^4.5.8", + "arktype": "^2.1.20", + "fast-check": "^3.0.0", + "lodash.isequal": "^4.5.0", + "valibot": "1.0.0-rc.1", + "zod": "^3.24.2", + "zod3": "npm:zod@3", + "zod4": "npm:zod@4.0.0-beta.20250420T053007" + }, + "peerDependencies": { + "@traversable/registry": "workspace:^" + } +} diff --git a/packages/schema-core/src/__generated__/__manifest__.ts b/packages/schema-core/src/__generated__/__manifest__.ts new file mode 100644 index 00000000..3cf973f6 --- /dev/null +++ b/packages/schema-core/src/__generated__/__manifest__.ts @@ -0,0 +1,56 @@ +export default { + "name": "@traversable/schema-core", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/schema" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { + "include": ["**/*.ts", "schemas/*.ts"] + }, + "generateIndex": { + "include": ["**/*.ts"] + } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "test": "vitest" + }, + "devDependencies": { + "@sinclair/typebox": "^0.34.33", + "@traversable/schema-zod-adapter": "workspace:^", + "@types/lodash.isequal": "^4.5.8", + "arktype": "^2.1.20", + "fast-check": "^3.0.0", + "lodash.isequal": "^4.5.0", + "valibot": "1.0.0-rc.1", + "zod": "^3.24.2", + "zod3": "npm:zod@3", + "zod4": "npm:zod@4.0.0-beta.20250420T053007" + }, + "peerDependencies": { + "@traversable/registry": "workspace:^" + } +} as const \ No newline at end of file diff --git a/packages/schema-core/src/clone.ts b/packages/schema-core/src/clone.ts new file mode 100644 index 00000000..203c416b --- /dev/null +++ b/packages/schema-core/src/clone.ts @@ -0,0 +1,9 @@ +import type { Param } from '@traversable/registry' +import type { LowerBound } from './types.js' + +export function clone(schema: S): S +export function clone(schema: S) { + function cloned(u: Param) { return schema(u) } + for (const k in schema) (cloned as typeof schema)[k] = schema[k] + return cloned +} diff --git a/packages/schema/src/combinators.ts b/packages/schema-core/src/combinators.ts similarity index 74% rename from packages/schema/src/combinators.ts rename to packages/schema-core/src/combinators.ts index dafb688b..89f48abc 100644 --- a/packages/schema/src/combinators.ts +++ b/packages/schema-core/src/combinators.ts @@ -1,18 +1,18 @@ import type * as T from './types.js' -import * as t from './schema.js' - -type Predicate = T.Predicate | t.Schema +import type { LowerBound } from './types.js' +import type { of } from './schemas/of.js' /** * ## {@link filter `t.filter`} */ -export function filter, T extends t.LowerBound>(schema: S, filter: T): T -export function filter, T extends S['_type']>(schema: S, filter: (s: S['_type']) => s is T): t.of -export function filter(schema: S, filter: (s: S['_type']) => boolean): S +export function filter>(schema: S, filter: T): T +export function filter(schema: S, filter: (s: S['_type']) => s is T): of +export function filter(schema: S, filter: (s: S['_type']) => boolean): S export function filter(guard: T.Guard, narrower: (x: T) => x is U): T.Guard +export function filter(guard: S): (predicate: (x: S['_type']) => boolean) => S export function filter(guard: T.Guard, predicate: (x: T) => boolean): T.Guard export function filter(guard: T.Guard): (predicate: (x: T) => boolean) => T.Guard -export function filter(...args: [guard: T.Guard] | [guard: T.Guard, predicate: T.Predicate]) { +export function filter(...args: [any] | [any, any]) { if (args.length === 1) return (predicate: T.Predicate) => filter(args[0], predicate) else return (x: T) => args[0](x) && args[1](x) } diff --git a/packages/schema-core/src/core.ts b/packages/schema-core/src/core.ts new file mode 100644 index 00000000..aba76690 --- /dev/null +++ b/packages/schema-core/src/core.ts @@ -0,0 +1,204 @@ +import type * as T from '@traversable/registry' +import { fn, has, symbol, URI } from '@traversable/registry' + +import type { Guarded, Schema } from './types.js' + +import type { of } from './schemas/of.js' +import { never as never_ } from './schemas/never.js' +import { any as any_ } from './schemas/any.js' +import { unknown as unknown_ } from './schemas/unknown.js' +import { void as void_ } from './schemas/void.js' +import { null as null_ } from './schemas/null.js' +import { undefined as undefined_ } from './schemas/undefined.js' +import { symbol as symbol_ } from './schemas/symbol.js' +import { boolean as boolean_ } from './schemas/boolean.js' +import { integer as integer } from './schemas/integer.js' +import { bigint as bigint_ } from './schemas/bigint.js' +import { number as number_ } from './schemas/number.js' +import { string as string_ } from './schemas/string.js' +import { eq } from './schemas/eq.js' +import { optional } from './schemas/optional.js' +import { array } from './schemas/array.js' +import { record } from './schemas/record.js' +import { union } from './schemas/union.js' +import { intersect } from './schemas/intersect.js' +import { tuple } from './schemas/tuple.js' +import { object as object_ } from './schemas/object.js' + +export type Inline = never | of> + +export type typeOf< + T extends { _type?: unknown }, + _ extends + | T['_type'] + = T['_type'] +> = never | _ + +export type Unary = + | eq + | array + | record + | optional + | union + | intersect + | tuple + | object_<{ [x: string]: Unary }> + + +export type F = + | Leaf + | eq + | array + | record + | optional + | union + | intersect + | tuple + | object_<{ [x: string]: T }> + +export declare namespace F { + type Unary = + | eq + | array + | record + | optional + | union + | intersect + | tuple + | object_<{ [x: string]: T }> +} + +export type Fixpoint = + | Leaf + | Unary + +export interface Free extends T.HKT { [-1]: F } + +export type Leaf = typeof leaves[number] +export type LeafTag = Leaf['tag'] +export type Nullary = typeof nullaries[number] +export type NullaryTag = Nullary['tag'] +export type Boundable = typeof boundables[number] +export type BoundableTag = Boundable['tag'] +export type Tag = typeof tags[number] +export type UnaryTag = typeof unaryTags[number] +export type TypeName = T.TypeName + +interface NullaryCatalog { + never: never_ + any: any_ + unknown: unknown_ + void: void_ + null: null_ + undefined: undefined_ + symbol: symbol_ + boolean: boolean_ +} + +interface BoundableCatalog { + integer: integer + number: number_ + bigint: bigint_ + string: string_ +} + +interface UnaryCatalog { + eq: eq + optional: optional + array: array + record: record + union: union + intersect: intersect + tuple: tuple + object: object_<{ [x: string]: T }> +} + +export interface Catalog extends Catalog.Boundable, Catalog.Nullary, Catalog.Unary {} +export declare namespace Catalog { + export { + BoundableCatalog as Boundable, + NullaryCatalog as Nullary, + UnaryCatalog as Unary, + } +} + +const hasTag = has('tag', (tag) => typeof tag === 'string') + +export const nullaries = [unknown_, never_, any_, void_, undefined_, null_, symbol_, boolean_] +export const nullaryTags = nullaries.map((x) => x.tag) +export const isNullaryTag = (u: unknown): u is NullaryTag => nullaryTags.includes(u as never) +export const isNullary = (u: unknown): u is Nullary => hasTag(u) && nullaryTags.includes(u.tag as never) + +export const boundables = [integer, bigint_, number_, string_] +export const boundableTags = boundables.map((x) => x.tag) +export const isBoundableTag = (u: unknown): u is BoundableTag => boundableTags.includes(u as never) +export const isBoundable = (u: unknown): u is Boundable => hasTag(u) && boundableTags.includes(u.tag as never) + +export const leaves = [...nullaries, ...boundables] +export const leafTags = leaves.map((leaf) => leaf.tag) +export const isLeaf = (u: unknown): u is Leaf => hasTag(u) && leafTags.includes(u.tag as never) + +export const unaryTags = [URI.optional, URI.eq, URI.array, URI.record, URI.tuple, URI.union, URI.intersect, URI.object] +export const tags = [...leafTags, ...unaryTags] +export const isUnary = (u: unknown): u is Unary => hasTag(u) && unaryTags.includes(u.tag as never) + +export const isCore: { + (u: F): u is F + (u: unknown): u is Fixpoint + (u: F): u is F +} = ((u: unknown) => hasTag(u) && tags.includes(u.tag as never)) as never + + +export declare namespace Functor { + export type Algebra = T.Algebra + export type Index = (keyof any)[] + export type IndexedAlgebra = T.IndexedAlgebra + export { + F, + Free, + Fixpoint, + } +} + +export const Functor: T.Functor = { + map(f) { + return (x) => { + switch (true) { + default: return fn.exhaustive(x) + case isLeaf(x): return x + case x.tag === URI.eq: return eq.def(x.def as never) as never + case x.tag === URI.array: return array.def(f(x.def), x) + case x.tag === URI.record: return record.def(f(x.def)) + case x.tag === URI.optional: return optional.def(f(x.def)) + case x.tag === URI.tuple: return tuple.def(fn.map(x.def, f)) + case x.tag === URI.object: return object_.def(fn.map(x.def, f)) + case x.tag === URI.union: return union.def(fn.map(x.def, f)) + case x.tag === URI.intersect: return intersect.def(fn.map(x.def, f)) + } + } + } +} + +export const IndexedFunctor: T.Functor.Ix = { + ...Functor, + mapWithIndex(f) { + return (x, ix) => { + switch (true) { + default: return fn.exhaustive(x) + case isLeaf(x): return x + case x.tag === URI.eq: return eq.def(x.def as never) as never + case x.tag === URI.array: return array.def(f(x.def, ix), x) + case x.tag === URI.record: return record.def(f(x.def, ix)) + case x.tag === URI.optional: return optional.def(f(x.def, ix)) + case x.tag === URI.tuple: return tuple.def(fn.map(x.def, (y, iy) => f(y, [...ix, iy])), x.opt) + case x.tag === URI.object: return object_.def(fn.map(x.def, (y, iy) => f(y, [...ix, iy])), {}, x.opt as never) + case x.tag === URI.union: return union.def(fn.map(x.def, (y, iy) => f(y, [...ix, symbol.union, iy]))) + case x.tag === URI.intersect: return intersect.def(fn.map(x.def, (y, iy) => f(y, [...ix, symbol.intersect, iy]))) + } + } + } +} + +export const unfold = fn.ana(Functor) +export const fold = fn.cata(Functor) +export const foldWithIndex = fn.cataIx(IndexedFunctor) diff --git a/packages/schema/src/enum.ts b/packages/schema-core/src/enum.ts similarity index 90% rename from packages/schema/src/enum.ts rename to packages/schema-core/src/enum.ts index dc79d4fb..d0dc2f85 100644 --- a/packages/schema/src/enum.ts +++ b/packages/schema-core/src/enum.ts @@ -1,11 +1,5 @@ import type { Join, Primitive, Returns, Showable, UnionToTuple } from '@traversable/registry' -import { URI } from '@traversable/registry' - -/** @internal */ -const Object_values = globalThis.Object.values - -/** @internal */ -const Object_assign = globalThis.Object.assign +import { Object_assign, Object_values, URI } from '@traversable/registry' export type EnumType = T extends readonly unknown[] ? T[number] : T[keyof T] @@ -53,7 +47,7 @@ function enum_]>(...args: V return enum_.def(values) } namespace enum_ { - export let prototype = { tag: URI.enum } + export let userDefinitions = { tag: URI.enum } export function def(args: readonly [...T]): enum_ /* v8 ignore next 1 */ export function def(values: T): enum_ { @@ -64,6 +58,6 @@ namespace enum_ { enumGuard.get = values enumGuard.toJsonSchema = toJsonSchema enumGuard.toString = toString - return Object_assign(enumGuard, prototype) as never + return Object_assign(enumGuard, userDefinitions) as never } } diff --git a/packages/schema/src/equals.ts b/packages/schema-core/src/equals.ts similarity index 100% rename from packages/schema/src/equals.ts rename to packages/schema-core/src/equals.ts diff --git a/packages/schema-core/src/exports.ts b/packages/schema-core/src/exports.ts new file mode 100644 index 00000000..10cbec4c --- /dev/null +++ b/packages/schema-core/src/exports.ts @@ -0,0 +1,92 @@ +export type { + Algebra, + Array, + Atoms, + Coalgebra, + Comparator, + Conform, + Const, + Dictionary, + Either, + Entries, + Force, + Functor, + HKT, + Identity, + IndexedAlgebra, + IndexedRAlgebra, + Intersect, + Join, + Kind, + Mut, + Mutable, + NonUnion, + Param, + Primitive, + RAlgebra, + ReadonlyArray, + Record, + Returns, + Showable, + Tuple, + Type, + TypeConstructor, + TypeError, + TypeName, + UnionToIntersection, + UnionToTuple, + inline, + newtype, + GlobalConfig, + SchemaConfig, + SchemaOptions, +} from '@traversable/registry' +export { + configure, + defaults, + getConfig, + applyOptions, + symbol, +} from '@traversable/registry' + +export * as t from './namespace.js' +export * from './extensions.js' +export type { + Boundable, + BoundableTag, + Nullary, + NullaryTag, + Unary, + UnaryTag, +} from './core.js' + +export * as recurse from './recursive.js' + +export * as Equal from './equals.js' +export type Equal = import('@traversable/registry').Equal + +export * as Predicate from './predicates.js' +export type Predicate = [T] extends [never] + ? import('./types.js').SchemaLike + : import('./types.js').Predicate + +export { clone } from './clone.js' + +export type { + FirstOptionalItem, + Guard, + IntersectType, + Label, + TupleType, + Typeguard, + ValidateTuple, +} from './types.js' + +export { get, get$ } from './utils.js' + +export { VERSION } from './version.js' + +export { + /** @internal */ + trim as __trim, +} from './recursive.js' diff --git a/packages/schema-core/src/extensions.ts b/packages/schema-core/src/extensions.ts new file mode 100644 index 00000000..cd37b25d --- /dev/null +++ b/packages/schema-core/src/extensions.ts @@ -0,0 +1,29 @@ +export type { + LowerBound as t_LowerBound, + Schema as t_Schema, +} from './types.js' +export { + enum as t_enum, +} from './enum.js' + +export type { never as t_never } from './schemas/never.js' +export type { any as t_any } from './schemas/any.js' +export type { unknown as t_unknown } from './schemas/unknown.js' +export type { void as t_void } from './schemas/void.js' +export type { null as t_null } from './schemas/null.js' +export type { undefined as t_undefined } from './schemas/undefined.js' +export type { symbol as t_symbol } from './schemas/symbol.js' +export type { boolean as t_boolean } from './schemas/boolean.js' +export type { integer as t_integer } from './schemas/integer.js' +export type { bigint as t_bigint } from './schemas/bigint.js' +export type { number as t_number } from './schemas/number.js' +export type { string as t_string } from './schemas/string.js' +export type { eq as t_eq } from './schemas/eq.js' +export type { optional as t_optional } from './schemas/optional.js' +export type { array as t_array } from './schemas/array.js' +export type { record as t_record } from './schemas/record.js' +export type { union as t_union } from './schemas/union.js' +export type { intersect as t_intersect } from './schemas/intersect.js' +export type { tuple as t_tuple } from './schemas/tuple.js' +export type { object as t_object } from './schemas/object.js' +export type { of as t_of } from './schemas/of.js' diff --git a/packages/schema/src/has.ts b/packages/schema-core/src/has.ts similarity index 58% rename from packages/schema/src/has.ts rename to packages/schema-core/src/has.ts index 23e8ce9c..a47b9dc3 100644 --- a/packages/schema/src/has.ts +++ b/packages/schema-core/src/has.ts @@ -1,18 +1,18 @@ import { has as has_, URI } from '@traversable/registry' -import * as t from './schema.js' - -export const key = t.union(t.string, t.number, t.symbol) +import type { Schema, SchemaLike } from './types.js' +import type { of } from './schemas/of.js' +import type * as t from './schemas/unknown.js' export interface has { tag: URI.has (u: unknown): u is this['_type'] _type: has_ - def: [path: KS, predicate: S] + get def(): [path: KS, predicate: S] } -export function has(...args: readonly [...path: KS, leafSchema?: S]): has> -export function has(...args: readonly [...path: KS, leafSchema?: S]): has -export function has(...path: readonly [...KS]): has> +export function has(...args: readonly [...path: KS, leafSchema?: S]): has> +export function has(...args: readonly [...path: KS, leafSchema?: S]): has +export function has(...path: readonly [...KS]): has export function has(...args: readonly [...KS]) { return has_(...args) } diff --git a/packages/schema-core/src/index.ts b/packages/schema-core/src/index.ts new file mode 100644 index 00000000..410a4bcb --- /dev/null +++ b/packages/schema-core/src/index.ts @@ -0,0 +1 @@ +export * from './exports.js' diff --git a/packages/schema-core/src/key.ts b/packages/schema-core/src/key.ts new file mode 100644 index 00000000..2dbace05 --- /dev/null +++ b/packages/schema-core/src/key.ts @@ -0,0 +1,6 @@ +import { number } from './schemas/number.js' +import { string } from './schemas/string.js' +import { symbol } from './schemas/symbol.js' +import { union } from './schemas/union.js' + +export const key = union(string, number, symbol) diff --git a/packages/schema/src/types.ts b/packages/schema-core/src/label.ts similarity index 95% rename from packages/schema/src/types.ts rename to packages/schema-core/src/label.ts index 42dd9730..35c9035f 100644 --- a/packages/schema/src/types.ts +++ b/packages/schema-core/src/label.ts @@ -1,74 +1,9 @@ -import type { TypeError } from '@traversable/registry' -import type * as t from './schema.js' - -export type Target = S extends Guard ? T : S extends Predicate ? T : never - -export type $ = [keyof S] extends [never] ? unknown : S - -export interface Predicate { - (value: T): boolean - (value?: T): boolean -} - -export type InvalidItem = never | TypeError<'A required element cannot follow an optional element.'> - -export type Guard = { (u: unknown): u is T } -export interface Typeguard { (u: unknown): u is this['_type']; readonly _type: T } - -export type { TypePredicate_ as TypePredicate } -type TypePredicate_ = never | TypePredicate<[I, O]> - -interface TypePredicate { - (u: T[0]): u is T[1] - (u: T[1]): boolean -} - -export type ValidateTuple< - T extends readonly unknown[], - LowerBound = t.optional, - V = ValidateOptionals<[...T], LowerBound>, -> = [V] extends [['ok']] ? T : V - -export type ValidateOptionals, Acc extends unknown[] = []> - = LowerBound extends S[number] - ? S extends [infer H, ...infer T] - ? LowerBound extends H - ? T[number] extends LowerBound - ? ['ok'] - : [...Acc, H, ...{ [Ix in keyof T]: T[Ix] extends LowerBound ? T[Ix] : InvalidItem }] - : ValidateOptionals - : ['ok'] - : ['ok'] - ; - -export type Label< - Req extends readonly any[], - Opt extends readonly any[] -> = [ - ...Label.Required, - /* @ts-expect-error */ - ...Label.Optional, - ] - -export declare namespace Label { - type Required< - S extends readonly unknown[], - _ = REQ[S['length'] & keyof REQ] - > = { [I in keyof _]: S[I & keyof S] } - - type Optional< - Offset extends number, - Base extends any[], - /* @ts-expect-error */ - Start = OPT[Offset][Base['length']] - > = { [I in keyof Start]: Base[I & keyof Base] } -} /** * ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻ */ -type REQ = typeof REQ -declare const REQ: { +export type REQ = typeof REQ +export declare const REQ: { [0x00]: [] [0x01]: [ᵃ: 0x01] [0x02]: [ᵃ: 0x01, ᵇ: 0x02] @@ -101,8 +36,8 @@ declare const REQ: { /** * ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻ */ -type OPT = typeof OPT -declare const OPT: { +export type OPT = typeof OPT +export declare const OPT: { [0x00]: { [0x00]: [] [0x01]: [ᵃ?: 0x01] diff --git a/packages/schema-core/src/namespace.ts b/packages/schema-core/src/namespace.ts new file mode 100644 index 00000000..ba3b5e74 --- /dev/null +++ b/packages/schema-core/src/namespace.ts @@ -0,0 +1,89 @@ +export * as recurse from './recursive.js' + +export { never } from './schemas/never.js' +export { any } from './schemas/any.js' +export { unknown } from './schemas/unknown.js' +export { void } from './schemas/void.js' +export { null } from './schemas/null.js' +export { undefined } from './schemas/undefined.js' +export { symbol } from './schemas/symbol.js' +export { boolean } from './schemas/boolean.js' +export { integer } from './schemas/integer.js' +export { bigint } from './schemas/bigint.js' +export { number } from './schemas/number.js' +export { string } from './schemas/string.js' +export { eq } from './schemas/eq.js' +export { optional } from './schemas/optional.js' +export { array, readonlyArray } from './schemas/array.js' +export { record } from './schemas/record.js' +export { union } from './schemas/union.js' +export { intersect } from './schemas/intersect.js' +export { tuple } from './schemas/tuple.js' +export { object } from './schemas/object.js' +export { nonnullable } from './nonnullable.js' +export { of } from './schemas/of.js' + +export type { + Boundable, + Catalog, + F, + Fixpoint, + Free, + Inline, + Leaf, + Tag, + TypeName, + typeOf as typeof, + Unary, +} from './core.ts' + +export { + isLeaf, + isNullary, + isNullaryTag, + isBoundable, + isBoundableTag, + isUnary, + isCore, + Functor, + IndexedFunctor, + fold, + foldWithIndex, + unfold, + tags, +} from './core.js' + +export type { + bottom, + invalid, + top, + Entry, + FirstOptionalItem, + Guard, + Guarded, + IntersectType, + TupleType, + LowerBound, + Optional, + Predicate, + Required, + Schema, + SchemaLike, + Typeguard, + UnknownSchema, + ValidateTuple, +} from './types.js' + +export { has } from './has.js' +export { key } from './key.js' + +/* data-types & combinators */ +export * from './combinators.js' +export { enum } from './enum.js' + +/** + * exported as escape hatches, to prevent collisions with built-in keywords + */ +export { null as null_ } from './schemas/null.js' +export { undefined as undefined_ } from './schemas/undefined.js' +export { void as void_ } from './schemas/void.js' diff --git a/packages/schema-core/src/nonnullable.ts b/packages/schema-core/src/nonnullable.ts new file mode 100644 index 00000000..4f3ac083 --- /dev/null +++ b/packages/schema-core/src/nonnullable.ts @@ -0,0 +1,38 @@ +import type { Unknown } from '@traversable/registry' +import { bindUserExtensions, Object_assign, URI } from '@traversable/registry' + +export { nonnullable } +interface nonnullable extends nonnullable.core { + //<%= Types %> +} + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +function NonNullableSchema(src: unknown) { return src != null } +const nonnullable = Object_assign( + NonNullableSchema, + userDefinitions, +) as nonnullable + +nonnullable.tag = URI.nonnullable; +(nonnullable.def as {}) = {} + +declare namespace nonnullable { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + _type: {} + tag: URI.nonnullable + get def(): this['_type'] + } +} + +Object_assign( + nonnullable, + bindUserExtensions(nonnullable, userExtensions), +) diff --git a/packages/schema-core/src/postinstall.ts b/packages/schema-core/src/postinstall.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/schema/src/predicates.ts b/packages/schema-core/src/predicates.ts similarity index 92% rename from packages/schema/src/predicates.ts rename to packages/schema-core/src/predicates.ts index f53b16c1..32223c6f 100644 --- a/packages/schema/src/predicates.ts +++ b/packages/schema-core/src/predicates.ts @@ -1,7 +1,9 @@ import type { Intersect, SchemaOptions } from '@traversable/registry' import { symbol as Symbol, URI } from '@traversable/registry' -import type * as t from './schema.js' +import type { Predicate } from './types.js' +import type { optional } from './schemas/optional.js' +import type * as t from './schemas/undefined.js' export { null_ as null, @@ -140,7 +142,7 @@ function isRequiredSchema(u: unknown): u is (_: unknown) => _ is T { return !!u && !isOptionalSchema(u) } export { isOptionalNotUndefinedSchema as __isOptionalNotUndefinedSchema } -function isOptionalNotUndefinedSchema(u: unknown): u is t.optional { +function isOptionalNotUndefinedSchema(u: unknown): u is optional { return !!u && isOptionalSchema(u) && u.def(undefined) === false } @@ -186,13 +188,13 @@ export function record( } } -function union u is unknown)[]>(guard: readonly [...T]): (u: unknown) => u is T[number] -function union u is unknown)[]>(qs: readonly [...T]) { +export function union u is unknown)[]>(guard: readonly [...T]): (u: unknown) => u is T[number] +export function union u is unknown)[]>(qs: readonly [...T]) { return (u: unknown): u is never => qs.some((q) => q(u)) } -function intersect u is unknown)[]>(guard: readonly [...T]): (u: unknown) => u is Intersect -function intersect u is unknown)[]>(qs: readonly [...T]) { +export function intersect u is unknown)[]>(guard: readonly [...T]): (u: unknown) => u is Intersect +export function intersect u is unknown)[]>(qs: readonly [...T]) { return (u: unknown): u is never => qs.every((q) => q(u)) } @@ -230,7 +232,6 @@ function treatUndefinedAndOptionalAsTheSame return true } - type Target = S extends { (_: any): _ is infer T } ? T : S extends { (u: infer T): boolean } ? T : never type Object$ = (u: unknown) => u is { [K in keyof T]: Target } @@ -253,7 +254,7 @@ function object$ u is unknown)[]>(...q } export function tuple$(options: Opts): - (qs: T) + (qs: T) => (u: unknown) => u is { [I in keyof T]: Target; } { - return (qs: T): (u: unknown) => u is { [I in keyof T]: Target } => { + return (qs: T): (u: unknown) => u is { [I in keyof T]: Target } => { const checkLength = (xs: readonly unknown[]) => options?.minLength === void 0 ? (xs.length === qs.length) diff --git a/packages/schema/src/recursive.ts b/packages/schema-core/src/recursive.ts similarity index 95% rename from packages/schema/src/recursive.ts rename to packages/schema-core/src/recursive.ts index 91c9d421..35ee5bec 100644 --- a/packages/schema/src/recursive.ts +++ b/packages/schema-core/src/recursive.ts @@ -1,7 +1,8 @@ import type * as T from '@traversable/registry' import { escape, fn, parseKey, typeName, URI } from '@traversable/registry' -import * as t from './schema.js' +import type { Schema } from './types.js' +import * as t from './core.js' /** * Note: strictly speaking, `undefined` is not a valid JSON value. It's @@ -129,13 +130,13 @@ export namespace Recursive { } const fold - : (algebra: T.Algebra) => (term: S) => string + : (algebra: T.Algebra) => (term: S) => string = t.fold as never export const toString - : (schema: t.Schema) => string + : (schema: Schema) => string = fold(Recursive.toString) export const toTypeString - : (schema: t.Schema) => string + : (schema: Schema) => string = (schema) => trim(fold(Recursive.toTypeString)(schema)) diff --git a/packages/schema-core/src/schemas/any.ts b/packages/schema-core/src/schemas/any.ts new file mode 100644 index 00000000..877f92af --- /dev/null +++ b/packages/schema-core/src/schemas/any.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { any_ as any } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface any_ extends any_.core { + //<%= Types %> +} + +function AnySchema(src: unknown): src is any { return true } +AnySchema.tag = URI.any +AnySchema.def = void 0 as any + +const any_ = Object_assign( + AnySchema, + userDefinitions, +) as any_ + +Object_assign(any_, userExtensions) + +declare namespace any_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.any + _type: any + get def(): this['_type'] + } +} diff --git a/packages/schema-core/src/schemas/array.ts b/packages/schema-core/src/schemas/array.ts new file mode 100644 index 00000000..65c8c03c --- /dev/null +++ b/packages/schema-core/src/schemas/array.ts @@ -0,0 +1,128 @@ +import type { + Bounds, + Integer, + Unknown, +} from '@traversable/registry' +import { + Array_isArray, + array as arrayOf, + bindUserExtensions, + carryover, + within, + _isPredicate, + has, + Math_max, + Math_min, + Number_isSafeInteger, + Object_assign, + URI, +} from '@traversable/registry' + +import type { Guarded, Schema, SchemaLike } from '../namespace.js' + +import type { of } from './of.js' + +/** @internal */ +function boundedArray(schema: S, bounds: Bounds, carry?: Partial>): ((u: unknown) => boolean) & Bounds & array +function boundedArray(schema: S, bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & array +function boundedArray(schema: S, bounds: Bounds, carry?: {}): ((u: unknown) => boolean) & Bounds & array { + return Object_assign(function BoundedArraySchema(u: unknown) { + return Array_isArray(u) && within(bounds)(u.length) + }, carry, array(schema)) +} + +export interface array extends array.core { + //<%= Types %> +} + +export function array(schema: S, readonly: 'readonly'): readonlyArray +export function array(schema: S): array +export function array(schema: S): array>> +export function array(schema: S): array { + return array.def(schema) +} + +export namespace array { + export let userDefinitions: Record = { + //<%= Definitions %> + } as array + export function def(x: S, prev?: array): array + export function def(x: S, prev?: unknown): array + export function def(x: S, prev?: array): array + /* v8 ignore next 1 */ + export function def(x: unknown, prev?: unknown): {} { + let userExtensions: Record = { + //<%= Extensions %> + } + const predicate = _isPredicate(x) ? arrayOf(x) : Array_isArray + function ArraySchema(src: unknown) { return predicate(src) } + ArraySchema.tag = URI.array + ArraySchema.def = x + ArraySchema.min = function arrayMin(minLength: Min) { + return Object_assign( + boundedArray(x, { gte: minLength }, carryover(this, 'minLength' as never)), + { minLength }, + ) + } + ArraySchema.max = function arrayMax(maxLength: Max) { + return Object_assign( + boundedArray(x, { lte: maxLength }, carryover(this, 'maxLength' as never)), + { maxLength }, + ) + } + ArraySchema.between = function arrayBetween( + min: Min, + max: Max, + minLength = Math_min(min, max), + maxLength = Math_max(min, max) + ) { + return Object_assign( + boundedArray(x, { gte: minLength, lte: maxLength }), + { minLength, maxLength }, + ) + } + if (has('minLength', Number_isSafeInteger)(prev)) ArraySchema.minLength = prev.minLength + if (has('maxLength', Number_isSafeInteger)(prev)) ArraySchema.maxLength = prev.maxLength + Object_assign(ArraySchema, userDefinitions) + return Object_assign(ArraySchema, bindUserExtensions(ArraySchema, userExtensions)) + } +} + +export declare namespace array { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.array + get def(): S + _type: S['_type' & keyof S][] + minLength?: number + maxLength?: number + min>(minLength: Min): array.Min + max>(maxLength: Max): array.Max + between, Max extends Integer>(minLength: Min, maxLength: Max): array.between<[min: Min, max: Max], S> + } + type Min + = [Self] extends [{ maxLength: number }] + ? array.between<[min: Min, max: Self['maxLength']], Self['def' & keyof Self]> + : array.min + ; + type Max + = [Self] extends [{ minLength: number }] + ? array.between<[min: Self['minLength'], max: Max], Self['def' & keyof Self]> + : array.max + ; + interface min extends array { minLength: Min } + interface max extends array { maxLength: Max } + interface between extends array { minLength: Bounds[0], maxLength: Bounds[1] } + type type = never | T +} + +export const readonlyArray: { + (schema: S): readonlyArray + (schema: S): readonlyArray> +} = array +export interface readonlyArray { + (u: unknown): u is this['_type'] + tag: URI.array + def: S + _type: ReadonlyArray +} diff --git a/packages/schema-core/src/schemas/bigint.ts b/packages/schema-core/src/schemas/bigint.ts new file mode 100644 index 00000000..a3d9ecfe --- /dev/null +++ b/packages/schema-core/src/schemas/bigint.ts @@ -0,0 +1,102 @@ +import type { Bounds, Unknown } from '@traversable/registry' +import { + bindUserExtensions, + carryover, + Object_assign, + URI, + withinBig as within, +} from '@traversable/registry' + +export { bigint_ as bigint } + +/** @internal */ +function boundedBigInt(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & bigint_ +function boundedBigInt(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & bigint_ +function boundedBigInt(bounds: Bounds, carry?: {}): {} { + return Object_assign(function BoundedBigIntSchema(u: unknown) { + return bigint_(u) && within(bounds)(u) + }, carry, bigint_) +} + +interface bigint_ extends bigint_.core { + //<%= Types %> +} + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +function BigIntSchema(src: unknown) { return typeof src === 'bigint' } +BigIntSchema.tag = URI.bigint +BigIntSchema.def = 0n + +const bigint_ = Object_assign( + BigIntSchema, + userDefinitions, +) as bigint_ + +bigint_.min = function bigIntMin(minimum) { + return Object_assign( + boundedBigInt({ gte: minimum }, carryover(this, 'minimum')), + { minimum }, + ) +} +bigint_.max = function bigIntMax(maximum) { + return Object_assign( + boundedBigInt({ lte: maximum }, carryover(this, 'maximum')), + { maximum }, + ) +} +bigint_.between = function bigIntBetween( + min, + max, + minimum = (max < min ? max : min), + maximum = (max < min ? min : max), +) { + return Object_assign( + boundedBigInt({ gte: minimum, lte: maximum }), + { minimum, maximum } + ) +} + +Object_assign( + bigint_, + bindUserExtensions(bigint_, userExtensions), +) + +declare namespace bigint_ { + interface core extends bigint_.methods { + (u: this['_type'] | Unknown): u is this['_type'] + _type: bigint + tag: URI.bigint + get def(): this['_type'] + minimum?: bigint + maximum?: bigint + } + type Min + = [Self] extends [{ maximum: bigint }] + ? bigint_.between<[min: X, max: Self['maximum']]> + : bigint_.min + + type Max + = [Self] extends [{ minimum: bigint }] + ? bigint_.between<[min: Self['minimum'], max: X]> + : bigint_.max + + interface methods { + min(minimum: Min): bigint_.Min + max(maximum: Max): bigint_.Max + between( + minimum: Min, + maximum: Max + ): bigint_.between<[min: Min, max: Max]> + } + interface min extends bigint_ { minimum: Min } + interface max extends bigint_ { maximum: Max } + interface between extends bigint_ { minimum: Bounds[0], maximum: Bounds[1] } +} + diff --git a/packages/schema-core/src/schemas/boolean.ts b/packages/schema-core/src/schemas/boolean.ts new file mode 100644 index 00000000..c89adf4b --- /dev/null +++ b/packages/schema-core/src/schemas/boolean.ts @@ -0,0 +1,37 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { boolean_ as boolean } + +interface boolean_ extends boolean_.core { + //<%= Types %> +} + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +function BooleanSchema(src: unknown): src is boolean { return typeof src === 'boolean' } + +BooleanSchema.tag = URI.boolean +BooleanSchema.def = false + +const boolean_ = Object_assign( + BooleanSchema, + userDefinitions, +) as boolean_ + +Object_assign(boolean_, userExtensions) + +declare namespace boolean_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.boolean + _type: boolean + get def(): this['_type'] + } +} diff --git a/packages/schema-core/src/schemas/eq.ts b/packages/schema-core/src/schemas/eq.ts new file mode 100644 index 00000000..d701a0f6 --- /dev/null +++ b/packages/schema-core/src/schemas/eq.ts @@ -0,0 +1,41 @@ +import type { Mut, Mutable, SchemaOptions as Options, Unknown } from '@traversable/registry' +import { applyOptions, bindUserExtensions, _isPredicate, Object_assign, URI } from '@traversable/registry' + +export function eq>(value: V, options?: Options): eq> +export function eq(value: V, options?: Options): eq +export function eq(value: V, options?: Options): eq { + return eq.def(value, options) +} + +export interface eq extends eq.core { + //<%= Types %> +} + +export namespace eq { + export let userDefinitions: Record = { + //<%= Definitions %> + } + export function def(value: T, options?: Options): eq + /* v8 ignore next 1 */ + export function def(x: T, $?: Options): {} { + let userExtensions: Record = { + //<%= Extensions %> + } + const options = applyOptions($) + const predicate = _isPredicate(x) ? x : (y: unknown) => options.eq.equalsFn(x, y) + function EqSchema(src: unknown) { return predicate(src) } + EqSchema.tag = URI.eq + EqSchema.def = x + Object_assign(EqSchema, eq.userDefinitions) + return Object_assign(EqSchema, bindUserExtensions(EqSchema, userExtensions)) + } +} + +export declare namespace eq { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.eq + _type: V + get def(): V + } +} diff --git a/packages/schema-core/src/schemas/integer.ts b/packages/schema-core/src/schemas/integer.ts new file mode 100644 index 00000000..d3492929 --- /dev/null +++ b/packages/schema-core/src/schemas/integer.ts @@ -0,0 +1,103 @@ +import type { Bounds, Integer, Unknown } from '@traversable/registry' +import { + bindUserExtensions, + carryover, + Math_min, + Math_max, + Number_isSafeInteger, + Object_assign, + URI, + within, +} from '@traversable/registry' + + +export { integer } + +/** @internal */ +function boundedInteger(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & integer +function boundedInteger(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & integer +function boundedInteger(bounds: Bounds, carry?: {}): {} { + return Object_assign(function BoundedIntegerSchema(u: unknown) { + return integer(u) && within(bounds)(u) + }, carry, integer) +} + +interface integer extends integer.core { + //<%= Types %> +} + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +function IntegerSchema(src: unknown) { return Number_isSafeInteger(src) } +IntegerSchema.tag = URI.integer +IntegerSchema.def = 0 + +const integer = Object_assign( + IntegerSchema, + userDefinitions, +) as integer + +integer.min = function integerMin(minimum) { + return Object_assign( + boundedInteger({ gte: minimum }, carryover(this, 'minimum')), + { minimum }, + ) +} +integer.max = function integerMax(maximum) { + return Object_assign( + boundedInteger({ lte: maximum }, carryover(this, 'maximum')), + { maximum }, + ) +} +integer.between = function integerBetween( + min, + max, + minimum = Math_min(min, max), + maximum = Math_max(min, max), +) { + return Object_assign( + boundedInteger({ gte: minimum, lte: maximum }), + { minimum, maximum }, + ) +} + +Object_assign( + integer, + bindUserExtensions(integer, userExtensions), +) + +declare namespace integer { + interface core extends integer.methods { + (u: this['_type'] | Unknown): u is this['_type'] + _type: number + tag: URI.integer + get def(): this['_type'] + minimum?: number + maximum?: number + } + interface methods { + min>(minimum: Min): integer.Min + max>(maximum: Max): integer.Max + between, const Max extends Integer>( + minimum: Min, + maximum: Max + ): integer.between<[min: Min, max: Max]> + } + type Min + = [Self] extends [{ maximum: number }] + ? integer.between<[min: X, max: Self['maximum']]> + : integer.min + type Max + = [Self] extends [{ minimum: number }] + ? integer.between<[min: Self['minimum'], max: X]> + : integer.max + interface min extends integer { minimum: Min } + interface max extends integer { maximum: Max } + interface between extends integer { minimum: Bounds[0], maximum: Bounds[1] } +} diff --git a/packages/schema-core/src/schemas/intersect.ts b/packages/schema-core/src/schemas/intersect.ts new file mode 100644 index 00000000..a3f89c92 --- /dev/null +++ b/packages/schema-core/src/schemas/intersect.ts @@ -0,0 +1,50 @@ +import type { Unknown } from '@traversable/registry' +import { + _isPredicate, + bindUserExtensions, + intersect as intersect$, + isUnknown as isAny, + Object_assign, + URI, +} from '@traversable/registry' + +import type { Entry, IntersectType, Schema, SchemaLike } from '../namespace.js' + +export function intersect(...schemas: S): intersect +export function intersect }>(...schemas: S): intersect +export function intersect(...schemas: readonly unknown[]) { + return intersect.def(schemas) +} + +export interface intersect extends intersect.core { + //<%= Types %> +} + +export namespace intersect { + export let userDefinitions: Record = { + //<%= Definitions %> + } as intersect + export function def(xs: readonly [...T]): intersect + /* v8 ignore next 1 */ + export function def(xs: readonly unknown[]): {} { + let userExtensions: Record = { + //<%= Extensions %> + } + const predicate = xs.every(_isPredicate) ? intersect$(xs) : isAny + function IntersectSchema(src: unknown) { return predicate(src) } + IntersectSchema.tag = URI.intersect + IntersectSchema.def = xs + Object_assign(IntersectSchema, intersect.userDefinitions) + return Object_assign(IntersectSchema, bindUserExtensions(IntersectSchema, userExtensions)) + } +} + +export declare namespace intersect { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.intersect + get def(): S + _type: IntersectType + } + type type> = never | T +} diff --git a/packages/schema-core/src/schemas/never.ts b/packages/schema-core/src/schemas/never.ts new file mode 100644 index 00000000..a0077281 --- /dev/null +++ b/packages/schema-core/src/schemas/never.ts @@ -0,0 +1,37 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { never_ as never } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface never_ extends never_.core { + //<%= Types %> +} + +function NeverSchema(src: unknown): src is never { return false } +NeverSchema.tag = URI.never; +NeverSchema.def = void 0 as never + +const never_ = Object_assign( + NeverSchema, + userDefinitions, +) as never_ + +Object_assign(never_, userExtensions) + +export declare namespace never_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.never + _type: never + get def(): this['_type'] + } +} + diff --git a/packages/schema-core/src/schemas/null.ts b/packages/schema-core/src/schemas/null.ts new file mode 100644 index 00000000..befebeb5 --- /dev/null +++ b/packages/schema-core/src/schemas/null.ts @@ -0,0 +1,39 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { null_ as null, null_ } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface null_ extends null_.core { + //<%= Types %> +} + +function NullSchema(src: unknown): src is null { return src === null } +NullSchema.def = null +NullSchema.tag = URI.null + +const null_ = Object_assign( + NullSchema, + userDefinitions, +) as null_ + +Object_assign( + null_, + userExtensions, +) + +declare namespace null_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.null + _type: null + get def(): this['_type'] + } +} diff --git a/packages/schema-core/src/schemas/number.ts b/packages/schema-core/src/schemas/number.ts new file mode 100644 index 00000000..9bcd98a9 --- /dev/null +++ b/packages/schema-core/src/schemas/number.ts @@ -0,0 +1,142 @@ +import type { Bounds, Unknown } from '@traversable/registry' +import { + bindUserExtensions, + carryover, + Math_min, + Math_max, + Object_assign, + URI, + within, +} from '@traversable/registry' + + +export { number_ as number } + +interface number_ extends number_.core { + //<%= Types %> +} + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +function NumberSchema(src: unknown) { return typeof src === 'number' } +NumberSchema.tag = URI.number +NumberSchema.def = 0 + +const number_ = Object_assign( + NumberSchema, + userDefinitions, +) as number_ + +number_.min = function numberMin(minimum) { + return Object_assign( + boundedNumber({ gte: minimum }, carryover(this, 'minimum')), + { minimum }, + ) +} +number_.max = function numberMax(maximum) { + return Object_assign( + boundedNumber({ lte: maximum }, carryover(this, 'maximum')), + { maximum }, + ) +} +number_.moreThan = function numberMoreThan(exclusiveMinimum) { + return Object_assign( + boundedNumber({ gt: exclusiveMinimum }, carryover(this, 'exclusiveMinimum')), + { exclusiveMinimum }, + ) +} +number_.lessThan = function numberLessThan(exclusiveMaximum) { + return Object_assign( + boundedNumber({ lt: exclusiveMaximum }, carryover(this, 'exclusiveMaximum')), + { exclusiveMaximum }, + ) +} +number_.between = function numberBetween( + min, + max, + minimum = Math_min(min, max), + maximum = Math_max(min, max), +) { + return Object_assign( + boundedNumber({ gte: minimum, lte: maximum }), + { minimum, maximum }, + ) +} + +Object_assign( + number_, + bindUserExtensions(number_, userExtensions), +) + +function boundedNumber(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & number_ +function boundedNumber(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & number_ +function boundedNumber(bounds: Bounds, carry?: {}): {} { + return Object_assign(function BoundedNumberSchema(u: unknown) { + return typeof u === 'number' && within(bounds)(u) + }, carry, number_) +} + +declare namespace number_ { + interface core extends number_.methods { + (u: this['_type'] | Unknown): u is this['_type'] + _type: number + tag: URI.number + get def(): this['_type'] + minimum?: number + maximum?: number + exclusiveMinimum?: number + exclusiveMaximum?: number + } + interface methods { + min(minimum: Min): number_.Min + max(maximum: Max): number_.Max + moreThan(moreThan: Min): ExclusiveMin + lessThan(lessThan: Max): ExclusiveMax + between( + minimum: Min, + maximum: Max + ): number_.between<[min: Min, max: Max]> + } + type Min + = [Self] extends [{ exclusiveMaximum: number }] + ? number_.minStrictMax<[min: X, max: Self['exclusiveMaximum']]> + : [Self] extends [{ maximum: number }] + ? number_.between<[min: X, max: Self['maximum']]> + : number_.min + + type Max + = [Self] extends [{ exclusiveMinimum: number }] + ? number_.maxStrictMin<[Self['exclusiveMinimum'], X]> + : [Self] extends [{ minimum: number }] + ? number_.between<[min: Self['minimum'], max: X]> + : number_.max + + type ExclusiveMin + = [Self] extends [{ exclusiveMaximum: number }] + ? number_.strictlyBetween<[X, Self['exclusiveMaximum']]> + : [Self] extends [{ maximum: number }] + ? number_.maxStrictMin<[min: X, Self['maximum']]> + : number_.moreThan + + type ExclusiveMax + = [Self] extends [{ exclusiveMinimum: number }] + ? number_.strictlyBetween<[Self['exclusiveMinimum'], X]> + : [Self] extends [{ minimum: number }] + ? number_.minStrictMax<[Self['minimum'], min: X]> + : number_.lessThan + + interface min extends number_ { minimum: Min } + interface max extends number_ { maximum: Max } + interface moreThan extends number_ { exclusiveMinimum: Min } + interface lessThan extends number_ { exclusiveMaximum: Max } + interface between extends number_ { minimum: Bounds[0], maximum: Bounds[1] } + interface minStrictMax extends number_ { minimum: Bounds[0], exclusiveMaximum: Bounds[1] } + interface maxStrictMin extends number_ { maximum: Bounds[1], exclusiveMinimum: Bounds[0] } + interface strictlyBetween extends number_ { exclusiveMinimum: Bounds[0], exclusiveMaximum: Bounds[1] } +} diff --git a/packages/schema-core/src/schemas/object.ts b/packages/schema-core/src/schemas/object.ts new file mode 100644 index 00000000..42a396d1 --- /dev/null +++ b/packages/schema-core/src/schemas/object.ts @@ -0,0 +1,77 @@ +import type { Force, SchemaOptions as Options, Unknown } from '@traversable/registry' +import { + applyOptions, + Array_isArray, + bindUserExtensions, + has, + _isPredicate, + Object_assign, + Object_keys, + record as record$, + object as object$, + isAnyObject, + symbol, + URI, +} from '@traversable/registry' + +import type { Entry, Optional, Required, Schema, SchemaLike } from '../namespace.js' + +export { object_ as object } + +function object_< + S extends { [x: string]: Schema }, + T extends { [K in keyof S]: Entry } +>(schemas: S, options?: Options): object_ +function object_< + S extends { [x: string]: SchemaLike }, + T extends { [K in keyof S]: Entry } +>(schemas: S, options?: Options): object_ +function object_(schemas: S, options?: Options) { + return object_.def(schemas, options) +} + +interface object_ extends object_.core { + //<%= Types %> +} + +namespace object_ { + export let userDefinitions: Record = { + //<%= Definitions %> + } as object_ + export function def(xs: T, $?: Options, opt?: string[]): object_ + export function def(xs: T, $?: Options, opt?: string[]): object_ + /* v8 ignore next 1 */ + export function def(xs: { [x: string]: unknown }, $?: Options, opt_?: string[]): {} { + let userExtensions: Record = { + //<%= Extensions %> + } + const keys = Object_keys(xs) + const opt = Array_isArray(opt_) ? opt_ : keys.filter((k) => has(symbol.optional)(xs[k])) + const req = keys.filter((k) => !has(symbol.optional)(xs[k])) + const predicate = !record$(_isPredicate)(xs) ? isAnyObject : object$(xs, applyOptions($)) + function ObjectSchema(src: unknown) { return predicate(src) } + ObjectSchema.tag = URI.object + ObjectSchema.def = xs + ObjectSchema.opt = opt + ObjectSchema.req = req + Object_assign(ObjectSchema, userDefinitions) + return Object_assign(ObjectSchema, bindUserExtensions(ObjectSchema, userExtensions)) + } +} + +declare namespace object_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + _type: object_.type + tag: URI.object + get def(): S + opt: Optional // TODO: use object_.Opt? + req: Required // TODO: use object_.Req? + } + type Opt = symbol.optional extends keyof S[K] ? never : K + type Req = symbol.optional extends keyof S[K] ? K : never + type type = Force< + & { [K in keyof S as Opt]-?: S[K]['_type' & keyof S[K]] } + & { [K in keyof S as Req]+?: S[K]['_type' & keyof S[K]] } + > +} diff --git a/packages/schema-core/src/schemas/of.ts b/packages/schema-core/src/schemas/of.ts new file mode 100644 index 00000000..c02a9210 --- /dev/null +++ b/packages/schema-core/src/schemas/of.ts @@ -0,0 +1,47 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +import type { + Entry, + Guard, + Guarded, + SchemaLike, +} from '../namespace.js' + +export interface of extends of.core { + //<%= Types %> +} + +export function of(typeguard: S): Entry +export function of(typeguard: S): of +export function of(typeguard: (Guard) & { tag?: URI.inline, def?: Guard }) { + typeguard.def = typeguard + return Object_assign(typeguard, of.prototype) +} + +export namespace of { + export let userDefinitions: Record = { + //<%= Definitions %> + } + export let userExtensions: Record = { + //<%= Extensions %> + } + export function def(guard: T): of + /* v8 ignore next 6 */ + export function def(guard: T) { + function InlineSchema(src: unknown) { return guard(src) } + InlineSchema.tag = URI.inline + InlineSchema.def = guard + return InlineSchema + } +} + +export declare namespace of { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + _type: Guarded + tag: URI.inline + get def(): S + } + type type> = never | T +} diff --git a/packages/schema-core/src/schemas/optional.ts b/packages/schema-core/src/schemas/optional.ts new file mode 100644 index 00000000..0574d117 --- /dev/null +++ b/packages/schema-core/src/schemas/optional.ts @@ -0,0 +1,55 @@ +import type { Unknown } from '@traversable/registry' +import { + bindUserExtensions, + has, + _isPredicate, + optional as optional$, + Object_assign, + symbol, + URI, + isUnknown as isAny, +} from '@traversable/registry' + +import type { Entry, Schema, SchemaLike } from '../namespace.js' + +export function optional(schema: S): optional +export function optional(schema: S): optional> +export function optional(schema: S): optional { return optional.def(schema) } + +export interface optional extends optional.core { + //<%= Types %> +} + +export namespace optional { + export let userDefinitions: Record = { + //<%= Definitions %> + } + export function def(x: T): optional + export function def(x: T) { + let userExtensions: Record = { + //<%= Extensions %> + } + const predicate = _isPredicate(x) ? optional$(x) : isAny + function OptionalSchema(src: unknown) { return predicate(src) } + OptionalSchema.tag = URI.optional + OptionalSchema.def = x + OptionalSchema[symbol.optional] = 1 + Object_assign(OptionalSchema, { ...optional.userDefinitions, get def() { return x } }) + return Object_assign(OptionalSchema, bindUserExtensions(OptionalSchema, userExtensions)) + } + export const is + : (u: unknown) => u is optional + /* v8 ignore next 1 */ + = has('tag', (u) => u === URI.optional) +} + +export declare namespace optional { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.optional + _type: undefined | S['_type' & keyof S] + def: S + [symbol.optional]: number + } + export type type = never | T +} diff --git a/packages/schema-core/src/schemas/record.ts b/packages/schema-core/src/schemas/record.ts new file mode 100644 index 00000000..c4e54a9a --- /dev/null +++ b/packages/schema-core/src/schemas/record.ts @@ -0,0 +1,50 @@ +import type { Unknown } from '@traversable/registry' +import { + isAnyObject, + record as record$, + bindUserExtensions, + _isPredicate, + Object_assign, + URI, +} from '@traversable/registry' + +import type { Entry, Schema, SchemaLike } from '../namespace.js' + +export function record(schema: S): record +export function record(schema: S): record> +export function record(schema: Schema) { + return record.def(schema) +} + +export interface record extends record.core { + //<%= Types %> +} + +export namespace record { + export let userDefinitions: Record = { + //<%= Definitions %> + } + export function def(x: T): record + /* v8 ignore next 1 */ + export function def(x: unknown): {} { + let userExtensions: Record = { + //<%= Extensions %> + } + const predicate = _isPredicate(x) ? record$(x) : isAnyObject + function RecordSchema(src: unknown) { return predicate(src) } + RecordSchema.tag = URI.record + RecordSchema.def = x + Object_assign(RecordSchema, record.userDefinitions) + return Object_assign(RecordSchema, bindUserExtensions(RecordSchema, userExtensions)) + } +} + +export declare namespace record { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.record + get def(): S + _type: Record + } + export type type> = never | T +} diff --git a/packages/schema-core/src/schemas/string.ts b/packages/schema-core/src/schemas/string.ts new file mode 100644 index 00000000..4c66f2e4 --- /dev/null +++ b/packages/schema-core/src/schemas/string.ts @@ -0,0 +1,105 @@ +import type { Bounds, Integer, Unknown } from '@traversable/registry' +import { + bindUserExtensions, + carryover, + Math_min, + Math_max, + Object_assign, + URI, + within, +} from '@traversable/registry' + +export { string_ as string } + +/** @internal */ +function boundedString(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & string_ +function boundedString(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & string_ +function boundedString(bounds: Bounds, carry?: {}): {} { + return Object_assign(function BoundedStringSchema(u: unknown) { + return string_(u) && within(bounds)(u.length) + }, carry, string_) +} + +interface string_ extends string_.core { + //<%= Types %> +} + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +function StringSchema(src: unknown) { return typeof src === 'string' } +StringSchema.tag = URI.string +StringSchema.def = '' + +const string_ = Object_assign( + StringSchema, + userDefinitions, +) as string_ + +string_.min = function stringMinLength(minLength) { + return Object_assign( + boundedString({ gte: minLength }, carryover(this, 'minLength')), + { minLength }, + ) +} +string_.max = function stringMaxLength(maxLength) { + return Object_assign( + boundedString({ lte: maxLength }, carryover(this, 'maxLength')), + { maxLength }, + ) +} +string_.between = function stringBetween( + min, + max, + minLength = Math_min(min, max), + maxLength = Math_max(min, max)) { + return Object_assign( + boundedString({ gte: minLength, lte: maxLength }), + { minLength, maxLength }, + ) +} + +Object_assign( + string_, + bindUserExtensions(string_, userExtensions), +) + +declare namespace string_ { + interface core extends string_.methods { + (u: this['_type'] | Unknown): u is this['_type'] + _type: string + tag: URI.string + get def(): this['_type'] + } + interface methods { + minLength?: number + maxLength?: number + min>(minLength: Min): string_.Min + max>(maxLength: Max): string_.Max + between, const Max extends Integer>( + minLength: Min, + maxLength: Max + ): string_.between<[min: Min, max: Max]> + } + type Min + = [Self] extends [{ maxLength: number }] + ? string_.between<[min: Min, max: Self['maxLength']]> + : string_.min + + type Max + = [Self] extends [{ minLength: number }] + ? string_.between<[min: Self['minLength'], max: Max]> + : string_.max + + interface min extends string_ { minLength: Min } + interface max extends string_ { maxLength: Max } + interface between extends string_ { + minLength: Bounds[0] + maxLength: Bounds[1] + } +} diff --git a/packages/schema-core/src/schemas/symbol.ts b/packages/schema-core/src/schemas/symbol.ts new file mode 100644 index 00000000..6e192b3e --- /dev/null +++ b/packages/schema-core/src/schemas/symbol.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { symbol_ as symbol } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface symbol_ extends symbol_.core { + //<%= Types %> +} + +function SymbolSchema(src: unknown): src is symbol { return typeof src === 'symbol' } +SymbolSchema.tag = URI.symbol +SymbolSchema.def = Symbol() + +const symbol_ = Object_assign( + SymbolSchema, + userDefinitions, +) as symbol_ + +Object_assign(symbol_, userExtensions) + +declare namespace symbol_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.symbol + _type: symbol + get def(): this['_type'] + } +} diff --git a/packages/schema-core/src/schemas/tuple.ts b/packages/schema-core/src/schemas/tuple.ts new file mode 100644 index 00000000..f2b9f6bb --- /dev/null +++ b/packages/schema-core/src/schemas/tuple.ts @@ -0,0 +1,86 @@ +import type { + SchemaOptions as Options, + TypeError, + Unknown +} from '@traversable/registry' + +import { + Array_isArray, + bindUserExtensions, + getConfig, + has, + _isPredicate, + Object_assign, + parseArgs, + symbol, + tuple as tuple$, + URI, +} from '@traversable/registry' + +import type { + Entry, + FirstOptionalItem, + invalid, + Schema, + SchemaLike, + TupleType, + ValidateTuple +} from '../namespace.js' + +import type { optional } from './optional.js' + +export { tuple } + +function tuple }>(...schemas: tuple.validate): tuple, T>> +function tuple(...schemas: tuple.validate): tuple, S>> +function tuple }>(...args: [...schemas: tuple.validate, options: Options]): tuple, T>> +function tuple(...args: [...schemas: tuple.validate, options: Options]): tuple, S>> +function tuple }>(...schemas: tuple.validate): tuple, T>> +function tuple(...schemas: tuple.validate): tuple, S>> +function tuple(...args: [...SchemaLike[]] | [...SchemaLike[], Options]) { + return tuple.def(...parseArgs(getConfig().schema, args)) +} + +interface tuple extends tuple.core { + //<%= Types %> +} + +namespace tuple { + export let userDefinitions: Record = { + //<%= Definitions %> + } as tuple + export function def(xs: readonly [...T], $?: Options, opt_?: number): tuple + /* v8 ignore next 1 */ + export function def(xs: readonly unknown[], $: Options = getConfig().schema, opt_?: number): {} { + let userExtensions: Record = { + //<%= Extensions %> + } + const opt = opt_ || xs.findIndex(has(symbol.optional)) + const options = { + ...$, minLength: $.optionalTreatment === 'treatUndefinedAndOptionalAsTheSame' ? -1 : xs.findIndex(has(symbol.optional)) + } satisfies tuple.InternalOptions + const predicate = !xs.every(_isPredicate) ? Array_isArray : tuple$(xs, options) + function TupleSchema(src: unknown) { return predicate(src) } + TupleSchema.tag = URI.tuple + TupleSchema.def = xs + TupleSchema.opt = opt + Object_assign(TupleSchema, tuple.userDefinitions) + return Object_assign(TupleSchema, bindUserExtensions(TupleSchema, userExtensions)) + } +} + +declare namespace tuple { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.tuple + _type: TupleType + opt: FirstOptionalItem + def: S + } + type type> = never | T + type InternalOptions = { minLength?: number } + type validate = ValidateTuple> + + type from + = TypeError extends V[number] ? { [I in keyof V]: V[I] extends TypeError ? invalid> : V[I] } : T +} diff --git a/packages/schema-core/src/schemas/undefined.ts b/packages/schema-core/src/schemas/undefined.ts new file mode 100644 index 00000000..0115f168 --- /dev/null +++ b/packages/schema-core/src/schemas/undefined.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { undefined_ as undefined } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface undefined_ extends undefined_.core { + //<%= Types %> +} + +function UndefinedSchema(src: unknown): src is undefined { return src === void 0 } +UndefinedSchema.tag = URI.undefined +UndefinedSchema.def = void 0 as undefined + +const undefined_ = Object_assign( + UndefinedSchema, + userDefinitions, +) as undefined_ + +Object_assign(undefined_, userExtensions) + +declare namespace undefined_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.undefined + _type: undefined + get def(): this['_type'] + } +} diff --git a/packages/schema-core/src/schemas/union.ts b/packages/schema-core/src/schemas/union.ts new file mode 100644 index 00000000..1e3705ee --- /dev/null +++ b/packages/schema-core/src/schemas/union.ts @@ -0,0 +1,50 @@ +import type { Unknown } from '@traversable/registry' +import { + _isPredicate, + bindUserExtensions, + isUnknown as isAny, + Object_assign, + union as union$, + URI, +} from '@traversable/registry' + +import type { Entry, Schema, SchemaLike } from '../namespace.js' + +export function union(...schemas: S): union +export function union }>(...schemas: S): union +export function union(...schemas: unknown[]) { + return union.def(schemas) +} + +export interface union extends union.core { + //<%= Types %> +} + +export namespace union { + export let userDefinitions: Record = { + //<%= Definitions %> + } as Partial> + export function def(xs: T): union + /* v8 ignore next 1 */ + export function def(xs: unknown[]) { + let userExtensions: Record = { + //<%= Extensions %> + } + const predicate = xs.every(_isPredicate) ? union$(xs) : isAny + function UnionSchema(src: unknown): src is unknown { return predicate(src) } + UnionSchema.tag = URI.union + UnionSchema.def = xs + Object_assign(UnionSchema, union.userDefinitions) + return Object_assign(UnionSchema, bindUserExtensions(UnionSchema, userExtensions)) + } +} + +export declare namespace union { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.union + _type: union.type + get def(): S + } + type type = never | T +} diff --git a/packages/schema-core/src/schemas/unknown.ts b/packages/schema-core/src/schemas/unknown.ts new file mode 100644 index 00000000..0a190db3 --- /dev/null +++ b/packages/schema-core/src/schemas/unknown.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { unknown_ as unknown } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface unknown_ extends unknown_.core { + //<%= Types %> +} + +function UnknownSchema(src: unknown): src is unknown { return true } +UnknownSchema.tag = URI.unknown +UnknownSchema.def = void 0 as unknown + +const unknown_ = Object_assign( + UnknownSchema, + userDefinitions, +) as unknown_ + +Object_assign(unknown_, userExtensions) + +declare namespace unknown_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.unknown + _type: unknown + get def(): this['_type'] + } +} diff --git a/packages/schema-core/src/schemas/void.ts b/packages/schema-core/src/schemas/void.ts new file mode 100644 index 00000000..d178213e --- /dev/null +++ b/packages/schema-core/src/schemas/void.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { void_ as void, void_ } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface void_ extends void_.core { + //<%= Types %> +} + +function VoidSchema(src: unknown): src is void { return src === void 0 } +VoidSchema.tag = URI.void +VoidSchema.def = void 0 as void + +const void_ = Object_assign( + VoidSchema, + userDefinitions, +) as void_ + +Object_assign(void_, userExtensions) + +declare namespace void_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.void + _type: void + get def(): this['_type'] + } +} diff --git a/packages/schema-core/src/types.ts b/packages/schema-core/src/types.ts new file mode 100644 index 00000000..3551b13f --- /dev/null +++ b/packages/schema-core/src/types.ts @@ -0,0 +1,137 @@ +import type { TypeError, URI, symbol } from '@traversable/registry' +import type { unknown } from './schemas/unknown.js' +import type { of } from './schemas/of.js' +import type { nonnullable } from './nonnullable.js' +import type { OPT, REQ } from './label.js' +import type { optional } from './schemas/optional.js' + +export interface top { tag: URI.top, readonly _type: unknown, def: this['_type'] } +export interface bottom { tag: URI.bottom, readonly _type: never, def: this['_type'], [symbol.optional]: number } +export interface invalid<_Err> extends TypeError<''> { tag: URI.never } +export type InvalidItem = never | TypeError<'A required element cannot follow an optional element.'> +export type $ = [keyof S] extends [never] ? unknown : S +export type Entry + = S extends { def: unknown } ? S + : S extends Guard ? of + : S extends globalThis.BooleanConstructor ? nonnullable.core + : S extends (() => infer _ extends boolean) + ? BoolLookup[`${_}`] + : S + +export type Source = T extends (_: infer S) => unknown ? S : unknown +export type Guarded = never | S extends (_: any) => _ is infer T ? T : S +export type Target = S extends Guard ? T : S extends Predicate ? T : never +export type SchemaLike = Schema | Predicate +export type Guard = { (u: unknown): u is T } +export interface Typeguard { (u: unknown): u is this['_type']; readonly _type: T } +export type { TypePredicate_ as TypePredicate } +type TypePredicate_ = never | TypePredicate<[I, O]> +interface TypePredicate { + (u: T[0]): u is T[1] +} + +export interface LowerBound { + (u: unknown): u is any + tag: string + def?: unknown + _type?: T +} + +export interface UnknownSchema { + (u: unknown): u is any + tag: string + def: unknown + _type: unknown +} + +export interface Schema { + tag?: any + def?: Fn['def'] + _type?: Fn['_type'] + (u: unknown): u is this['_type'] +} + +export interface Predicate { + (value: T): boolean + (value?: T): boolean +} + +export type ValidateTuple< + T extends readonly unknown[], + LowerBound = optional, + V = ValidateOptionals<[...T], LowerBound>, +> = [V] extends [['ok']] ? T : V + +export type ValidateOptionals + = LowerBound extends S[number] + ? S extends [infer H, ...infer T] + ? LowerBound extends Partial + ? T[number] extends LowerBound + ? ['ok'] + : [...Acc, H, ...{ [Ix in keyof T]: T[Ix] extends LowerBound ? T[Ix] : InvalidItem }] + : ValidateOptionals + : ['ok'] + : ['ok'] + + +export type Optional = never | + string extends K ? string + : K extends K ? S[K] extends bottom | { [symbol.optional]: any } ? K + : never + : never + +export type Required = never | + string extends K ? string + : K extends K ? S[K] extends bottom | { [symbol.optional]: any } ? never + : K + : never + +export type IntersectType + = Todo extends readonly [infer H, ...infer T] + ? IntersectType + : Out + +export type TupleType> + = [Opt] extends [never] ? { [ix in keyof S]: S[ix]['_type' & keyof S[ix]] } + : S extends readonly [infer Head, ...infer Tail] + ? symbol.optional extends keyof Head ? Label< + { [ix in keyof Out]: Out[ix]['_type' & keyof Out[ix]] }, + { [ix in keyof S]: S[ix]['_type' & keyof S[ix]] } + > + : TupleType + : { [ix in keyof S]: S[ix]['_type' & keyof S[ix]] } + +export type FirstOptionalItem + = S extends readonly [infer H, ...infer T] + ? symbol.optional extends keyof H ? Offset['length'] + : FirstOptionalItem + : never + +export type BoolLookup = never | { + true: top + false: bottom + boolean: unknown.core +} + +export type Label< + Req extends readonly any[], + Opt extends readonly any[] +> = [ + ...Label.Required, + /* @ts-expect-error */ + ...Label.Optional, + ] + +export declare namespace Label { + type Required< + S extends readonly unknown[], + _ = REQ[S['length'] & keyof REQ] + > = { [I in keyof _]: S[I & keyof S] } + + type Optional< + Offset extends number, + Base extends any[], + /* @ts-expect-error */ + Start = OPT[Offset][Base['length']] + > = { [I in keyof Start]: Base[I & keyof Base] } +} diff --git a/packages/schema/src/utils.ts b/packages/schema-core/src/utils.ts similarity index 91% rename from packages/schema/src/utils.ts rename to packages/schema-core/src/utils.ts index f6b97948..f92b7e0a 100644 --- a/packages/schema/src/utils.ts +++ b/packages/schema-core/src/utils.ts @@ -1,11 +1,11 @@ import { __get as get_ } from '@traversable/registry' -import type * as t from './schema.js' +import type { Schema } from './types.js' export { get } function get< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], /* @ts-expect-error */ K2 extends keyof S['def'][K1]['def'], @@ -28,7 +28,7 @@ function get< >(schema: S, ...path: [K1, K2, K3, K4, K5, K6, K7, K8, K9]): V function get< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], /* @ts-expect-error */ K2 extends keyof S['def'][K1]['def'], @@ -49,7 +49,7 @@ function get< >(schema: S, ...path: [K1, K2, K3, K4, K5, K6, K7, K8]): V function get< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], /* @ts-expect-error */ K2 extends keyof S['def'][K1]['def'], @@ -68,7 +68,7 @@ function get< >(schema: S, ...path: [K1, K2, K3, K4, K5, K6, K7]): V function get< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], /* @ts-expect-error */ K2 extends keyof S['def'][K1]['def'], @@ -85,7 +85,7 @@ function get< >(schema: S, ...path: [K1, K2, K3, K4, K5, K6]): V function get< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], /* @ts-expect-error */ K2 extends keyof S['def'][K1]['def'], @@ -100,7 +100,7 @@ function get< >(schema: S, ...path: [K1, K2, K3, K4, K5]): V function get< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], /* @ts-expect-error */ K2 extends keyof S['def'][K1]['def'], @@ -113,7 +113,7 @@ function get< >(schema: S, ...path: [K1, K2, K3, K4]): V function get< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], /* @ts-expect-error */ K2 extends keyof S['def'][K1]['def'], @@ -124,7 +124,7 @@ function get< >(schema: S, ...path: [K1, K2, K3]): V function get< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], /* @ts-expect-error */ K2 extends keyof S['def'][K1]['def'], @@ -133,28 +133,28 @@ function get< >(schema: S, ...path: [K1, K2]): V function get< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], V = S['def'][K1] >(schema: S, ...key: [K1]): V function get< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], V = S['def'][K1] >(schema: S, ...path: [K1]): V function get< - S extends t.Schema, + S extends Schema, >(schema: S, ...path: []): S -function get(schema: S, ...path: (keyof any)[]) { +function get(schema: S, ...path: (keyof any)[]) { return get_(schema, path.flatMap((segment) => ['def', segment])) } export { get$ } function get$< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], K2 extends keyof S['def'][K1], K3 extends keyof S['def'][K1][K2], @@ -168,7 +168,7 @@ function get$< >(schema: S, ...path: [K1, K2, K3, K4, K5, K6, K7, K8, K9]): V function get$< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], K2 extends keyof S['def'][K1], K3 extends keyof S['def'][K1][K2], @@ -181,7 +181,7 @@ function get$< >(schema: S, ...path: [K1, K2, K3, K4, K5, K6, K7, K8]): V function get$< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], K2 extends keyof S['def'][K1], K3 extends keyof S['def'][K1][K2], @@ -193,7 +193,7 @@ function get$< >(schema: S, ...path: [K1, K2, K3, K4, K5, K6, K7]): V function get$< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], K2 extends keyof S['def'][K1], K3 extends keyof S['def'][K1][K2], @@ -204,7 +204,7 @@ function get$< >(schema: S, ...path: [K1, K2, K3, K4, K5, K6]): V function get$< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], K2 extends keyof S['def'][K1], K3 extends keyof S['def'][K1][K2], @@ -214,7 +214,7 @@ function get$< >(schema: S, ...path: [K1, K2, K3, K4, K5]): V function get$< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], K2 extends keyof S['def'][K1], K3 extends keyof S['def'][K1][K2], @@ -223,7 +223,7 @@ function get$< >(schema: S, ...path: [K1, K2, K3, K4]): V function get$< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], K2 extends keyof S['def'][K1], K3 extends keyof S['def'][K1][K2], @@ -231,15 +231,15 @@ function get$< >(schema: S, ...path: [K1, K2, K3]): V function get$< - S extends t.Schema, + S extends Schema, K1 extends keyof S['def'], K2 extends keyof S['def'][K1], V = S['def'][K1][K2] >(schema: S, ...path: [K1, K2]): V -function get$(schema: S, ...path: [K1]): V -function get$(schema: S, ...path: []): S +function get$(schema: S, ...path: [K1]): V +function get$(schema: S, ...path: []): S /// impl. -function get$(schema: S, ...path: (keyof any)[]) { +function get$(schema: S, ...path: (keyof any)[]) { return get_(schema, path.length === 0 ? [] : ['def', ...path]) } diff --git a/packages/schema-core/src/version.ts b/packages/schema-core/src/version.ts new file mode 100644 index 00000000..660ff1ca --- /dev/null +++ b/packages/schema-core/src/version.ts @@ -0,0 +1,3 @@ +import pkg from './__generated__/__manifest__.js' +export const VERSION = `${pkg.name}@${pkg.version}` as const +export type VERSION = typeof VERSION diff --git a/packages/schema/test/bounded.test.ts b/packages/schema-core/test/bounded.test.ts similarity index 80% rename from packages/schema/test/bounded.test.ts rename to packages/schema-core/test/bounded.test.ts index a5637cf6..ae6a2fc5 100644 --- a/packages/schema/test/bounded.test.ts +++ b/packages/schema-core/test/bounded.test.ts @@ -1,59 +1,8 @@ import * as vi from 'vitest' -import { t, __within as within, __withinBig as withinBig } from '@traversable/schema' +import { t } from '@traversable/schema-core' import { fc, test } from '@fast-check/vitest' -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema/bounded❳', () => { - vi.it('〖⛳️〗‹ ❲within❳', () => { - // SUCCESS - vi.assert.isTrue(within({ gt: 0 })(1)) - vi.assert.isTrue(within({ gt: 0, lt: 2 })(1)) - vi.assert.isTrue(within({ lt: 2 })(1)) - // FAILURE - vi.assert.isFalse(within({ gt: 0 })(0)) - vi.assert.isFalse(within({ gt: 0, lt: 0 })(0)) - vi.assert.isFalse(within({ lt: 0 })(0)) - /* @ts-expect-error */ - vi.assert.throws(() => within({ gt: '' })(0.5)) - }) - - vi.it('〖⛳️〗‹ ❲withinBig❳', () => { - // SUCCESS - vi.assert.isTrue(withinBig({})(1)) - vi.assert.isTrue(withinBig({ lte: 1 })(1)) - vi.assert.isTrue(withinBig({ gte: 1 })(1)) - vi.assert.isTrue(withinBig({ lt: 2 })(1)) - vi.assert.isTrue(withinBig({ gt: 0 })(1)) - vi.assert.isTrue(withinBig({ gt: 0, lt: 2 })(1)) - vi.assert.isTrue(withinBig({ gt: 0, lte: 1 })(1)) - vi.assert.isTrue(withinBig({ gte: 0, lt: 2 })(1)) - vi.assert.isTrue(withinBig({ gte: 0, lte: 1 })(1)) - vi.assert.isTrue(withinBig({ gt: 0, lt: 2, gte: 1 })(1)) - vi.assert.isTrue(withinBig({ gt: 0, lte: 1, gte: 1 })(1)) - vi.assert.isTrue(withinBig({ gte: 1, lt: 2, lte: 1 })(1)) - vi.assert.isTrue(withinBig({ gte: 1, lte: 1, lt: 2 })(1)) - vi.assert.isTrue(withinBig({ gte: 1, lte: 1, gt: 0 })(1)) - vi.assert.isTrue(withinBig({ gte: 1, lt: 2, gt: 0 })(1)) - vi.assert.isTrue(withinBig({ gte: 1, lte: 1, lt: 2, gt: 0 })(1)) - // FAILURE - vi.assert.isFalse(withinBig({ lte: 0 })(1)) - vi.assert.isFalse(withinBig({ gte: 1 })(-1)) - vi.assert.isFalse(withinBig({ lt: 0 })(1)) - vi.assert.isFalse(withinBig({ gt: 2 })(-1)) - vi.assert.isFalse(withinBig({ gt: 0, lt: 2 })(-1)) - vi.assert.isFalse(withinBig({ gt: 0, lte: 1 })(-1)) - vi.assert.isFalse(withinBig({ gte: 0, lt: 2 })(-1)) - vi.assert.isFalse(withinBig({ gte: 0, lte: 1 })(-1)) - vi.assert.isFalse(withinBig({ gt: 0, lt: 2, gte: 1 })(-1)) - vi.assert.isFalse(withinBig({ gt: 0, lte: 1, gte: 1 })(-1)) - vi.assert.isFalse(withinBig({ gte: 1, lt: 2, lte: 1 })(-1)) - vi.assert.isFalse(withinBig({ gte: 1, lte: 1, lt: 2 })(-1)) - vi.assert.isFalse(withinBig({ gte: 1, lte: 1, gt: 0 })(-1)) - vi.assert.isFalse(withinBig({ gte: 1, lt: 2, gt: 0 })(-1)) - vi.assert.isFalse(withinBig({ gte: 1, lte: 1, lt: 2, gt: 0 })(-1)) - /* @ts-expect-error */ - vi.assert.throws(() => withinBig({ gt: '' })(0.5)) - }) - +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core/bounded❳', () => { vi.describe('〖⛳️〗‹‹ ❲t.integer❳', () => { vi.it('〖⛳️〗‹ ❲t.integer.min(x).max(y)❳', () => { let ex_01 = t.integer.min(0).max(1) diff --git a/packages/schema-core/test/combinators.test.ts b/packages/schema-core/test/combinators.test.ts new file mode 100644 index 00000000..9e3c3b98 --- /dev/null +++ b/packages/schema-core/test/combinators.test.ts @@ -0,0 +1,69 @@ +import * as vi from 'vitest' +import { t } from '@traversable/schema-core' +import { fc, test } from '@fast-check/vitest' +import * as Seed from './seed.js' + +/** + * (go: fc.LetrecTypedTie) => { + * + * never: Arbitrary<"@traversable/schema-core/URI::never">; + * any: Arbitrary<"@traversable/schema-core/URI::any">; + * unknown: Arbitrary<"@traversable/schema-core/URI::unknown">; + * void: Arbitrary<"@traversable/schema-core/URI::void">; + * null: Arbitrary<"@traversable/schema-core/URI::null">; + * undefined: Arbitrary<"@traversable/schema-core/URI::undefined">; + * symbol: Arbitrary<"@traversable/schema-core/URI::symbol">; + * boolean: Arbitrary<"@traversable/schema-core/URI::boolean">; + * bigint: Arbitrary<"@traversable/schema-core/URI::bigint">; + * number: Arbitrary<"@traversable/schema-core/URI::number">; + * string: Arbitrary<"@traversable/schema-core/URI::string">; + * eq: Arbitrary<["@traversable/schema-core/URI::eq", Fixpoint]>; + * optional: Arbitrary<["@traversable/schema-core/URI::optional", "@traversable/schema-core/URI::never" | "@traversable/schema-core/URI::any" | "@traversable/schema-core/URI::unknown" | "@traversable/schema-core/URI::void" | "@traversable/schema-core/URI::null" | "@traversable/schema-core/URI::undefined" | "@traversable/schema-core/URI::symbol" | "@traversable/schema-core/URI::boolean" | "@traversable/schema-core/URI::bigint" | "@traversable/schema-core/URI::number" | "@traversable/schema-core/URI::string" | [tag: "@traversable/schema-core/URI::object", seed: [k: string, Fixpoint][]] | [tag: "@traversable/schema-core/URI::eq", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::array", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::record", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::optional", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::tuple", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::union", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::intersect", seed: readonly Fixpoint[]]]>; + * array: Arbitrary<["@traversable/schema-core/URI::array", "@traversable/schema-core/URI::never" | "@traversable/schema-core/URI::any" | "@traversable/schema-core/URI::unknown" | "@traversable/schema-core/URI::void" | "@traversable/schema-core/URI::null" | "@traversable/schema-core/URI::undefined" | "@traversable/schema-core/URI::symbol" | "@traversable/schema-core/URI::boolean" | "@traversable/schema-core/URI::bigint" | "@traversable/schema-core/URI::number" | "@traversable/schema-core/URI::string" | [tag: "@traversable/schema-core/URI::object", seed: [k: string, Fixpoint][]] | [tag: "@traversable/schema-core/URI::eq", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::array", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::record", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::optional", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::tuple", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::union", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::intersect", seed: readonly Fixpoint[]]]>; + * record: Arbitrary<["@traversable/schema-core/URI::record", "@traversable/schema-core/URI::never" | "@traversable/schema-core/URI::any" | "@traversable/schema-core/URI::unknown" | "@traversable/schema-core/URI::void" | "@traversable/schema-core/URI::null" | "@traversable/schema-core/URI::undefined" | "@traversable/schema-core/URI::symbol" | "@traversable/schema-core/URI::boolean" | "@traversable/schema-core/URI::bigint" | "@traversable/schema-core/URI::number" | "@traversable/schema-core/URI::string" | [tag: "@traversable/schema-core/URI::object", seed: [k: string, Fixpoint][]] | [tag: "@traversable/schema-core/URI::eq", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::array", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::record", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::optional", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::tuple", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::union", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::intersect", seed: readonly Fixpoint[]]]>; + * tuple: Arbitrary<["@traversable/schema-core/URI::tuple", ("@traversable/schema-core/URI::never" | "@traversable/schema-core/URI::any" | "@traversable/schema-core/URI::unknown" | "@traversable/schema-core/URI::void" | "@traversable/schema-core/URI::null" | "@traversable/schema-core/URI::undefined" | "@traversable/schema-core/URI::symbol" | "@traversable/schema-core/URI::boolean" | "@traversable/schema-core/URI::bigint" | "@traversable/schema-core/URI::number" | "@traversable/schema-core/URI::string" | [tag: "@traversable/schema-core/URI::object", seed: [k: string, Fixpoint][]] | [tag: "@traversable/schema-core/URI::eq", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::array", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::record", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::optional", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::tuple", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::union", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::intersect", seed: readonly Fixpoint[]])[]]>; + * object: Arbitrary<["@traversable/schema-core/URI::object", [k: string, v: "@traversable/schema-core/URI::never" | "@traversable/schema-core/URI::any" | "@traversable/schema-core/URI::unknown" | "@traversable/schema-core/URI::void" | "@traversable/schema-core/URI::null" | "@traversable/schema-core/URI::undefined" | "@traversable/schema-core/URI::symbol" | "@traversable/schema-core/URI::boolean" | "@traversable/schema-core/URI::bigint" | "@traversable/schema-core/URI::number" | "@traversable/schema-core/URI::string" | [tag: "@traversable/schema-core/URI::object", seed: [k: string, Fixpoint][]] | [tag: "@traversable/schema-core/URI::eq", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::array", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::record", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::optional", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::tuple", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::union", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::intersect", seed: readonly Fixpoint[]]][]]>; + * union: Arbitrary<["@traversable/schema-core/URI::union", ("@traversable/schema-core/URI::never" | "@traversable/schema-core/URI::any" | "@traversable/schema-core/URI::unknown" | "@traversable/schema-core/URI::void" | "@traversable/schema-core/URI::null" | "@traversable/schema-core/URI::undefined" | "@traversable/schema-core/URI::symbol" | "@traversable/schema-core/URI::boolean" | "@traversable/schema-core/URI::bigint" | "@traversable/schema-core/URI::number" | "@traversable/schema-core/URI::string" | [tag: "@traversable/schema-core/URI::object", seed: [k: string, Fixpoint][]] | [tag: "@traversable/schema-core/URI::eq", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::array", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::record", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::optional", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::tuple", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::union", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::intersect", seed: readonly Fixpoint[]])[]]>; + * intersect: Arbitrary<["@traversable/schema-core/URI::intersect", ("@traversable/schema-core/URI::never" | "@traversable/schema-core/URI::any" | "@traversable/schema-core/URI::unknown" | "@traversable/schema-core/URI::void" | "@traversable/schema-core/URI::null" | "@traversable/schema-core/URI::undefined" | "@traversable/schema-core/URI::symbol" | "@traversable/schema-core/URI::boolean" | "@traversable/schema-core/URI::bigint" | "@traversable/schema-core/URI::number" | "@traversable/schema-core/URI::string" | [tag: "@traversable/schema-core/URI::object", seed: [k: string, Fixpoint][]] | [tag: "@traversable/schema-core/URI::eq", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::array", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::record", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::optional", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::tuple", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::union", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::intersect", seed: readonly Fixpoint[]])[]]>; + * tree: Arbitrary<"@traversable/schema-core/URI::never" | "@traversable/schema-core/URI::any" | "@traversable/schema-core/URI::unknown" | "@traversable/schema-core/URI::void" | "@traversable/schema-core/URI::null" | "@traversable/schema-core/URI::undefined" | "@traversable/schema-core/URI::symbol" | "@traversable/schema-core/URI::boolean" | "@traversable/schema-core/URI::bigint" | "@traversable/schema-core/URI::number" | "@traversable/schema-core/URI::string" | [tag: "@traversable/schema-core/URI::object", seed: [k: string, Fixpoint][]] | [tag: "@traversable/schema-core/URI::eq", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::array", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::record", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::optional", seed: Fixpoint] | [tag: "@traversable/schema-core/URI::tuple", seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::union", * seed: readonly Fixpoint[]] | [tag: "@traversable/schema-core/URI::intersect", seed: readonly Fixpoint[]]>; + * } + */ + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳', () => { + const natural = t.filter(t.integer, x => x >= 0) + const varchar = t.filter(t.string)(x => 0x100 >= x.length) + + const arbitrary = fc.letrec(Seed.seed()).tree.chain((seed) => fc.constant([ + Seed.toSchema(seed), + fc.constant(Seed.toJson(seed)), + ] satisfies [any, any])) + + vi.describe('〖⛳️〗‹‹ ❲t.filter❳', () => { + test.prop([fc.nat()])( + '〖⛳️〗‹ ❲t.filter(t.integer, q)❳: returns true when `q` is satisied', + (x) => vi.assert.isTrue(natural(x)) + ) + test.prop([fc.nat().map((x) => -x).filter((x) => x !== 0)])( + '〖⛳️〗‹ ❲t.filter(t.integer, q)❳: returns false when `q` is not satisfied', + (x) => vi.assert.isFalse(natural(x)) + ) + + test.prop([fc.string({ maxLength: 0x100 })])( + '〖⛳️〗‹ ❲t.filter(t.string, q)❳: returns true when `q` is satisfied', + (x) => vi.assert.isTrue(varchar(x)) + ) + test.prop([fc.string({ minLength: 0x101 })], {})( + '〖⛳️〗‹ ❲t.filter(t.string, q)❳: returns false when `q` is not satisfied', + (x) => vi.assert.isFalse(varchar(x)) + ) + + test.prop([arbitrary, fc.func(fc.boolean())])( + /** + * See also: + * https://www.wisdom.weizmann.ac.il/~/oded/VO/mono1.pdf + */ + '〖⛳️〗‹ ❲t.filter(s, q)❳: is monotone cf. s ∩ q', + ([s, x], q) => vi.assert.equal(t.filter(s, q)(x), s(x) && q(x)) + ) + }) +}) diff --git a/packages/schema/test/enum.test.ts b/packages/schema-core/test/enum.test.ts similarity index 57% rename from packages/schema/test/enum.test.ts rename to packages/schema-core/test/enum.test.ts index 28d2f47c..4412c627 100644 --- a/packages/schema/test/enum.test.ts +++ b/packages/schema-core/test/enum.test.ts @@ -1,7 +1,7 @@ import * as vi from 'vitest' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳', () => { vi.it('〖⛳️〗‹ ❲t.enum❳', () => { vi.assert.isFalse(t.enum()(1)) vi.assert.isFalse(t.enum.def([])(1)) diff --git a/packages/schema/test/equals.bench.ts b/packages/schema-core/test/equals.bench.ts similarity index 99% rename from packages/schema/test/equals.bench.ts rename to packages/schema-core/test/equals.bench.ts index e4ca99bb..b15f9528 100644 --- a/packages/schema/test/equals.bench.ts +++ b/packages/schema-core/test/equals.bench.ts @@ -2,7 +2,7 @@ import * as fc from 'fast-check' import * as vi from 'vitest' import lodashIsEqual from 'lodash.isequal' import * as NodeJSUtil from 'node:util' -import { Equal } from '@traversable/schema' +import { Equal } from '@traversable/schema-core' const isEqual = Equal.deep diff --git a/packages/schema/test/equals.test.ts b/packages/schema-core/test/equals.test.ts similarity index 87% rename from packages/schema/test/equals.test.ts rename to packages/schema-core/test/equals.test.ts index 1c426238..72edf1ce 100644 --- a/packages/schema/test/equals.test.ts +++ b/packages/schema-core/test/equals.test.ts @@ -2,9 +2,9 @@ import * as vi from 'vitest' import * as NodeJSUtil from 'node:util' import { fc, test } from '@fast-check/vitest' -import { Equal } from '@traversable/schema' +import { Equal } from '@traversable/schema-core' -vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema/equal❳', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core/equal❳', () => { test.prop([fc.anything(), fc.anything()], { // numRuns: 100_000, })('〖⛳️〗› ❲Equal.deep❳: oracle (NodeJSUtil.isDeepStrictEqual)', (xs, ys) => { diff --git a/packages/schema/test/has.test.ts b/packages/schema-core/test/has.test.ts similarity index 72% rename from packages/schema/test/has.test.ts rename to packages/schema-core/test/has.test.ts index cda17d80..7ae39238 100644 --- a/packages/schema/test/has.test.ts +++ b/packages/schema-core/test/has.test.ts @@ -2,9 +2,9 @@ import * as vi from 'vitest' import { fc, test } from '@fast-check/vitest' import { __fromPath as fromPath } from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' -vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema❳', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳', () => { let leaf = Symbol.for('leaf') test.prop([fc.array(fc.string())])('〖⛳️〗‹ ❲t.has❳', (path) => { diff --git a/packages/schema/test/inline.test.ts b/packages/schema-core/test/inline.test.ts similarity index 97% rename from packages/schema/test/inline.test.ts rename to packages/schema-core/test/inline.test.ts index ad48aad1..eb165302 100644 --- a/packages/schema/test/inline.test.ts +++ b/packages/schema-core/test/inline.test.ts @@ -1,7 +1,9 @@ import * as vi from 'vitest' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳: support for native type predicates', () => { +let xs = t.optional((u): u is Record<`${boolean}`, boolean> => true) + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳: support for native type predicates', () => { vi.it('〖⛳️〗‹‹‹ ❲t.optional❳: supports native type predicates', () => { vi.assertType>>(t.optional((u): u is Record<`${boolean}`, boolean> => true).def) vi.assertType<{ b: string } | undefined>(t.optional(t.object({ b: t.string }))._type) @@ -288,5 +290,4 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳: support for native })._type ) }) - }) diff --git a/packages/schema-core/test/intersect.test.ts b/packages/schema-core/test/intersect.test.ts new file mode 100644 index 00000000..a3d7a683 --- /dev/null +++ b/packages/schema-core/test/intersect.test.ts @@ -0,0 +1,8 @@ +import * as vi from 'vitest' +import { t } from '@traversable/schema-core' + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳', () => { + vi.it('〖⛳️〗‹ ❲t.intersect❳: if t.intersect.def receives a non-function, it returns `constTrue`', () => { + vi.assert.isTrue(t.intersect.def([1])(1)) + }) +}) diff --git a/packages/schema-core/test/nonnullable.test.ts b/packages/schema-core/test/nonnullable.test.ts new file mode 100644 index 00000000..cf9f3958 --- /dev/null +++ b/packages/schema-core/test/nonnullable.test.ts @@ -0,0 +1,15 @@ +import * as vi from 'vitest' +import { t } from '@traversable/schema-core' +import { fc, test } from '@fast-check/vitest' + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳', () => { + vi.it('〖⛳️〗‹ ❲t.nonnullable❳: failure case', () => { + vi.assert.isFalse(t.nonnullable(null)) + vi.assert.isFalse(t.nonnullable(void 0)) + }) + test.prop([fc.anything().filter((_) => _ != null)], {})( + '〖⛳️〗‹ ❲t.nonnullable❳: success case', + (_) => vi.assert.isTrue(t.nonnullable(_)) + ) +}) + diff --git a/packages/schema-core/test/optional.test.ts b/packages/schema-core/test/optional.test.ts new file mode 100644 index 00000000..7014bac1 --- /dev/null +++ b/packages/schema-core/test/optional.test.ts @@ -0,0 +1,16 @@ +import * as vi from 'vitest' +import { URI } from '@traversable/registry' +import { t } from '@traversable/schema-core' + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳', () => { + vi.it('〖⛳️〗‹ ❲t.optional❳: t.optional.is', () => { + vi.assert.isTrue(t.optional.is(t.optional(t.string))) + vi.assert.isTrue(t.optional.is({ tag: URI.optional })) + + vi.assert.isFalse(t.optional.is({ tag: URI.string })) + vi.assert.isFalse(t.optional.is({ tag: 1 })) + vi.assert.isFalse(t.optional.is(1)) + vi.assert.isFalse(t.optional.is(t.undefined)) + vi.assert.isFalse(t.optional.is(t.void)) + }) +}) diff --git a/packages/schema/test/predicates.test.ts b/packages/schema-core/test/predicates.test.ts similarity index 99% rename from packages/schema/test/predicates.test.ts rename to packages/schema-core/test/predicates.test.ts index a791a18c..b47209b2 100644 --- a/packages/schema/test/predicates.test.ts +++ b/packages/schema-core/test/predicates.test.ts @@ -2,10 +2,10 @@ import * as vi from 'vitest' import { fc, test } from '@fast-check/vitest' import { symbol } from '@traversable/registry' -import { Predicate, Predicate as q, t } from '@traversable/schema' +import { Predicate, Predicate as q, t } from '@traversable/schema-core' import * as Seed from './seed.js' -vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema❳', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳', () => { const arbitrary = Seed.predicateWithData({ eq: {}, optionalTreatment: 'exactOptional', diff --git a/packages/schema/test/recursive.test.ts b/packages/schema-core/test/recursive.test.ts similarity index 64% rename from packages/schema/test/recursive.test.ts rename to packages/schema-core/test/recursive.test.ts index 2424567c..033a7806 100644 --- a/packages/schema/test/recursive.test.ts +++ b/packages/schema-core/test/recursive.test.ts @@ -1,8 +1,8 @@ import * as vi from 'vitest' -import { recurse, t, __trim as trim } from '@traversable/schema' +import { recurse, t, __trim as trim } from '@traversable/schema-core' -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳', () => { vi.it('〖⛳️〗› ❲recurse.trim❳', () => { vi.assert.equal(trim(), 'undefined') }) diff --git a/packages/schema/test/schema.test.ts b/packages/schema-core/test/schema.test.ts similarity index 96% rename from packages/schema/test/schema.test.ts rename to packages/schema-core/test/schema.test.ts index e370ed1f..e6337808 100644 --- a/packages/schema/test/schema.test.ts +++ b/packages/schema-core/test/schema.test.ts @@ -12,9 +12,7 @@ import { recurse, t, clone, - __replaceBooleanConstructor as replaceBooleanConstructor, - __carryover as carryover, -} from '@traversable/schema' +} from '@traversable/schema-core' import * as Seed from './seed.js' configure({ @@ -144,23 +142,23 @@ const builder * validate property-keys -- only property-values. */ -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳: property-based test suite', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core-core❳: property-based test suite', () => { test.prop( [builder().tree, fc.jsonValue()], { // numRuns: 10_000, endOnFailure: true, examples: [ - [["@traversable/schema/URI::eq", { "_": undefined }], {}], + [[URI.eq, { "_": undefined }], {}], // For parity with zod, which does not differentiate between 0 and -0, // we added a configuration option that allows users to pass a custom // "equalsFn", which defaults to IsStrictlyEqual ('===') - [["@traversable/schema/URI::eq", 0], -0], + [[URI.eq, 0], -0], [ [ - "@traversable/schema/URI::union", + URI.union, [ - "@traversable/schema/URI::string", - ["@traversable/schema/URI::eq", { "_O_$M$": "" }] + URI.string, + [URI.eq, { "_O_$M$": "" }] ] ], {} @@ -179,7 +177,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳: property-based test ) }) -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳', () => { vi.it('〖⛳️〗› ❲t.array❳', () => { const schema_01 = t.array(t.string) vi.assert.isFunction(schema_01) @@ -717,11 +715,6 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema#config❳', () => { vi.assert.isFalse(t.isUnary({})) }) - vi.it('〖⛳️〗› ❲~replaceBooleanConstructor❳', () => { - vi.assert.equal(replaceBooleanConstructor(globalThis.Boolean), t.nonnullable) - vi.assert.equal(replaceBooleanConstructor(t.string), t.string) - }) - vi.it('〖⛳️〗› ❲clone❳', () => { vi.assert.isFunction(clone(t.number)) vi.assert.isTrue(clone(t.number)(2)) @@ -736,10 +729,4 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema#config❳', () => { vi.assert.isTrue(t.isBoundableTag(URI.string)) vi.assert.isFalse(t.isBoundableTag(URI.null)) }) - - vi.it('〖⛳️〗› ❲~carryover❳', () => { - vi.assert.deepEqual(carryover({}), {}) - }) - /// coverage /// - ////////////////////// }) diff --git a/packages/schema/test/seed.ts b/packages/schema-core/test/seed.ts similarity index 97% rename from packages/schema/test/seed.ts rename to packages/schema-core/test/seed.ts index 44d1f752..e1c77688 100644 --- a/packages/schema/test/seed.ts +++ b/packages/schema-core/test/seed.ts @@ -2,10 +2,10 @@ import type * as T from '@traversable/registry' import { fn, parseKey, symbol, URI } from '@traversable/registry' import { Json } from '@traversable/json' -import type { SchemaOptions } from '@traversable/schema' -import { Equal, Predicate, t } from '@traversable/schema' +import type { SchemaOptions } from '@traversable/schema-core' +import { Equal, Predicate, t } from '@traversable/schema-core' -import * as fc from './fast-check.js' +import * as fc from 'fast-check' export { /* model */ @@ -393,7 +393,7 @@ namespace Recursive { } } - export const toSchema: T.Functor.Algebra = (x) => { + export const toSchema: T.Functor.Algebra = (x) => { if (x == null) return x switch (true) { default: return fn.exhaustive(x) @@ -432,7 +432,7 @@ namespace Recursive { case x[0] === URI.eq: return fc.constant(x[1]) case x[0] === URI.array: return fc.array(x[1]) case x[0] === URI.record: return fc.dictionary(fc.string(), x[1]) - case x[0] === URI.optional: return fc.optional(x[1]) + case x[0] === URI.optional: return fc.option(x[1], { nil: undefined }) case x[0] === URI.tuple: return fc.tuple(...x[1]) case x[0] === URI.union: return fc.oneof(...x[1]) case x[0] === URI.object: return fc.record(Object_fromEntries(x[1])) @@ -857,22 +857,33 @@ const Nullaries = { string: fc.constant(URI.string), } + +export type UniqueArrayDefaults = fc.UniqueArrayConstraintsRecommended +let entries = (model: fc.Arbitrary, constraints?: UniqueArrayDefaults) => fc.uniqueArray( + fc.tuple( + fc.stringMatching(new RegExp('^[$_a-zA-Z][$_a-zA-Z0-9]*$', 'u')), + model), + { ...constraints, selector: ([k]) => k } +) + function seed(_: Constraints = defaults) { const $ = parseConstraints(_) - return (go: fc.LetrecTypedTie) => ({ + return (go: fc.LetrecTypedTie): { [K in keyof Builder]: fc.Arbitrary } => ({ ...Nullaries, eq: go('tree').map((_) => [URI.eq, toJson(_)]), array: go('tree').map((_) => [URI.array, _]), record: go('tree').map((_) => [URI.record, _]), - optional: fc.optional(go('tree')).map((_) => [URI.optional, _]), + optional: go('tree').map((_) => [URI.optional, _]), tuple: fc.array(go('tree'), $.tuple).map((_) => [URI.tuple, _.sort(sortSeedOptionalsLast)] satisfies [any, any]), - object: fc.entries(go('tree'), $.object).map((_) => [URI.object, _]), + object: entries(go('tree'), $.object).map((_) => [URI.object, _]), union: fc.array(go('tree'), $.union).map((_) => [URI.union, _]), intersect: fc.array(go('tree'), $.intersect).map((_) => [URI.intersect, _]), tree: fc.oneof($.tree, ...pickAndSortNodes(initialOrder)($).map(go) as ReturnType>[]), } satisfies fc.LetrecValue) } +let zs = fc.letrec(seed()) + const identity = fold(Recursive.identity) // ^? diff --git a/packages/schema-core/test/types/deep-object--no-baseline.bench.types.ts b/packages/schema-core/test/types/deep-object--no-baseline.bench.types.ts new file mode 100644 index 00000000..ffa44ded --- /dev/null +++ b/packages/schema-core/test/types/deep-object--no-baseline.bench.types.ts @@ -0,0 +1,200 @@ +import { bench } from "@ark/attest" +import { t as core } from "@traversable/schema-core" +import { z as zod3 } from "zod3" +import { z as zod4 } from "zod4" +import { type as arktype } from "arktype" +import { Type as typebox } from "@sinclair/typebox" +import * as valibot from "valibot" + +export declare let RESULTS: [ + { + libraryName: "@traversable/schema" + instantiations: 1180 + }, + { + libraryName: "zod@4" + instantiations: 3328 + }, + { + libraryName: "@sinclair/typebox" + instantiations: 14320 + }, + { + libraryName: "arktype" + instantiations: 16235 + }, + { + libraryName: "valibot" + instantiations: 40168 + }, + { + libraryName: "zod@3" + instantiations: 40197 + } +] + +bench.baseline(() => void {}) + +bench("@traversable/schema: deep object (no baseline)", () => + core.object({ + a: core.object({ + b: core.object({ + c: core.optional( + core.object({ + d: core.boolean, + e: core.integer, + f: core.array( + core.object({ + g: core.unknown, + }), + ), + }), + ), + h: core.optional(core.record(core.string)), + }), + i: core.optional(core.bigint), + }), + j: core.optional( + core.object({ + k: core.array(core.record(core.object({ l: core.string }))), + }), + ), + }), +).types + ([1180, "instantiations"]) + +bench("zod@4: deep object (no baseline)", () => + zod4.object({ + a: zod4.object({ + b: zod4.object({ + c: zod4.optional( + zod4.object({ + d: zod4.boolean(), + e: zod4.number().int(), + f: zod4.array(zod4.object({ g: zod4.unknown() })), + }), + ), + h: zod4.optional(zod4.record(zod4.string(), zod4.string())), + }), + i: zod4.optional(zod4.bigint()), + }), + j: zod4.optional( + zod4.object({ + k: zod4.array( + zod4.record(zod4.string(), zod4.object({ l: zod4.string() })), + ), + }), + ), + }), +).types + ([3328, "instantiations"]) + +bench("@sinclair/typebox: deep object (no baseline)", () => + typebox.Object({ + a: typebox.Object({ + b: typebox.Object({ + c: typebox.Optional( + typebox.Object({ + d: typebox.Boolean(), + e: typebox.Integer(), + f: typebox.Array(typebox.Object({ g: typebox.Unknown() })), + }), + ), + h: typebox.Optional(typebox.Record(typebox.String(), typebox.String())), + }), + i: typebox.Optional(typebox.BigInt()), + }), + j: typebox.Optional( + typebox.Object({ + k: typebox.Array( + typebox.Record( + typebox.String(), + typebox.Object({ l: typebox.String() }), + ), + ), + }), + ), + }), +).types + ([14320, "instantiations"]) + +bench("arktype: deep object (no baseline)", () => + arktype({ + a: { + b: { + "c?": { + d: "boolean", + e: "number.integer", + f: arktype({ + g: "unknown", + }).array(), + }, + "h?": "Record", + }, + "i?": "bigint", + }, + "j?": { + k: arktype.Record("string", { l: "string" }).array(), + }, + }), +).types + ([16235, "instantiations"]) + +bench("valibot: deep object (no baseline)", () => + valibot.object({ + a: valibot.object({ + b: valibot.object({ + c: valibot.optional( + valibot.object({ + d: valibot.boolean(), + e: valibot.pipe(valibot.number(), valibot.integer()), + f: valibot.array( + valibot.object({ + g: valibot.unknown(), + }), + ), + }), + ), + h: valibot.optional(valibot.record(valibot.string(), valibot.string())), + }), + i: valibot.optional(valibot.bigint()), + }), + j: valibot.optional( + valibot.object({ + k: valibot.array( + valibot.record( + valibot.string(), + valibot.object({ l: valibot.string() }), + ), + ), + }), + ), + }), +).types + ([40168, "instantiations"]) + +bench("zod@3: deep object (no baseline)", () => + zod3.object({ + a: zod3.object({ + b: zod3.object({ + c: zod3.optional( + zod3.object({ + d: zod3.boolean(), + e: zod3.number().int(), + f: zod3.array(zod3.object({ g: zod3.unknown() })), + }), + ), + h: zod3.optional(zod3.record(zod3.string(), zod3.string())), + }), + i: zod3.optional(zod3.bigint()), + }), + j: zod3.optional( + zod3.object({ + k: zod3.array( + zod3.record(zod3.string(), zod3.object({ l: zod3.string() })), + ), + }), + ), + }), +).types + ([40197, "instantiations"]) diff --git a/packages/schema-core/test/types/deep-object.bench.types.ts b/packages/schema-core/test/types/deep-object.bench.types.ts new file mode 100644 index 00000000..7746ac4b --- /dev/null +++ b/packages/schema-core/test/types/deep-object.bench.types.ts @@ -0,0 +1,206 @@ +import { bench } from "@ark/attest" +import { t as core } from "@traversable/schema-core" +import { z as zod3 } from "zod3" +import { z as zod4 } from "zod4" +import { type as arktype } from "arktype" +import { Type as typebox } from "@sinclair/typebox" +import * as valibot from "valibot" + +export declare let RESULTS: [ + { + libraryName: "@traversable/schema" + instantiations: 1149 + }, + { + libraryName: "zod@4" + instantiations: 3199 + }, + { + libraryName: "arktype" + instantiations: 12359 + }, + { + libraryName: "@sinclair/typebox" + instantiations: 14294 + }, + { + libraryName: "zod@3" + instantiations: 19292 + }, + { + libraryName: "valibot" + instantiations: 40067 + } +] + +bench.baseline(() => { + core.tuple(core.string) + zod3.tuple([zod3.string()]) + typebox.Tuple([typebox.String()]) + valibot.tuple([valibot.string()]) + arktype(["string"]) +}) + +bench("@traversable/schema: deep object", () => + core.object({ + a: core.object({ + b: core.object({ + c: core.optional( + core.object({ + d: core.boolean, + e: core.integer, + f: core.array( + core.object({ + g: core.unknown, + }), + ), + }), + ), + h: core.optional(core.record(core.string)), + }), + i: core.optional(core.bigint), + }), + j: core.optional( + core.object({ + k: core.array(core.record(core.object({ l: core.string }))), + }), + ), + }), +).types + ([1149, "instantiations"]) + +bench("zod@4: deep object", () => + zod4.object({ + a: zod4.object({ + b: zod4.object({ + c: zod4.optional( + zod4.object({ + d: zod4.boolean(), + e: zod4.number().int(), + f: zod4.array(zod4.object({ g: zod4.unknown() })), + }), + ), + h: zod4.optional(zod4.record(zod4.string(), zod4.string())), + }), + i: zod4.optional(zod4.bigint()), + }), + j: zod4.optional( + zod4.object({ + k: zod4.array( + zod4.record(zod4.string(), zod4.object({ l: zod4.string() })), + ), + }), + ), + }), +).types + ([3199, "instantiations"]) + +bench("arktype: deep object", () => + arktype({ + a: { + b: { + "c?": { + d: "boolean", + e: "number.integer", + f: arktype({ + g: "unknown", + }).array(), + }, + "h?": "Record", + }, + "i?": "bigint", + }, + "j?": { + k: arktype.Record("string", { l: "string" }).array(), + }, + }), +).types + ([12359, "instantiations"]) + +bench("@sinclair/typebox: deep object", () => + typebox.Object({ + a: typebox.Object({ + b: typebox.Object({ + c: typebox.Optional( + typebox.Object({ + d: typebox.Boolean(), + e: typebox.Integer(), + f: typebox.Array(typebox.Object({ g: typebox.Unknown() })), + }), + ), + h: typebox.Optional(typebox.Record(typebox.String(), typebox.String())), + }), + i: typebox.Optional(typebox.BigInt()), + }), + j: typebox.Optional( + typebox.Object({ + k: typebox.Array( + typebox.Record( + typebox.String(), + typebox.Object({ l: typebox.String() }), + ), + ), + }), + ), + }), +).types + ([14294, "instantiations"]) + +bench("zod@3: deep object", () => + zod3.object({ + a: zod3.object({ + b: zod3.object({ + c: zod3.optional( + zod3.object({ + d: zod3.boolean(), + e: zod3.number().int(), + f: zod3.array(zod3.object({ g: zod3.unknown() })), + }), + ), + h: zod3.optional(zod3.record(zod3.string(), zod3.string())), + }), + i: zod3.optional(zod3.bigint()), + }), + j: zod3.optional( + zod3.object({ + k: zod3.array( + zod3.record(zod3.string(), zod3.object({ l: zod3.string() })), + ), + }), + ), + }), +).types + ([19292, "instantiations"]) + +bench("valibot: deep object", () => + valibot.object({ + a: valibot.object({ + b: valibot.object({ + c: valibot.optional( + valibot.object({ + d: valibot.boolean(), + e: valibot.pipe(valibot.number(), valibot.integer()), + f: valibot.array( + valibot.object({ + g: valibot.unknown(), + }), + ), + }), + ), + h: valibot.optional(valibot.record(valibot.string(), valibot.string())), + }), + i: valibot.optional(valibot.bigint()), + }), + j: valibot.optional( + valibot.object({ + k: valibot.array( + valibot.record( + valibot.string(), + valibot.object({ l: valibot.string() }), + ), + ), + }), + ), + }), +).types + ([40067, "instantiations"]) diff --git a/packages/schema-core/test/types/object--no-baseline.bench.types.ts b/packages/schema-core/test/types/object--no-baseline.bench.types.ts new file mode 100644 index 00000000..a98392ad --- /dev/null +++ b/packages/schema-core/test/types/object--no-baseline.bench.types.ts @@ -0,0 +1,84 @@ +import { bench } from "@ark/attest" +import { t as core } from "@traversable/schema-core" +import { z as zod3 } from "zod3" +import { z as zod4 } from "zod4" +import { type as arktype } from "arktype" +import { Type as typebox } from "@sinclair/typebox" +import * as valibot from "valibot" + +export declare let RESULTS: [ + { + libraryName: "@traversable/schema" + instantiations: 102 + }, + { + libraryName: "@sinclair/typebox" + instantiations: 159 + }, + { + libraryName: "zod@4" + instantiations: 484 + }, + { + libraryName: "arktype" + instantiations: 5011 + }, + { + libraryName: "valibot" + instantiations: 15973 + }, + { + libraryName: "zod@3" + instantiations: 25146 + } +] + +bench.baseline(() => void {}) + +bench("@traversable/schema: object (no baseline)", () => + core.object({ + a: core.boolean, + b: core.optional(core.number), + }), +).types + ([102, "instantiations"]) + +bench("zod@4: object", () => + zod4.object({ + a: zod4.boolean(), + b: zod4.optional(zod4.number()), + }), +).types + ([484, "instantiations"]) + +bench("arktype: object (no baseline)", () => + arktype({ + a: "boolean", + "b?": "number", + }), +).types + ([5011, "instantiations"]) + +bench("@sinclair/typebox: object (no baseline)", () => + typebox.Object({ + a: typebox.Boolean(), + b: typebox.Optional(typebox.Number()), + }), +).types + ([159, "instantiations"]) + +bench("zod@3: object", () => + zod3.object({ + a: zod3.boolean(), + b: zod3.optional(zod3.number()), + }), +).types + ([25146, "instantiations"]) + +bench("valibot: object (no baseline)", () => + valibot.object({ + a: valibot.boolean(), + b: valibot.optional(valibot.number()), + }), +).types + ([15973, "instantiations"]) diff --git a/packages/schema-core/test/types/object.bench.types.ts b/packages/schema-core/test/types/object.bench.types.ts new file mode 100644 index 00000000..97aad00c --- /dev/null +++ b/packages/schema-core/test/types/object.bench.types.ts @@ -0,0 +1,90 @@ +import { bench } from "@ark/attest" +import { t as core } from "@traversable/schema-core" +import { z as zod3 } from "zod3" +import { z as zod4 } from "zod4" +import { type as arktype } from "arktype" +import { Type as typebox } from "@sinclair/typebox" +import * as valibot from "valibot" + +export declare let RESULTS: [ + { + libraryName: "@traversable/schema" + instantiations: 86 + }, + { + libraryName: "@sinclair/typebox" + instantiations: 150 + }, + { + libraryName: "zod@4" + instantiations: 356 + }, + { + libraryName: "arktype" + instantiations: 1663 + }, + { + libraryName: "zod@3" + instantiations: 4541 + }, + { + libraryName: "valibot" + instantiations: 16091 + } +] + +bench.baseline(() => { + core.tuple(core.string) + zod3.tuple([zod3.string()]) + typebox.Tuple([typebox.String()]) + valibot.tuple([valibot.string()]) + arktype(["string"]) +}) + +bench("@traversable/schema: object", () => + core.object({ + a: core.boolean, + b: core.optional(core.number), + }), +).types + ([86, "instantiations"]) + +bench("zod@4: object", () => + zod4.object({ + a: zod4.boolean(), + b: zod4.optional(zod4.number()), + }), +).types + ([356, "instantiations"]) + +bench("arktype: object", () => + arktype({ + a: "boolean", + "b?": "number", + }), +).types + ([1663, "instantiations"]) + +bench("@sinclair/typebox: object", () => + typebox.Object({ + a: typebox.Boolean(), + b: typebox.Optional(typebox.Number()), + }), +).types + ([150, "instantiations"]) + +bench("zod@3: object", () => + zod3.object({ + a: zod3.boolean(), + b: zod3.optional(zod3.number()), + }), +).types + ([4541, "instantiations"]) + +bench("valibot: object", () => + valibot.object({ + a: valibot.boolean(), + b: valibot.optional(valibot.number()), + }), +).types + ([16091, "instantiations"]) diff --git a/packages/schema/test/utils.test.ts b/packages/schema-core/test/utils.test.ts similarity index 96% rename from packages/schema/test/utils.test.ts rename to packages/schema-core/test/utils.test.ts index 6a015360..78386771 100644 --- a/packages/schema/test/utils.test.ts +++ b/packages/schema-core/test/utils.test.ts @@ -1,5 +1,5 @@ import * as vi from 'vitest' -import { t, get, get$ } from '@traversable/schema' +import { t, get, get$ } from '@traversable/schema-core' import { symbol } from '@traversable/registry' const Schema_01 = t.tuple(t.eq(1)) @@ -26,7 +26,7 @@ const Schema_05 = t.object({ c: t.record(t.boolean) }) -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳', () => { vi.it('〖⛳️〗› ❲get❳', () => { vi.assert.equal(get(Schema_01), Schema_01) vi.assert.equal(get(Schema_02), Schema_02) diff --git a/packages/schema-core/test/version.test.ts b/packages/schema-core/test/version.test.ts new file mode 100644 index 00000000..074a9e1d --- /dev/null +++ b/packages/schema-core/test/version.test.ts @@ -0,0 +1,10 @@ +import * as vi from 'vitest' +import { VERSION } from '@traversable/schema-core' +import pkg from '../package.json' with { type: 'json' } + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳', () => { + vi.it('〖⛳️〗› ❲VERSION❳', () => { + const expected = `${pkg.name}@${pkg.version}` + vi.assert.equal(VERSION, expected) + }) +}) diff --git a/packages/schema-core/tsconfig.bench.json b/packages/schema-core/tsconfig.bench.json new file mode 100644 index 00000000..5f6952d1 --- /dev/null +++ b/packages/schema-core/tsconfig.bench.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "strict": true, + "tsBuildInfoFile": ".tsbuildinfo/bench.tsbuildinfo", + "rootDir": "bench", + "types": ["node"], + "noEmit": true, + "paths": { + "@traversable/registry": ["../../packages/registry/src/index.js"], + "@traversable/registry/*": ["../../packages/registry/src/*.js"], + "@traversable/schema-core": ["../../packages/schema-core/src/index.js"], + "@traversable/schema-core/*": ["../../packages/schema-core/src/*.js"] + } + }, + "references": [ + { "path": "tsconfig.src.json" } + ], + "include": ["bench", "test/types/deep-object--no-baseline.bench.types.ts", "test/types/deep-object.bench.types.ts", "test/types/object--no-baseline.bench.types.ts", "test/types/object.bench.types.ts"] +} diff --git a/packages/schema-core/tsconfig.build.json b/packages/schema-core/tsconfig.build.json new file mode 100644 index 00000000..0a93b6f4 --- /dev/null +++ b/packages/schema-core/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "types": ["node"], + "declarationDir": "build/dts", + "outDir": "build/esm", + "stripInternal": true + }, + "references": [ + { "path": "../registry" } + ] +} diff --git a/packages/schema-core/tsconfig.json b/packages/schema-core/tsconfig.json new file mode 100644 index 00000000..2c291d21 --- /dev/null +++ b/packages/schema-core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/packages/schema-core/tsconfig.src.json b/packages/schema-core/tsconfig.src.json new file mode 100644 index 00000000..7e6324da --- /dev/null +++ b/packages/schema-core/tsconfig.src.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "types": ["node"], + "outDir": "build/src" + }, + "references": [ + { "path": "../registry" } + ], + "include": ["src"] +} diff --git a/packages/schema-core/tsconfig.test.json b/packages/schema-core/tsconfig.test.json new file mode 100644 index 00000000..06b45e93 --- /dev/null +++ b/packages/schema-core/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "types": ["node"], + "noEmit": true + }, + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../registry" }, + { "path": "../schema-zod-adapter" }, + ], + "include": ["test"] +} diff --git a/packages/schema-core/vitest.config.ts b/packages/schema-core/vitest.config.ts new file mode 100644 index 00000000..64dba4ad --- /dev/null +++ b/packages/schema-core/vitest.config.ts @@ -0,0 +1,6 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import sharedConfig from '../../vite.config.js' + +const localConfig = defineConfig({}) + +export default mergeConfig(sharedConfig, localConfig) \ No newline at end of file diff --git a/packages/schema-generator/README.md b/packages/schema-generator/README.md new file mode 100644 index 00000000..62d25f35 --- /dev/null +++ b/packages/schema-generator/README.md @@ -0,0 +1,38 @@ +
+

ᯓ𝘁𝗿𝗮𝘃𝗲𝗿𝘀𝗮𝗯𝗹𝗲/𝘀𝗰𝗵𝗲𝗺𝗮-𝗴𝗲𝗻𝗲𝗿𝗮𝘁𝗼𝗿

+
+ +

+ Internal package that generates schemas. Keeps schemas fast and bundles small by making the `@traversable/schema` feature set 100% opt-in. +

+ +
+ NPM Version +   + TypeScript +   + Static Badge +   + npm +   +
+ +
+ npm bundle size (scoped) +   + Static Badge +   + Static Badge +   +
+ +
+ Demo (StackBlitz) +   •   + TypeScript Playground +   •   + npm +
+
+
+
diff --git a/packages/schema-generator/package.json b/packages/schema-generator/package.json new file mode 100644 index 00000000..8762e45b --- /dev/null +++ b/packages/schema-generator/package.json @@ -0,0 +1,69 @@ +{ + "name": "@traversable/schema-generator", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/schema-generator" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { + "include": [ + "**/*.ts" + ] + }, + "generateIndex": { + "include": [ + "**/*.ts" + ] + } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "bin": "./src/cli.ts", + "scripts": { + "bench": "echo NOTHING TO BENCH", + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "gen": "pnpm dlx tsx ./src/build.ts", + "gen:w": "pnpm dlx tsx --watch ./src/build.ts", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "clean:gen": "rm -rf src/__schemas__ src/temp test/__generated__", + "_postinstall": "pnpm dlx tsx ./src/build.ts", + "test": "vitest" + }, + "peerDependencies": { + "@traversable/derive-validators": "workspace:^", + "@traversable/derive-equals": "workspace:^", + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^", + "@traversable/schema-to-json-schema": "workspace:^", + "@traversable/schema-to-string": "workspace:^" + }, + "devDependencies": { + "@clack/prompts": "^0.10.1", + "@traversable/derive-validators": "workspace:^", + "@traversable/derive-equals": "workspace:^", + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^", + "@traversable/schema-to-json-schema": "workspace:^", + "@traversable/schema-to-string": "workspace:^", + "picocolors": "^1.1.1" + } +} diff --git a/packages/schema-generator/src/__build.ts__ b/packages/schema-generator/src/__build.ts__ new file mode 100755 index 00000000..cc92a028 --- /dev/null +++ b/packages/schema-generator/src/__build.ts__ @@ -0,0 +1,584 @@ +#!/usr/bin/env pnpm dlx tsx +import type { IfUnaryReturns } from '@traversable/registry' +import * as path from 'node:path' +import * as fs from 'node:fs' +import { fn } from '@traversable/registry' +import { t } from '@traversable/schema-core' +import { generateSchemas } from '@traversable/schema-generator' + +/** + * ## TODO + * + * - [x] Pull the .ts files out of `@traversable/schema-core` + * - [x] Pull the .ts files out of `@traversable/derive-equals` + * - [x] Pull the .ts files out of `@traversable/schema-to-json-schema` + * - [x] Pull the .ts files out of `@traversable/derive-validators` + * - [x] Pull the .ts files out of `@traversable/schema-to-string` + * - [x] Read extension config files from `extensions` dir + * - [x] Allow local imports to pass through the parser + * - [x] Write generated schemas to namespace file so they can be used by other schemas + * - [x] Clean up the temp dir + * - [x] Configure the package.json file to export from `__schemas__` + */ + +let CWD = process.cwd() + +let PATH = { + sourceDir: path.join(CWD, 'node_modules', '@traversable'), + tempDir: path.join(CWD, 'src', 'temp'), + extensionsDir: path.join(CWD, 'src', 'extensions'), + targetDir: path.join(CWD, 'src', '__schemas__'), + namespaceFile: path.join(CWD, 'src', '_namespace.ts'), +} + +let EXTENSION_FILES_IGNORE_LIST = [ + 'equals.ts', + 'toJsonSchema.ts', + 'toString.ts', + 'validate.ts', +] + +/** + * TODO: Derive this list from the {@link EXTENSION_FILES_IGNORE_LIST ignore list} + */ +let REMOVE_IMPORTS_LIST = [ + /.*equals.js'\n/, + /.*toJsonSchema.js'\n/, + /.*toString.js'\n/, + /.*validate.js'\n/, +] + +type Library = typeof Library[keyof typeof Library] +let Library = { + Core: 'schema-core', + Equals: 'derive-equals', + ToJsonSchema: 'schema-to-json-schema', + ToString: 'schema-to-string', + Validators: 'derive-validators', +} as const + +let LIB_NAME_TO_TARGET_FILENAME = { + [Library.Core]: 'core', + [Library.Equals]: 'equals', + [Library.ToJsonSchema]: 'toJsonSchema', + [Library.Validators]: 'validate', + [Library.ToString]: 'toString', +} as const satisfies Record + +let removeIgnoredImports = (content: string) => { + for (let ignore of REMOVE_IMPORTS_LIST) + content = content.replace(ignore, '') + return content +} + +let localSchemaNames = { + any: 'any_', + bigint: 'bigint_', + boolean: 'boolean_', + never: 'never_', + null: 'null_', + number: 'number_', + object: 'object_', + string: 'string_', + symbol: 'symbol_', + undefined: 'undefined_', + unknown: 'unknown_', + void: 'void_', + array: 'array', + eq: 'eq', + integer: 'integer', + intersect: 'intersect', + of: 'of', + optional: 'optional', + record: 'record', + tuple: 'tuple', + union: 'union', +} as Record + +let TargetReplace = { + internal: { + /** + * @example + * // from: + * import type { Guarded, Schema, SchemaLike } from '../../_namespace.js' + * // to: + * import type { Guarded, Schema, SchemaLike } from '../namespace.js' + */ + from: /'(\.\.\/)namespace.js'/g, + to: '\'../_namespace.js\'', + }, + namespace: { + from: /'@traversable\/schema-core'/g, + to: '\'../_exports.js\'', + }, + coverageDirective: { + from: /\s*\/\* v8 ignore .+ \*\//g, + to: '', + }, + selfReference: (schemaName: string) => { + let localSchemaName = localSchemaNames[schemaName] + return { + from: `t.${schemaName}`, + to: localSchemaName, + } + }, +} + +type Rewrite = (x: string) => string +let rewriteCoreInternalImport: Rewrite = (_) => _.replaceAll(TargetReplace.internal.from, TargetReplace.internal.to) +let rewriteCoreNamespaceImport: Rewrite = (_) => _.replaceAll(TargetReplace.namespace.from, TargetReplace.namespace.to) +let removeCoverageDirectives: Rewrite = (_) => _.replaceAll(TargetReplace.coverageDirective.from, TargetReplace.coverageDirective.to) +let rewriteSelfReferences: (schemaName: string) => Rewrite = (schemaName) => { + let { from, to } = TargetReplace.selfReference(schemaName) + return (_) => _.replaceAll(from, to) +} + +let isKeyOf = (k: keyof any, t: T): k is keyof T => + !!t && (typeof t === 'object' || typeof t === 'function') && k in t + +type GetTargetFileName = (libName: string, schemaName: string) => `${string}.ts` +type PostProcessor = (sourceFileContent: string, schemaName: string) => string + +type LibOptions = t.typeof +let LibOptions = t.object({ + /** + * ## {@link LibOptions.def.relativePath `LibOptions.relativePath`} + */ + relativePath: t.string, + /** + * ## {@link LibOptions.def.getTargetFileName `LibOptions.getTargetFileName`} + */ + getTargetFileName: (x): x is GetTargetFileName => typeof x === 'function', + // TODO: actually exclude files + /** + * ## {@link LibOptions.def.excludeFiles `LibOptions.excludeFiles`} + */ + excludeFiles: t.array(t.string), + /** + * ## {@link LibOptions.def.includeFiles `LibOptions.includeFiles`} + */ + includeFiles: t.optional(t.array(t.string)), +}) + +type BuildOptions = t.typeof +let BuildOptions = t.object({ + /** + * ## {@link BuildOptions.def.dryRun `Options.dryRun`} + * + * Execute the build as a dry run. Will read files and directory content, + * but won't write anything to disc. + */ + dryRun: t.optional(t.boolean), + pkgNameForHeader: t.string, + /** + * ## {@link BuildOptions.def.extensionFiles `Options.extensionFiles`} + * + * An array of string 2-tuples containing schema name, and extension + * file content encoded as a utf-8 string. + */ + extensionFiles: t.array(t.tuple(t.string, t.string)), + /** + * ## {@link BuildOptions.def.skipCleanup `Options.skipCleanup`} + */ + skipCleanup: t.optional(t.boolean), + /** + * ## {@link BuildOptions.def.postProcessor `Options.postProcessor`} + * + * A function to run over every generated file before writing to disc. + */ + postProcessor: (x): x is PostProcessor => typeof x === 'function', + /** + * ## {@link BuildOptions.def.excludeSchemas `Options.excludeSchemas`} + */ + excludeSchemas: t.optional(t.union(t.array(t.string), t.null)), + /** + * ## {@link BuildOptions.def.getSourceDir `Options.getSourceDir`} + */ + getSourceDir: t.optional((x): x is (() => string) => typeof x === 'function'), + /** + * ## {@link BuildOptions.def.getNamespaceFile `Options.getNamespaceFile`} + */ + getNamespaceFile: t.optional((x): x is (() => string) => typeof x === 'function'), + /** + * ## {@link BuildOptions.def.getTempDir `Options.getTempDir`} + */ + getTempDir: t.optional((x): x is (() => string) => typeof x === 'function'), + /** + * ## {@link BuildOptions.def.getTargetDir `Options.getTargetDir`} + */ + getTargetDir: t.optional((x): x is (() => string) => typeof x === 'function'), + /** + * ## {@link BuildOptions.def.getExtensionFilesDir `Options.getExtensionFilesDir`} + */ + getExtensionFilesDir: t.optional((x): x is (() => string) => typeof x === 'function'), +}) + +type LibsOptions = never | { libs: Record> } +type LibsConfig = never | { libs: Record } +type ParseOptions = never | { [K in keyof T as K extends `get${infer P}` ? Uncapitalize

: K]-?: IfUnaryReturns } +type BuildConfig = ParseOptions + +interface Options extends BuildOptions, LibsOptions {} +interface Config extends BuildConfig, LibsConfig {} + +let defaultGetTargetFileName = ( + (libName, _schemaName) => isKeyOf(libName, LIB_NAME_TO_TARGET_FILENAME) + ? `${LIB_NAME_TO_TARGET_FILENAME[libName]}.ts` as const + : `${libName}.ts` +) satisfies LibOptions['getTargetFileName'] + +let defaultPostProcessor = (content: string, schemaName: string) => fn.pipe( + content, + rewriteCoreInternalImport, + rewriteCoreNamespaceImport, + removeCoverageDirectives, + removeIgnoredImports, + rewriteSelfReferences(schemaName), +) + +let defaultLibOptions = { + relativePath: 'src/schemas', + excludeFiles: [], + getTargetFileName: defaultGetTargetFileName, +} satisfies LibOptions + +let defaultLibs = { + [Library.Core]: defaultLibOptions, + [Library.Equals]: defaultLibOptions, + [Library.ToJsonSchema]: defaultLibOptions, + [Library.ToString]: defaultLibOptions, + [Library.Validators]: defaultLibOptions, +} satisfies Record + +let defaultOptions = { + dryRun: false, + pkgNameForHeader: '', + skipCleanup: false, + postProcessor: defaultPostProcessor, + excludeSchemas: null, + getExtensionFilesDir: () => { + if (!fs.existsSync(PATH.extensionsDir)) + throw Error('No extensions specified') + return PATH.extensionsDir + }, + getNamespaceFile: () => PATH.namespaceFile, + getSourceDir: () => { + if (fs.existsSync(PATH.sourceDir)) return PATH.sourceDir + else + try { fs.mkdirSync(PATH.sourceDir); return PATH.sourceDir } + catch (e) { throw Error('could not create source dir: ' + PATH.sourceDir) } + }, + getTempDir: () => { + if (fs.existsSync(PATH.tempDir)) return PATH.tempDir + else + try { fs.mkdirSync(PATH.tempDir); return PATH.tempDir } + catch (e) { throw Error('could not create temp dir: ' + PATH.tempDir) } + }, + getTargetDir: () => { + if (fs.existsSync(PATH.targetDir)) return PATH.targetDir + else + try { fs.mkdirSync(PATH.targetDir); return PATH.targetDir } + catch (e) { throw Error('could not create target dir: ' + PATH.targetDir) } + }, + libs: defaultLibs, +} satisfies Omit & LibsOptions, 'extensionFiles'> + +function parseLibOptions({ + excludeFiles = defaultLibOptions.excludeFiles, + relativePath = defaultLibOptions.relativePath, + getTargetFileName = defaultLibOptions.getTargetFileName, + includeFiles, +}: Partial): LibOptions { + return { + excludeFiles, + relativePath, + getTargetFileName, + ...includeFiles && { includeFiles } + } +} + +function parseOptions(options: Options): Config +function parseOptions({ + extensionFiles, + dryRun = defaultOptions.dryRun, + excludeSchemas = null, + pkgNameForHeader = defaultOptions.pkgNameForHeader, + getExtensionFilesDir = defaultOptions.getExtensionFilesDir, + getNamespaceFile = defaultOptions.getNamespaceFile, + getSourceDir = defaultOptions.getSourceDir, + getTargetDir = defaultOptions.getTargetDir, + getTempDir = defaultOptions.getTempDir, + libs, + postProcessor = defaultOptions.postProcessor, + skipCleanup = defaultOptions.skipCleanup, +}: Options): Config { + return { + dryRun, + excludeSchemas, + extensionFiles, + extensionFilesDir: getExtensionFilesDir(), + libs: fn.map(libs, parseLibOptions), + namespaceFile: getNamespaceFile(), + pkgNameForHeader, + postProcessor, + skipCleanup, + sourceDir: getSourceDir(), + targetDir: getTargetDir(), + tempDir: getTempDir(), + } +} + +let tap + : (effect: (s: S) => T) => (x: S) => S + = (effect) => (x) => (effect(x), x) + +let ensureDir + : (dirpath: string, $: Config) => void + = (dirpath, $) => !$.dryRun + ? void (!fs.existsSync(dirpath) && fs.mkdirSync(dirpath)) + : void ( + console.group('[[DRY_RUN]]: `ensureDir`'), + console.debug('mkDir:', dirpath), + console.groupEnd() + ) + +function writeExtensionFiles($: Config) { + if (!fs.existsSync($.extensionFilesDir)) { + throw Error('Could not find extensions dir: ' + $.extensionFilesDir) + } + let extensionFiles = $.extensionFiles + // fs + // .readdirSync($.extensionFilesDir) + .filter(([filename]) => !EXTENSION_FILES_IGNORE_LIST.includes(filename)) + + extensionFiles.forEach(([filename, fileContent]) => { + let tempDirName = filename.slice(0, -'.ts'.length) + let tempDirPath = path.join($.tempDir, tempDirName) + let tempPath = path.join(tempDirPath, 'extension.ts') + // let sourcePath = path.join($.extensionFilesDir, filename) + // let content = fs.readFileSync(sourcePath).toString('utf8') + ensureDir(tempDirPath, $) + if ($.dryRun) { + console.group('\n\n[[DRY_RUN]]:: `copyExtensionFiles`') + console.debug('\ntempPath:\n', tempPath) + console.debug('\nfile content:\n', fileContent) + console.groupEnd() + } else { + console.group('\n\n[[DRY_RUN]]:: `copyExtensionFiles`') + console.debug('\ntempPath:\n', tempPath) + console.debug('\nfile content:\n', fileContent) + console.groupEnd() + + fs.writeFileSync(tempPath, fileContent) + } + }) +} + +function buildSchemas($: Config): void { + let cache = new Set() + + return void fs.readdirSync( + path.join($.sourceDir), { withFileTypes: true }) + .filter(({ name }) => Object.keys($.libs).includes(name)) + .map( + (sourceDir) => { + let LIB_NAME = sourceDir.name + let LIB = $.libs[LIB_NAME] + return fn.pipe( + path.join( + sourceDir.parentPath, + LIB_NAME, + $.libs[LIB_NAME].relativePath, + ), + (schemasDir) => fs.readdirSync(schemasDir, { withFileTypes: true }), + fn.map( + (schemaFile) => { + let sourceFilePath = path.join(schemaFile.parentPath, schemaFile.name) + let sourceFileContent = fs.readFileSync(sourceFilePath).toString('utf8') + let targetFileName = LIB.getTargetFileName(LIB_NAME, schemaFile.name) + let schemaName = schemaFile.name.endsWith('.ts') + ? schemaFile.name.slice(0, -'.ts'.length) + : schemaFile.name + + let targetFilePath = path.join( + $.tempDir, + schemaName, + targetFileName + ) + + let tempDirPath = path.join( + $.tempDir, + schemaFile.name.slice(0, -'.ts'.length), + ) + + if (!cache.has(tempDirPath) && !$.dryRun) { + cache.add(tempDirPath) + ensureDir(tempDirPath, $) + } + + if (!$.dryRun) { + fs.writeFileSync( + targetFilePath, + sourceFileContent, + ) + } else { + console.group('\n\n[[DRY_RUN]]:: `buildSchemas`') + console.debug('\ntargetFilePath:\n', targetFilePath) + console.debug('\nsourceFileContent:\n', sourceFileContent) + console.groupEnd() + } + } + ), + ) + } + ) +} + +function getSourcePaths($: Config): Record> { + if (!fs.existsSync($.tempDir)) { + throw Error('[getSourcePaths] Expected temp directory to exist: ' + $.tempDir) + } + + return fs.readdirSync($.tempDir, { withFileTypes: true }) + .reduce( + (acc, { name, parentPath }) => ({ + ...acc, + [name]: fs + .readdirSync(path.join(parentPath, name), { withFileTypes: true }) + .reduce( + (acc, { name, parentPath }) => ({ + ...acc, + [name.slice(0, -'.ts'.length)]: path.join(parentPath, name) + }), + {} + ) + }), + {} + ) +} + +function createTargetPaths($: Config, sourcePaths: Record>) { + return fn.map(sourcePaths, (_, schemaName) => path.join($.targetDir, `${schemaName}.ts`)) +} + +export function writeSchemas( + $: Config, + sources: Record>, + targets: Record, + pkgNameForHeader: string, +): void { + let schemas = generateSchemas(sources, targets, pkgNameForHeader) + for (let [target, generatedContent] of schemas) { + let pathSegments = target.split('/') + let fileName = pathSegments[pathSegments.length - 1] + let schemaName = fileName.endsWith('.ts') ? fileName.slice(0, -'.ts'.length) : fileName + let content = $.postProcessor(generatedContent, schemaName) + if ($.dryRun) { + console.group('\n\n[[DRY_RUN]]:: `writeSchemas`') + console.debug('\ntarget:\n', target) + console.debug('\ncontent after post-processing:\n', content) + console.groupEnd() + } else { + fs.writeFileSync(target, content) + } + } +} + +function getNamespaceFileContent(previousContent: string, $: Config, sources: Record>) { + let targetDirNames = $.targetDir.split('/') + let targetDirName = targetDirNames[targetDirNames.length - 1] + let lines = Object.keys(sources).map((schemaName) => `export { ${schemaName} } from './${targetDirName}/${schemaName}.js'`) + return previousContent + '\r\n' + lines.join('\n') + '\r\n' +} + +export function writeNamespaceFile($: Config, sources: Record>) { + let content = getNamespaceFileContent(fs.readFileSync($.namespaceFile).toString('utf8'), $, sources) + if (content.includes('export {')) { + if ($.dryRun) { + console.group('\n\n[[DRY_RUN]]:: `writeNamespaceFile`') + console.debug('\ntarget file already have term-level exports:\n', content) + console.groupEnd() + } else { + return void 0 + } + } + else if ($.dryRun) { + console.group('\n\n[[DRY_RUN]]:: `writeNamespaceFile`') + console.debug('\nnamespace file path:\n', $.namespaceFile) + console.debug('\nnamespace file content:\n', content) + console.groupEnd() + } else { + fs.writeFileSync($.namespaceFile, content) + } +} + +export function cleanupTempDir($: Config) { + if ($.dryRun) { + console.group('\n\n[[DRY_RUN]]:: `cleanupTempDir`') + console.debug('\ntemp dir path:\n', $.tempDir) + console.groupEnd() + } + else { + void fs.rmSync($.tempDir, { force: true, recursive: true }) + } +} + +function build(options: Options) { + options.dryRun + let $ = parseOptions(options) + let extensionFiles = $.extensionFiles + + void ensureDir($.tempDir, $) + void writeExtensionFiles($) + buildSchemas($) + + let sources = fn.pipe( + getSourcePaths($), + ) + let targets = createTargetPaths($, sources) + + if ($.dryRun) { + console.group('\n\n[[DRY_RUN]]:: `build`') + console.debug('\nsources:\n', sources) + console.debug('\ntargets:\n', targets) + console.groupEnd() + } + + fn.map(sources, (v, k) => { + console.log('source k:', k, 'typeof k:', typeof k) + console.log('source v:', v) + }) + + void ensureDir($.targetDir, $) + void writeSchemas($, sources, targets, $.pkgNameForHeader) + void writeNamespaceFile($, sources) + + if ($.skipCleanup) { + console.group('\n\n[[SKIP_CLEANUP]]: `build`\n') + console.debug('\n`build` received \'skipCleanup\': true. ' + $.tempDir + ' was not removed.') + console.groupEnd() + return void 0 + } else { + void cleanupTempDir($) + } +} + +let extensionFiles = fs + .readdirSync(PATH.extensionsDir, { withFileTypes: true }) + .map( + ({ name, parentPath }) => + fn.pipe( + fs.readFileSync(path.join(parentPath, name)), + (buffer) => buffer.toString('utf8'), + (content) => [name, content] satisfies [any, any] + ) + ) + +console.log('extensionFiles', extensionFiles) + +build({ + ...defaultOptions, + // dryRun: true, + skipCleanup: true, + extensionFiles, +}) diff --git a/packages/schema-generator/src/__generated__/__manifest__.ts b/packages/schema-generator/src/__generated__/__manifest__.ts new file mode 100644 index 00000000..d05c6d42 --- /dev/null +++ b/packages/schema-generator/src/__generated__/__manifest__.ts @@ -0,0 +1,65 @@ +export default { + "name": "@traversable/schema-generator", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/schema-generator" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { + "include": ["**/*.ts"] + }, + "generateIndex": { + "include": ["**/*.ts"] + } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "bin": "./src/cli.ts", + "scripts": { + "bench": "echo NOTHING TO BENCH", + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "gen": "pnpm dlx tsx ./src/build.ts", + "gen:w": "pnpm dlx tsx --watch ./src/build.ts", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "clean:gen": "rm -rf src/__schemas__ src/temp test/__generated__", + "_postinstall": "pnpm dlx tsx ./src/build.ts", + "test": "vitest" + }, + "peerDependencies": { + "@traversable/derive-validators": "workspace:^", + "@traversable/derive-equals": "workspace:^", + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^", + "@traversable/schema-to-json-schema": "workspace:^", + "@traversable/schema-to-string": "workspace:^" + }, + "devDependencies": { + "@clack/prompts": "^0.10.1", + "@traversable/derive-validators": "workspace:^", + "@traversable/derive-equals": "workspace:^", + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^", + "@traversable/schema-to-json-schema": "workspace:^", + "@traversable/schema-to-string": "workspace:^", + "picocolors": "^1.1.1" + } +} as const \ No newline at end of file diff --git a/packages/schema-generator/src/_exports.ts b/packages/schema-generator/src/_exports.ts new file mode 100644 index 00000000..6cbf2955 --- /dev/null +++ b/packages/schema-generator/src/_exports.ts @@ -0,0 +1,2 @@ +export * as t from './_namespace.js' +export { getConfig } from '@traversable/schema-core' diff --git a/packages/schema-generator/src/_namespace.ts b/packages/schema-generator/src/_namespace.ts new file mode 100644 index 00000000..94ca1a4c --- /dev/null +++ b/packages/schema-generator/src/_namespace.ts @@ -0,0 +1,14 @@ +export type { + Entry, + FirstOptionalItem, + IntersectType, + Guard, + Guarded, + invalid, + Optional, + Required, + Schema, + SchemaLike, + TupleType, + ValidateTuple, +} from '@traversable/schema-core/namespace' diff --git a/packages/schema-generator/src/cli.ts b/packages/schema-generator/src/cli.ts new file mode 100755 index 00000000..2a3ea9a6 --- /dev/null +++ b/packages/schema-generator/src/cli.ts @@ -0,0 +1,440 @@ +#!/usr/bin/env pnpm dlx tsx +import * as fs from 'node:fs' +import * as path from 'node:path' +import { execSync as x } from 'node:child_process' + +import * as p from '@clack/prompts' +import color from 'picocolors' +import { VERSION } from './version.js' + +import { t } from '@traversable/schema-core' + +let Primitives = t.eq([ + { value: 'null', label: 'null' }, + { value: 'undefined', label: 'undefined' }, + { value: 'boolean', label: 'boolean' }, + { value: 'number', label: 'number' }, + { value: 'string', label: 'string' }, + { value: 'bigint', label: 'bigint' }, + { value: 'integer', label: 'integer' }, + { value: 'unknown', label: 'unknown' }, + { value: 'never', label: 'never' }, + { value: 'void', label: 'void' }, + { value: 'any', label: 'any' }, + { value: 'symbol', label: 'symbol' }, +]).def + +let Combinators = t.eq([ + { value: 'object', label: 'object' }, + { value: 'array', label: 'array' }, + { value: 'union', label: 'union' }, + { value: 'intersect', label: 'intersect' }, + { value: 'record', label: 'record' }, + { value: 'optional', label: 'optional' }, + { value: 'tuple', label: 'tuple' }, + { value: 'eq', label: 'eq' }, +]).def + +let Features = t.eq([ + { value: 'equals', label: 'equals' }, + { value: 'toJsonSchema', label: 'toJsonSchema' }, + { value: 'toString', label: 'toString' }, + { value: 'validate', label: 'validate' }, +]).def + +async function prompt() { + p.intro(' 🌳 ' + color.underline(VERSION) + ' ') + + let $ = await p.group({ + target: () => p.text({ + message: 'Where would you like to install your schemas?', + placeholder: 'Path to an empty directory', + validate(path) { + if (!path.startsWith('./')) + return 'Please enter a relative path (e.g., \'./schemas\'' + }, + }), + primitives: () => p.multiselect({ + message: 'Pick your primitives', + initialValues: ['*'], + options: [ + { value: '*', label: '* (all)' }, + ...Primitives, + ], + }), + combinators: () => p.multiselect({ + message: 'Pick your combinators', + initialValues: ['*'], + options: [ + { value: '*', label: '* (all)' }, + ...Combinators, + ] + }), + features: () => p.multiselect({ + message: 'Which methods methods would you like to install?', + options: Features, + }) + }, { + onCancel: () => { + p.cancel('Operation cancelled.') + process.exit(0) + }, + }) + + let TARGET = path.join(process.cwd(), $.target) + let PACKAGE_JSON = path.join(TARGET, 'package.json') + let NODE_MODULES = path.join(TARGET, 'node_modules') + let TRAVERSABLE_SCHEMA = path.join(NODE_MODULES, '@traversable/schema-core') + + + if (!fs.existsSync(TARGET)) { + throw Error('The path ' + TARGET + ' does not exist.') + } + if (!fs.statSync(TARGET).isDirectory()) { + console.log(TARGET + ' is not a directory.') + process.exit(1) + } + try { + fs.accessSync(TARGET, fs.constants.R_OK | fs.constants.W_OK) + } catch (e) { + console.log('\n' + + 'Unable to write to path ' + + TARGET + + '. It\'s likely you do not have permissions for this folder.') + } + if (!fs.statSync(NODE_MODULES).isDirectory()) { + console.log('' + + '\n' + + 'The path ' + + TARGET + + ' does not contain a node_modules folder.' + + '\n' + ) + process.exit(1) + } + if (!fs.statSync(TRAVERSABLE_SCHEMA).isDirectory()) { + let installCore = await p.confirm({ + message: 'This script will install \'@traversable/schema\' to ' + NODE_MODULES + '. Proceed?', + initialValue: true, + }) + if (installCore === false) { + console.log('Exiting...') + process.exit(0) + } + } + + return $ +} + +function postinstall() { + let USER_DIR = process.env.INIT_CWD + let LIB_DIR = process.cwd() + console.log('USER_DIR', USER_DIR) + console.log('LIB_DIR', LIB_DIR) + + if (!USER_DIR) { + throw Error('Unable to infer the initial working directory') + } + try { + const USER_PKG_JSON = JSON.parse(fs.readFileSync(path.resolve(USER_DIR, 'package.json'), 'utf8')) + const LIB_PKG_JSON = JSON.parse(fs.readFileSync(path.resolve(LIB_DIR, 'package.json'), 'utf8')) + console.log('\n\nuser package.json: \n', USER_PKG_JSON) + console.log('\n\nlib package.json: \n', LIB_PKG_JSON) + } catch (e) { + console.log(e) + throw Error('Could not read package.json') + } + + setTimeout(() => { + x('echo hey') + + // let answers = prompt() + // console.log('answers', answers) + }, 3000) +} + +postinstall() + + +// type SchemaByName = typeof SchemaByName +// let SchemaByName = { +// any: t.any, +// array: t.array, +// bigint: t.bigint, +// boolean: t.boolean, +// eq: t.eq, +// integer: t.integer, +// intersect: t.intersect, +// never: t.never, +// null: t.null, +// number: t.number, +// object: t.object, +// optional: t.optional, +// record: t.record, +// string: t.string, +// symbol: t.symbol, +// tuple: t.tuple, +// undefined: t.undefined, +// union: t.union, +// unknown: t.unknown, +// void: t.void, +// } satisfies Record + +// type SchemaName = t.typeof +// let SchemaName = t.enum(...t.typeNames) + +// type Options = t.typeof +// interface Config extends Required> { +// schemas: { [K in S]: SchemaByName[K] } +// } + +// let Options = t.object({ +// cwd: t.optional(t.string), +// schemas: t.optional(t.array(SchemaName)), +// }) + +// // let getTarget = await p.text({ +// // message: 'Where would you like to install your schemas?', +// // placeholder: 'Path to an empty directory', +// // validate(value) { +// // if (!t.filter(t.string, (s) => s.startsWith('./'))) +// // return 'Please enter a relative path (e.g., \'./schemas\'' +// // }, +// // }) + +// // let multiselectOptions = Object +// // .entries(SchemaByName) +// // .map(([schemaName]) => ({ value: schemaName } satisfies p.Option)) + +// // .map(([schemaName]) => ({ value: schemaName } satisfies Prompt.Option)); + +// // let multiselect = p.multiselect({ +// // message: 'Select your schemas', +// // options: multiselectOptions, +// // required: true, +// // initialValues: Object.keys(SchemaName), +// // }) + + +// // let confirm = await p.confirm({ +// // message: 'Ready?', +// // }) + +// // p.intro('create-my-app'); +// // p.outro('You're all set!'); + + +// // type PromptGroup = { +// // [P in keyof T]: (opts: { +// // results: Prettify>>>; +// // }) => undefined | Promise; +// // }; + +// let RelativePath = t.filter(t.string, (s) => s.startsWith('./')) + +// let spinner = p.spinner() + +// let Primitives = [ +// { value: 'null', label: 'null' }, +// { value: 'undefined', label: 'undefined' }, +// { value: 'boolean', label: 'boolean' }, +// { value: 'number', label: 'number' }, +// { value: 'string', label: 'string' }, +// { value: 'bigint', label: 'bigint' }, +// { value: 'integer', label: 'integer' }, +// { value: 'unknown', label: 'unknown' }, +// { value: 'never', label: 'never' }, +// { value: 'void', label: 'void' }, +// { value: 'any', label: 'any' }, +// { value: 'symbol', label: 'symbol' }, +// ] as const satisfies p.Option[] + +// let Combinators = [ +// { value: 'object', label: 'object' }, +// { value: 'array', label: 'array' }, +// { value: 'union', label: 'union' }, +// { value: 'intersect', label: 'intersect' }, +// { value: 'record', label: 'record' }, +// { value: 'optional', label: 'optional' }, +// { value: 'tuple', label: 'tuple' }, +// { value: 'eq', label: 'eq' }, +// ] as const satisfies p.Option[] + +// let Features = [ +// { value: 'equals', label: 'equals' }, +// { value: 'toJsonSchema', label: 'toJsonSchema' }, +// { value: 'toString', label: 'toString' }, +// { value: 'validate', label: 'validate' }, +// ] as const satisfies p.Option[] + +// async function main() { +// p.intro(' 🌳 ' + color.underline(VERSION) + ' ') + +// let $ = await p.group({ +// target: () => p.text({ +// message: 'Where would you like to install your schemas?', +// placeholder: 'Path to an empty directory', +// validate(path) { +// if (!path.startsWith('./')) +// return 'Please enter a relative path (e.g., \'./schemas\'' +// }, +// }), +// primitives: () => p.multiselect({ +// message: 'Pick your primitives', +// initialValues: ['*'], +// options: [ +// { value: '*', label: '* (all)' }, +// ...Primitives, +// ], +// }), +// combinators: () => p.multiselect({ +// message: 'Pick your combinators', +// initialValues: ['*'], +// options: [ +// { value: '*', label: '* (all)' }, +// ...Combinators, +// ] +// }), +// features: () => p.multiselect({ +// message: 'Which methods methods would you like to install?', +// options: Features, +// }) +// }, { +// onCancel: () => { +// p.cancel('Operation cancelled.') +// process.exit(0) +// }, +// }) + +// let TARGET = path.join(process.cwd(), $.target) +// let PACKAGE_JSON = path.join(TARGET, 'package.json') +// let NODE_MODULES = path.join(TARGET, 'node_modules') +// let TRAVERSABLE_SCHEMA = path.join(NODE_MODULES, '@traversable/schema-core') + + +// if (!fs.existsSync(TARGET)) { +// throw Error('The path ' + TARGET + ' does not exist.') +// } +// if (!fs.statSync(TARGET).isDirectory()) { +// console.log(TARGET + ' is not a directory.') +// process.exit(1) +// } +// try { +// fs.accessSync(TARGET, fs.constants.R_OK | fs.constants.W_OK) +// } catch (e) { +// console.log('\n' +// + 'Unable to write to path ' +// + TARGET +// + '. It\'s likely you do not have permissions for this folder.') +// } +// if (!fs.statSync(NODE_MODULES).isDirectory()) { +// console.log('' +// + '\n' +// + 'The path ' +// + TARGET +// + ' does not contain a node_modules folder.' +// + '\n' +// ) +// process.exit(1) +// } +// if (!fs.statSync(TRAVERSABLE_SCHEMA).isDirectory()) { +// let installCore = await p.confirm({ +// message: 'This script will install \'@traversable/schema\' to ' + NODE_MODULES + '. Proceed?', +// initialValue: true, +// }) +// if (installCore === false) { +// console.log('Exiting...') +// process.exit(0) +// } +// } + +// // console.log('target', TARGET) +// // console.log('NODE_MODULES', NODE_MODULES) +// console.log('@traversable/schema path:', TRAVERSABLE_SCHEMA) + +// // console.log('primitives', primitives) +// // console.log('combinators', combinators) +// } + +// await main() + + +// // let defaultOptions = { +// // cwd: process.cwd(), +// // schemas: SchemaByName, +// // } satisfies Config + +// // function configure(options?: Options): Config +// // function configure(options?: Options) { +// // if (!options) return defaultOptions +// // else if (Options(options)) { +// // if (options.schemas) { +// // let schemas: { [K in SchemaName]+?: SchemaByName[keyof SchemaByName] } = {} +// // for (let schemaName of options.schemas) { +// // schemas[schemaName] = SchemaByName[schemaName] +// // } +// // return { +// // cwd: options.cwd ?? defaultOptions.cwd, +// // schemas, +// // } satisfies Config +// // } +// // } +// // else return defaultOptions +// // } + +// // interface Error extends globalThis.Error { +// // tag: Tag +// // } + +// // function createError(tag: Tag, defineError: ($: Config) => string): ($: Config) => Error +// // function createError(tag: Tag, defineError: ($: Config) => string): ($: Config) => globalThis.Error { +// // return ($: Config) => { +// // let error: globalThis.Error & { tag?: string } = globalThis.Error(defineError($)) +// // error.tag = tag +// // return error +// // } +// // } + +// // let UnableToResolveCwd = createError( +// // 'UnableToResolveCwd', +// // ($) => 'The path ' + $.cwd + ' does not exist.' +// // ) + +// // let NoWriteAccessToCwd = createError( +// // 'NoWriteAccessToCwd', +// // ($) => 'Unable to write to path ' + $.cwd + '. It\'s likely you do not have permissions for this folder.' +// // ) + +// // let UnableToFindPackageJson = createError( +// // 'UnableToFindPackageJson', +// // ($) => 'Unable to locate a package.json file in ' + $.cwd + '.' +// // ) + +// // let MalformedPackageJson = createError( +// // 'UnableToFindPackageJson', +// // ($) => 'Unable to read package.json file at ' + $.cwd + '.' +// // ) + +// // function init(options?: Options) { +// // let $ = configure(options) +// // let ERRORS = Array.of>() +// // if (!fs.existsSync($.cwd)) ERRORS.push(UnableToResolveCwd($)) + +// // try { fs.accessSync($.cwd, fs.constants.R_OK | fs.constants.W_OK) } +// // catch (e) { ERRORS.push(NoWriteAccessToCwd($)) } + +// // let PACKAGE_JSON_PATH = path.resolve($.cwd, 'package.json') +// // if (!fs.existsSync(PACKAGE_JSON_PATH)) ERRORS.push(UnableToFindPackageJson($)) + +// // try { let PACKAGE_JSON = fs.readFileSync(PACKAGE_JSON_PATH) } +// // catch (e) { ERRORS.push(MalformedPackageJson($)) } + +// // if (ERRORS.length > 0) { +// // for (let error of ERRORS) { +// // console.error('\n[Error]: ' + error.tag + '\r\n\n' + error.message) +// // console.debug() +// // } +// // process.exit(1) +// // } + +// // } diff --git a/packages/schema-generator/src/define.ts b/packages/schema-generator/src/define.ts new file mode 100644 index 00000000..0004e1bc --- /dev/null +++ b/packages/schema-generator/src/define.ts @@ -0,0 +1,7 @@ +import type { ParsedExtensionFile } from './parser.js' + +export type DefineExtension = Required + +export function defineExtension(extension: DefineExtension): DefineExtension { + return extension +} diff --git a/packages/schema-generator/src/exports.ts b/packages/schema-generator/src/exports.ts new file mode 100644 index 00000000..9fb3e69e --- /dev/null +++ b/packages/schema-generator/src/exports.ts @@ -0,0 +1,34 @@ +export * from './version.js' +export * as P from './parser-combinators.js' + +export type { DefineExtension } from './define.js' +export { defineExtension } from './define.js' + +export type { + ExtensionsBySchemaName, + ParsedImport, + ParsedImports, +} from './imports.js' + +export { + deduplicateImports, + makeImport, + makeImports, + makeImportsBySchemaName, +} from './imports.js' + +export { + generateSchemas, + writeSchemas, +} from './generate.js' + +export type { + ParsedSourceFile, +} from './parser.js' + +export { + createProgram, + parseFile, + parseSourceFile, + replaceExtensions, +} from './parser.js' diff --git a/packages/schema-generator/src/extensions/any.ts b/packages/schema-generator/src/extensions/any.ts new file mode 100644 index 00000000..c4ecbb88 --- /dev/null +++ b/packages/schema-generator/src/extensions/any.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/src/extensions/array.ts b/packages/schema-generator/src/extensions/array.ts new file mode 100644 index 00000000..0927d4dd --- /dev/null +++ b/packages/schema-generator/src/extensions/array.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + toString: toString + equals: equals + toJsonSchema: toJsonSchema + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toJsonSchema, + validate, + toString, + equals, +} diff --git a/packages/schema-generator/src/extensions/bigint.ts b/packages/schema-generator/src/extensions/bigint.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema-generator/src/extensions/bigint.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/src/extensions/boolean.ts b/packages/schema-generator/src/extensions/boolean.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema-generator/src/extensions/boolean.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/src/extensions/eq.ts b/packages/schema-generator/src/extensions/eq.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema-generator/src/extensions/eq.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/src/extensions/equals.ts b/packages/schema-generator/src/extensions/equals.ts new file mode 100644 index 00000000..013153b1 --- /dev/null +++ b/packages/schema-generator/src/extensions/equals.ts @@ -0,0 +1,3 @@ +export { dummyEquals as equals } +let dummyEquals = (..._: any) => { throw Error('Called dummy equals') } +interface dummyEquals<_ = any> { } diff --git a/packages/schema-generator/src/extensions/integer.ts b/packages/schema-generator/src/extensions/integer.ts new file mode 100644 index 00000000..1f68a7e8 --- /dev/null +++ b/packages/schema-generator/src/extensions/integer.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toString, +} + +export let Extensions = { + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/src/extensions/intersect.ts b/packages/schema-generator/src/extensions/intersect.ts new file mode 100644 index 00000000..0927d4dd --- /dev/null +++ b/packages/schema-generator/src/extensions/intersect.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + toString: toString + equals: equals + toJsonSchema: toJsonSchema + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toJsonSchema, + validate, + toString, + equals, +} diff --git a/packages/schema-generator/src/extensions/never.ts b/packages/schema-generator/src/extensions/never.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema-generator/src/extensions/never.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/src/extensions/null.ts b/packages/schema-generator/src/extensions/null.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema-generator/src/extensions/null.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/src/extensions/number.ts b/packages/schema-generator/src/extensions/number.ts new file mode 100644 index 00000000..1a06ace0 --- /dev/null +++ b/packages/schema-generator/src/extensions/number.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + toString: toString + equals: equals + toJsonSchema: toJsonSchema + validate: validate +} + +export let Definitions = { + toString, + equals, +} + +export let Extensions = { + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/src/extensions/object.ts b/packages/schema-generator/src/extensions/object.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema-generator/src/extensions/object.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/src/extensions/of.ts b/packages/schema-generator/src/extensions/of.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema-generator/src/extensions/of.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/src/extensions/optional.ts b/packages/schema-generator/src/extensions/optional.ts new file mode 100644 index 00000000..0e7c4478 --- /dev/null +++ b/packages/schema-generator/src/extensions/optional.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} + diff --git a/packages/schema-generator/src/extensions/record.ts b/packages/schema-generator/src/extensions/record.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema-generator/src/extensions/record.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/src/extensions/string.ts b/packages/schema-generator/src/extensions/string.ts new file mode 100644 index 00000000..c64c1266 --- /dev/null +++ b/packages/schema-generator/src/extensions/string.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + toString, + equals, +} + +export let Extensions = { + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/src/extensions/symbol.ts b/packages/schema-generator/src/extensions/symbol.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema-generator/src/extensions/symbol.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/src/extensions/toJsonSchema.ts b/packages/schema-generator/src/extensions/toJsonSchema.ts new file mode 100644 index 00000000..16341d68 --- /dev/null +++ b/packages/schema-generator/src/extensions/toJsonSchema.ts @@ -0,0 +1,3 @@ +export { dummyToJsonSchema as toJsonSchema } +let dummyToJsonSchema = (..._: any) => { throw Error('Called dummy toJsonSchema') } +interface dummyToJsonSchema<_ = any> { } diff --git a/packages/schema-generator/src/extensions/toString.ts b/packages/schema-generator/src/extensions/toString.ts new file mode 100644 index 00000000..d7215f1f --- /dev/null +++ b/packages/schema-generator/src/extensions/toString.ts @@ -0,0 +1,3 @@ +export { dummyToString as toString } +let dummyToString = (..._: any) => { throw Error('Called dummy toString') } +interface dummyToString<_ = any> { } diff --git a/packages/schema-generator/src/extensions/tuple.ts b/packages/schema-generator/src/extensions/tuple.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema-generator/src/extensions/tuple.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/src/extensions/undefined.ts b/packages/schema-generator/src/extensions/undefined.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema-generator/src/extensions/undefined.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/src/extensions/union.ts b/packages/schema-generator/src/extensions/union.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema-generator/src/extensions/union.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/src/extensions/unknown.ts b/packages/schema-generator/src/extensions/unknown.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema-generator/src/extensions/unknown.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/src/extensions/validate.ts b/packages/schema-generator/src/extensions/validate.ts new file mode 100644 index 00000000..ba09b9fe --- /dev/null +++ b/packages/schema-generator/src/extensions/validate.ts @@ -0,0 +1,3 @@ +export { dummyValidate as validate } +let dummyValidate = (..._: any) => { throw Error('Called dummy validate') } +interface dummyValidate<_ = any> { } diff --git a/packages/schema-generator/src/extensions/void.ts b/packages/schema-generator/src/extensions/void.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema-generator/src/extensions/void.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/src/generate.ts b/packages/schema-generator/src/generate.ts new file mode 100644 index 00000000..94c4d397 --- /dev/null +++ b/packages/schema-generator/src/generate.ts @@ -0,0 +1,115 @@ +import * as fs from 'node:fs' +import { + fn, + omit_ as omit, + pick_ as pick, +} from "@traversable/registry" + +import type { ParsedSourceFile, ParsedExtensionFile } from './parser.js' +import { + parseExtensionFile, + parseFile, + replaceExtensions, +} from './parser.js' +import { makeImports } from './imports.js' +import { VERSION } from './version.js' + +let isKeyOf = (k: keyof any, t: T): k is keyof T => !!t && typeof t === 'object' && k in t + +let makeSchemaFileHeader = (schemaName: string, pkgName: string) => [ + ` +/** + * t.${schemaName.endsWith('_') ? schemaName.slice(-1) : schemaName} schema + * made with ᯓᡣ𐭩 by ${pkgName} + */ +`.trim(), +].join('\n') + +let makeHeaderComment = (header: string) => [ + `///////` + '/'.repeat(header.length) + `///////`, + `/// ` + header + ` ///`, +].join('\n') + +let makeFooterComment = (footer: string) => [ + `/// ` + footer + ` ///`, + `///////` + '/'.repeat(footer.length) + `///////`, +].join('\n') + +function makeSchemaFileContent( + schemaName: string, + parsedSourceFiles: Record, + parsedExtensionFile: ParsedExtensionFile, + imports: string, + pkgName: string, +) { + let core = replaceExtensions(pick(parsedSourceFiles, 'core').core.body, parsedExtensionFile) + let noCore = omit(parsedSourceFiles, 'core') + let files = fn.pipe( + fn.map(noCore, (source) => source.body.trim()), + Object.entries, + fn.map(([k, body]) => [ + makeHeaderComment(k), + body, + makeFooterComment(k), + ].join('\n')), + ) + + return [ + makeSchemaFileHeader(schemaName, pkgName), + imports, + ...files.map((ext) => '\r' + ext), + '\r', + core, + ] +} + +export function generateSchemas>>( + sources: T, + targets: Record, + pkgNameForHeader: string, +): [path: string, content: string][] + +export function generateSchemas( + sources: Record>, + targets: Record, + pkgNameForHeader: string, +): [path: string, content: string][] { + let parsedSourceFiles = fn.map(sources, fn.map(parseFile)) + let exts = fn.map(sources, (src) => pick(src, 'extension').extension) + let noExts = fn.map(parsedSourceFiles, (src) => omit(src, 'extension')) + let parsedExtensionFiles = fn.map(exts, parseExtensionFile) + let importsBySchemaName = makeImports(fn.map(parsedSourceFiles, fn.map((_) => _.imports))) + let contentBySchemaName = fn.map( + noExts, + (v, k) => makeSchemaFileContent( + k, + v, + parsedExtensionFiles[k], + importsBySchemaName[k], + pkgNameForHeader, + ) + ) + + return Object.entries(contentBySchemaName).map(([k, content]) => { + if (!isKeyOf(k, targets)) throw Error('No write target found for schema type "' + k + '"') + else return [targets[k], content.join('\n') + '\n'] satisfies [any, any] + }) +} + +export function writeSchemas>>( + sources: T, + targets: Record, + pkgNameForHeader: string +): void +export function writeSchemas( + ...args: [ + sources: Record>, + targets: Record, + pkgNameForHeader: string + ] +): void { + let schemas = generateSchemas(...args) + for (let [target, content] of schemas) { + void fs.writeFileSync(target, content) + } +} diff --git a/packages/schema-generator/src/imports.ts b/packages/schema-generator/src/imports.ts new file mode 100644 index 00000000..ed408dbc --- /dev/null +++ b/packages/schema-generator/src/imports.ts @@ -0,0 +1,177 @@ +import type * as T from '@traversable/registry' +import { fn, Array_isArray } from "@traversable/registry" +import { t } from '@traversable/schema-core' + +let stringComparator: T.Comparator = (l, r) => l.localeCompare(r) + +export type Import = t.typeof +export let Import = t.object({ + named: t.array(t.string), + namespace: t.optional(t.string), +}) + +export type Imports = t.typeof +export let Imports = t.object({ type: Import, term: Import }) + +export type SchemaDependencies = t.typeof +export let SchemaDependencies = t.record(t.record(Imports)) + +export type ExtensionsBySchemaName = T.Record< + 'array' | 'string', + T.Record< + 'core' | 'equals', + T.Record< + '@traversable/registry' | '@traversable/schema-core', + Imports + > + > +> + +export let ExtensionsBySchemaName = t.record(SchemaDependencies) + +export type DeduplicatedImport = { + named: Set + namespace: Set +} + +export type DeduplicatedImports = { + type: DeduplicatedImport + term: DeduplicatedImport +} + +export type ParsedImport = { + named: string[] + namespace: string[] +} +export type ParsedImports = { + type: ParsedImport + term: ParsedImport +} + +let makeSpacing = (singleLine: boolean) => ({ + bracket: singleLine ? ' ' : '\n', + indent: singleLine ? '' : ' ', + separator: singleLine ? ', ' : ',\n', + space: singleLine ? ' ' : ' ', +}) + +export function makeImport(dependency: string, { term, type }: ParsedImports, maxPerLine = 3): string[] { + let out = Array.of() + if (Array_isArray(type.namespace)) + out.push(...type.namespace.map((ns) => `import type * as ${ns} from '${dependency}'`)) + if (Array_isArray(term.namespace)) + out.push(...term.namespace.map((ns) => `import * as ${ns} from '${dependency}'`)) + if (Array.isArray(type.named) && type.named.length > 0) { + let singleLine = type.named.length <= maxPerLine + let $$ = makeSpacing(singleLine) + out.push(`import type {${$$.bracket}` + + type.named + .map((_) => typeof _ === 'string' ? `${$$.indent}${_}` : `${$$.space}${_[0]} as ${_[1]}`) + .join($$.separator) + `${$$.bracket}} from '${dependency}'`) + } + if (Array.isArray(term.named) && term.named.length > 0) { + let singleLine = term.named.length <= maxPerLine + let $$ = makeSpacing(singleLine) + out.push(`import {${singleLine ? ' ' : '\n'}` + + term.named + .map((_) => typeof _ === 'string' ? `${$$.indent}${_}` : `${$$.space}${_[0]} as ${_[1]}`) + .join($$.separator) + `${$$.bracket}} from '${dependency}'`) + } + return out +} + +let getDependenciesFromImportsForSchema = (schemaExtensions: ExtensionsBySchemaName[keyof ExtensionsBySchemaName]) => { + if (!schemaExtensions) return [] + else { + let xs = Object.values(schemaExtensions) + .filter((_) => !!_) + .flatMap((_) => Object.keys(_)) + return Array.from(new Set(xs)) + } +} + +export function deduplicateImports(extensionsBySchemaName: ExtensionsBySchemaName): Record> { + return fn.map( + extensionsBySchemaName, + (extension) => { + if (!extension) return {} + + let init: Record = {} + let pkgNames = getDependenciesFromImportsForSchema(extension) + + for (let pkgName of pkgNames) { + init[pkgName] = { + type: { + named: new Set(), + namespace: new Set(), + }, + term: { + named: new Set(), + namespace: new Set(), + }, + } + } + + fn.map(extension, (imports) => { + if (!imports) return {} + + fn.map(imports, (imports, pkgName) => { + if (!imports) return {} + let { type, term } = imports + + for (let name of type.named) { + if (!init[pkgName].term.named.has(name)) + init[pkgName].type.named.add(name) + } + if (t.string(type.namespace)) { + if (!init[pkgName].term.namespace.has(type.namespace)) + init[pkgName].type.namespace.add(type.namespace) + } + for (let name of term.named) { + if (init[pkgName].type.named.has(name)) init[pkgName].type.named.delete(name) + init[pkgName].term.named.add(name) + } + if (t.string(term.namespace)) { + if (init[pkgName].type.namespace.has(term.namespace)) init[pkgName].type.namespace.delete(term.namespace) + init[pkgName].term.namespace.add(term.namespace) + } + }) + }) + + return init + } + ) +} + +export function makeImportsBySchemaName(extensionsBySchemaName: S) { + return Object.entries(deduplicateImports(extensionsBySchemaName)) + .map(([schemaName, schemaDeps]) => [ + schemaName, + fn.map( + schemaDeps, + ({ type, term }, depName) => makeImport( + depName, { + type: { + named: [...type.named.values()].sort(stringComparator), + namespace: [...type.namespace.values()].sort(stringComparator), + }, + term: { + named: [...term.named.values()].sort(stringComparator), + namespace: [...term.namespace.values()].sort(stringComparator), + }, + }) + ) + ] satisfies [any, any]) + .reduce>( + (acc, [k, v]) => (acc[k] = Object.values(v).filter((_) => _.length > 0).map((_) => _.join('\n')), acc), + {} + ) +} + +export function makeImports(extensionsBySchemaName: S): { [K in keyof S]: string } +export function makeImports(extensionsBySchemaName: ExtensionsBySchemaName): {} { + return fn.map( + makeImportsBySchemaName(extensionsBySchemaName), + (importArray) => importArray.join('\n'), + ) +} diff --git a/packages/schema-generator/src/index.ts b/packages/schema-generator/src/index.ts new file mode 100644 index 00000000..410a4bcb --- /dev/null +++ b/packages/schema-generator/src/index.ts @@ -0,0 +1 @@ +export * from './exports.js' diff --git a/packages/schema-generator/src/parser-combinators.ts b/packages/schema-generator/src/parser-combinators.ts new file mode 100644 index 00000000..10b315e1 --- /dev/null +++ b/packages/schema-generator/src/parser-combinators.ts @@ -0,0 +1,361 @@ +export interface Success { + success: true + index: number + value: T +} + +export interface Failure { + success: false + index: number +} + +export type Result = Success | Failure + +export type ResultTypes = U extends [infer Head, ...infer Tail] ? [Parser.typeof, ...ResultTypes] : []; + +export type ParserHandler = (input: string, index: number, state: any) => Result + +export type Options = { + handler: ParserHandler + name?: string + info?: string +} + +export type Found = { + index: number + input: string + result: Result +} + +export interface ParserContext { + handler(input: string, index: number, state: any): Result +} + +let PATTERN = { + Alpha: /[a-z]/i, + Digit: /[0-9]/, + Whitespace: /\s+/, + Alphanum: /[a-zA-Z0-9]/, + Id: /^[_$a-zA-Z][_$a-zA-Z0-9]*/, +} satisfies { [x: string]: RegExp } + +export function success(index: number, value: T): Success { + return { + success: true, + value: value, + index: index, + } +} + +export function failure(index: number): Failure { + return { + success: false, + index: index + } +} + +export function succeed(value: U): Parser { + return Parser.new((_, index) => { + return success(index, value); + }, 'succeeded'); +} + +function withTrace(handler: ParserHandler, tag: string): ParserHandler { + return (input, index, state) => { + if (state.trace) { + const pos = `${index}` + console.log(`${pos.padEnd(6, ' ')}enter ${tag}`) + const result = handler(input, index, state) + if (result.success) { + const pos = `${index}:${result.index}` + console.log(`${pos.padEnd(6, ' ')}success ${tag}`) + } else { + const pos = `${index}` + console.log(`${pos.padEnd(6, ' ')}failure ${tag}`) + } + return result + } + return handler(input, index, state) + } +} + +export class Parser { + static new + : ( + handler: ParserHandler, + tag?: string, + info?: string, + ) => Parser + = (handler, tag, info) => new Parser(handler, tag ?? '', info) + + find(text: string): Found | undefined + find(text: string, state: State): Found | undefined + find(text: string, state?: { [x: string]: unknown }) { return find(this, text, state) } + + many(): Parser + many(options: many.Options): Parser + many($: many.Options = {}): Parser { return many(this, $) } + + map(f: (value: S) => T): Parser { return map(this, f) } + + parse(input: string, state?: State): Result + parse(input: string, state = {}): Result { return parse(this, input, state) } + + run(input: string): Result + run(input: string, state?: State, offset?: number, debug?: boolean): Result + run(input: string, state = {}, offset: number = 0, debug = false): Result { + let handler = debug ? withTrace(this.handler, this.tag) : this.handler + return handler(input, offset, state) + } + + times(n: 1): Parser<[S]> + times(n: 2): Parser<[S, S]> + times(n: 3): Parser<[S, S, S]> + times(n: 4): Parser<[S, S, S, S]> + times(n: 5): Parser<[S, S, S, S, S]> + times(n: 6): Parser<[S, S, S, S, S, S]> + times(n: 7): Parser<[S, S, S, S, S, S, S]> + times(n: 8): Parser<[S, S, S, S, S, S, S, S]> + times(n: 9): Parser<[S, S, S, S, S, S, S, S, S]> + times(n: 0): Parser<[]> + times(n: number): Parser + times(n: number): Parser { return times(this, n) } + + trim(): Parser { return index([spaces, this, spaces], 1) } + + private constructor( + private handler: ParserHandler, + public tag: string, + public info?: string, + ) { } +} + +export declare namespace Parser { + export { typeOf as typeof } + export type typeOf

= P extends Parser ? T : never +} + +function find(parser: Parser, text: string): Found | undefined +function find>(parser: Parser, text: string, state: State): Found | undefined +function find>(parser: Parser, text: string, state?: State): Found | undefined +function find(parser: Parser, text: string, state?: Record): Found | undefined { + for (let index = 0; index < text.length; index++) { + const innerState = Object.assign({}, state) + const result = parser.run(text, innerState, index) + if (result.success) return { + index, + result, + input: text, + } + } + return undefined; +} + +function map(parser: Parser, f: (s: S) => T): Parser { + return Parser.new((input, index, state) => { + const result = parser.run(input, state, index) + if (!result.success) { + return result + } + return success(result.index, f(result.value)) + }, 'map', parser.tag) +} + +function parse(parser: Parser, input: string, state: State): Result +function parse(p: Parser, input: string, state: State): Result { + let parser = index([p, eof], 0) + return parser.run(input, state, 0) +} + +function times(parser: Parser, n: number): Parser { + if (n < 1 || !Number.isInteger(n)) + throw Error('Expected "n" to be a non-negative, non-zero integer') + return Parser.new((input, ix, state) => { + let result + let cursor = ix + const seed = Array.of() + while (cursor < input.length) { + result = parser.run(input, state, cursor) + if (!result.success) break + cursor = result.index + seed.push(result.value) + } + if (seed.length < n) return failure(cursor) + else return success(cursor, seed) + }) +} + + +export function seq(...parsers: { [I in keyof T]: Parser }): Parser +export function seq(...parsers: Parser[]): Parser { + return Parser.new((input, index, state) => { + let result + let latestIndex = index + let seed = Array.of() + for (let i = 0; i < parsers.length; i++) { + result = parsers[i].run(input, state, latestIndex) + if (!result.success) { + return result + } + latestIndex = result.index + seed.push(result.value) + } + return success(latestIndex, seed) + }, 'seq', `length=${parsers.length}`) +} + +export function alt(...parsers: { [I in keyof T]: Parser }): Parser { + return Parser.new((input, index, state) => { + let result + for (let ix = 0, len = parsers.length; ix < len; ix++) { + result = parsers[ix].run(input, state, index) + if (result.success) return result + } + return failure(index) + }, `alt length=${parsers.length}`) +} + +export function string(value: T): Parser { + return Parser.new((input, index) => { + if ((input.length - index) < value.length) + return failure(index) + else if (input.slice(index, index + value.length) !== value) + return failure(index) + else return success(index + value.length, value) + }, `string=${value}`) +} + +export function index< + T extends readonly unknown[], + I extends keyof T & number +>( + parsers: { [I in keyof T]: Parser }, + index: I +): Parser { + return seq(...parsers).map(values => values[index]); +} + +export function regexp(pattern: RegExp): Parser { + const expr = RegExp(`^(?:${pattern.source})`, pattern.flags); + return Parser.new((input, index) => { + const text = input.slice(index); + const result = expr.exec(text); + if (result == null) { + return failure(index); + } + return success(index + result[0].length, result[0]); + }, `pattern=${pattern}`); +} + +export type Char = [T] extends [`${string}${infer T}`] ? T extends '' ? string : never : never +export function char(): Parser +export function char>(char: T): Parser +export function char(char: string = '') { + return char === '' ? anyChar : string(char) +} + +export const anyChar = Parser.new((input, index) => { + if ((input.length - index) < 1) + return failure(index) + const value = input[index] + return success(index + 1, value) +}, 'any') + + +export function not(parser: Parser): Parser { + return Parser.new((input, index, state) => { + const result = parser.run(input, state, index) + return !result.success + ? success(index, null) + : failure(index) + }, 'not') +} + +export function many(parser: Parser, options?: many.Options): Parser +export function many(parser: Parser, $: many.Options = {}): Parser { + if (!!$.not) return many( + index([not($.not), parser], 1), + { min: $.min, max: $.max } + ) + else return Parser.new((input, ix, state) => { + let result + let cursor = ix + const seed = Array.of() + while (cursor < input.length) { + result = parser.run(input, state, cursor) + if (!result.success) break + cursor = result.index + seed.push(result.value) + } + if ($.min != null && seed.length < $.min) + return failure(cursor) + else if ($.max != null && seed.length > $.max) + return failure(cursor) + else return success(cursor, seed) + }, 'many') +} + +export declare namespace many { + type Options = { + min?: number + max?: number + not?: Parser + } +} + +export let trim = (parser: Parser) => index([ + spaces, + parser, + spaces, +], 1) + +export let eof + : Parser + = Parser.new( + (input, index) => index >= input.length ? success(index, null) : failure(index), + 'eof' + ) + +export let optional + : (parser: Parser) => Parser + = (parser) => alt(parser, succeed(null)) + +export let CR = char('\r') +export let LF = char('\n') +export let CRLF = string('\r\n') +export let newline = alt(CRLF, CR, LF) +export let whitespace = regexp(PATTERN.Whitespace) +export let spaces = optional(regexp(/[ \t\r\n]/).many()) + +export let alpha = regexp(PATTERN.Alpha) +export let digit = regexp(PATTERN.Digit) +export let alphanum = regexp(PATTERN.Alphanum) +export let ident = regexp(PATTERN.Id) + +export function lazy(thunk: () => Parser, tag?: string): Parser { + return Parser.new((input, index, state) => { + let parser = thunk() + return parser.run(input, state, index) + }) +} + +export type Language = { [K in keyof U]: U[K] extends Parser ? U[K] : never }; + +export type LanguageSource */> = { [K in keyof U]: (lang: U) => U[K] }; + +export function language>(source: LanguageSource): T +export function language(source: { [x: string]: (go: Record>) => Parser }) { + let lang: Record> = {} + for (const k of Object.keys(source)) { + let loop = source[k] + lang[k] = lazy(() => { + const parser = loop(lang) + if (parser == null || !(parser instanceof Parser)) { + throw new Error('syntax must return a Parser.') + } + parser.tag = `${parser.tag} key=${k}` + return parser + }) + } + return lang +} diff --git a/packages/schema-generator/src/parser.ts b/packages/schema-generator/src/parser.ts new file mode 100644 index 00000000..e440ef94 --- /dev/null +++ b/packages/schema-generator/src/parser.ts @@ -0,0 +1,244 @@ +import * as fs from 'node:fs' +import ts from 'typescript' + +import type { Imports } from './imports.js' +import * as P from './parser-combinators.js' +import { Array_isArray, Comparator } from '@traversable/registry' + +export type ParsedSourceFile = { + imports: Record + body: string +} + +let LIB_FILE_NAME = '/lib/lib.d.ts' +let LIB = [].join('\n') + +export let VarName = { + Type: 'Types', + Def: 'Definitions', + Ext: 'Extensions', +} as const + +export type ParsedExtensionFile = never | { + [VarName.Type]?: string + [VarName.Def]?: string + [VarName.Ext]?: string +} + +export let typesMarker = `//<%= ${VarName.Type} %>` as const +export let definitionsMarker = `//<%= ${VarName.Def} %>` as const +export let extensionsMarker = `//<%= ${VarName.Ext} %>` as const + +export function createProgram(source: string): ts.Program { + let filename = '/source.ts' + let files = new Map() + files.set(filename, source) + files.set(LIB_FILE_NAME, LIB) + return ts.createProgram( + [filename], { + target: ts.ScriptTarget.ESNext, + module: ts.ModuleKind.ESNext, + strict: true, + noEmit: true, + isolatedModules: true, + types: [], + }, { + fileExists: (filename) => files.has(filename), + getCanonicalFileName: (f) => f.toLowerCase(), + getCurrentDirectory: () => '/', + getDefaultLibFileName: () => LIB_FILE_NAME, + getDirectories: () => [], + getNewLine: () => '\n', + getSourceFile: (filename, options) => { + let content = files.get(filename) + if (content === void 0) throw Error('missing file') + return ts.createSourceFile(filename, content, options) + }, + readFile: (filename) => files.get(filename), + useCaseSensitiveFileNames: () => false, + writeFile: () => { throw Error('unimplemented') }, + }) +} + +export function parseFile(sourceFilePath: string): ParsedSourceFile { + let source = fs.readFileSync(sourceFilePath).toString('utf-8') + let program = createProgram(source) + /* initialize the type checker, otherwise we can't perform a traversal */ + let checker = program.getTypeChecker() + let sourceFile = program.getSourceFiles()[1] + return parseSourceFile(sourceFile) +} + +let isDefinitionsVariable = (node: ts.Node): node is ts.VariableStatement => + ts.isVariableStatement(node) + && !!node.declarationList.declarations.find((declaration) => declaration.name.getText() === VarName.Def) + + +let isExtensionsVariable = (node: ts.Node): node is ts.VariableStatement => + ts.isVariableStatement(node) + && !!node.declarationList.declarations.find((declaration) => declaration.name.getText() === VarName.Ext) + +let isTypeDeclaration = (node: ts.Node): node is ts.InterfaceDeclaration | ts.TypeAliasDeclaration => + (ts.isInterfaceDeclaration(node) && findIdentifier(node)?.getText() === VarName.Type) + || (ts.isTypeAliasDeclaration(node) && findIdentifier(node)?.getText() === VarName.Type) + +let findIdentifier = (node: ts.Node) => node.getChildren().find(ts.isIdentifier) + +type ParsedTypesNode = { [VarName.Type]: ts.InterfaceDeclaration | ts.TypeAliasDeclaration } +type ParsedDefinitionsNode = { [VarName.Def]: ts.VariableStatement } +type ParsedExtensionsNode = { [VarName.Ext]: ts.VariableStatement } +type ParsedNodes = + & ParsedTypesNode + & ParsedDefinitionsNode + & ParsedExtensionsNode + +export function parseExtensionFile(sourceFilePath: string): ParsedExtensionFile { + let source = fs.readFileSync(sourceFilePath).toString('utf-8') + let program = createProgram(source) + /* initialize the type checker, otherwise we can't perform a traversal */ + void program.getTypeChecker() + let sourceFile = program.getSourceFiles()[1] + let nodes: Partial = {} + let out: ParsedExtensionFile = {} + + void ts.forEachChild(sourceFile, (node) => { + if (isTypeDeclaration(node)) nodes[VarName.Type] = node + if (isDefinitionsVariable(node)) nodes[VarName.Def] = node + if (isExtensionsVariable(node)) nodes[VarName.Ext] = node + }) + if (nodes[VarName.Type]) { + let text = nodes[VarName.Type]!.getText() + out[VarName.Type] = text.slice(text.indexOf('{') + 1, text.lastIndexOf('}')) + } + if (nodes[VarName.Def]) { + let text = nodes[VarName.Def]!.getText() + out[VarName.Def] = text.slice(text.indexOf('{') + 1, text.lastIndexOf('}')) + } + if (nodes[VarName.Ext]) { + let text = nodes[VarName.Ext]!.getText() + out[VarName.Ext] = text.slice(text.indexOf('{') + 1, text.lastIndexOf('}')) + } + return out +} + +export function parseSourceFile(sourceFile: ts.SourceFile): ParsedSourceFile { + let imports: Record = {} + let bodyStart: number = 0 + + void ts.forEachChild(sourceFile, (node) => { + if (ts.isImportDeclaration(node)) { + let importClause = node.importClause + if (importClause === undefined) return void 0 + if (node.end > bodyStart) void (bodyStart = node.end) + let dependencyName = node.moduleSpecifier.getText().slice(`"`.length, -`"`.length) + void (imports[dependencyName] ??= { term: { named: [] }, type: { named: [] } }) + let dep = imports[dependencyName] + void importClause.forEachChild((importNode) => { + if (ts.isNamedImports(importNode)) { + void importNode.forEachChild((specifier) => { + if (importClause.isTypeOnly) + void dep.type.named.push(specifier.getText()) + else + void dep.term.named.push(specifier.getText()) + }) + } else if (ts.isNamespaceImport(importNode)) { + if (importClause.isTypeOnly) + void (dep.type.namespace = importNode.name.text) + else + void (dep.term.namespace = importNode.name.text) + } + }) + } + }) + + let body = sourceFile.getFullText().slice(bodyStart).trim() + + return { imports, body } +} + +let parseTypeMarker = P.seq(P.char('{'), P.spaces, P.string(typesMarker), P.spaces, P.char('}')) +let parseDefinitionsMarker = P.seq(P.char('{'), P.spaces, P.string(definitionsMarker), P.spaces, P.char('}')) +let parseExtensionsMarker = P.seq(P.char('{'), P.spaces, P.string(extensionsMarker), P.spaces, P.char('}')) + +type Splice = { + start: number + end: number + content: string + offset: number + firstLineOffset: number +} + +let spliceComparator: Comparator = (l, r) => l.start < r.start ? -1 : r.start < l.start ? 1 : 0 + +function splice3(source: string, first: Splice, second: Splice, third: Splice) { + return '' + + source.slice(0, first.start) + + '\n' + ' '.repeat(first.firstLineOffset) + first.content.split('\n').map((_) => ' '.repeat(first.offset) + _).join('\n').trimStart() + + source.slice(first.end, second.start) + + '\n' + ' '.repeat(second.firstLineOffset) + second.content.split('\n').map((_) => ' '.repeat(second.offset) + _).join('\n').trimStart() + + source.slice(second.end, third.start) + + '\n' + ' '.repeat(third.firstLineOffset) + third.content.split('\n').map((_) => ' '.repeat(third.offset) + _).join('\n').trimStart() + + source.slice(third.end) +} + +export function replaceExtensions(source: string, parsedExtensionFile: ParsedExtensionFile) { + let typeMarker = parseTypeMarker.find(source) + let defMarker = parseDefinitionsMarker.find(source) + let extMarker = parseExtensionsMarker.find(source) + + if (typeMarker == null) throw Error(`missing ${VarName.Type} marker`) + if (defMarker == null) throw Error(`missing ${VarName.Def} marker`) + if (extMarker == null) throw Error(`missing ${VarName.Ext} marker`) + + if (parsedExtensionFile[VarName.Type] == null) throw Error(`missing ${VarName.Type} text`) + if (parsedExtensionFile[VarName.Def] == null) throw Error(`missing ${VarName.Def} text`) + if (parsedExtensionFile[VarName.Ext] == null) throw Error(`missing ${VarName.Ext} text`) + + if (!typeMarker.result.success) throw Error(`parse for ${VarName.Type} marker failed`) + if (!defMarker.result.success) throw Error(`parse for ${VarName.Def} marker failed`) + if (!extMarker.result.success) throw Error(`parse for ${VarName.Ext} marker failed`) + + if (!Array_isArray(typeMarker.result.value[1])) throw Error(`unknown error when parsing ${VarName.Type} marker`) + if (!Array_isArray(defMarker.result.value[1])) throw Error(`unknown error when parsing ${VarName.Def} marker`) + if (!Array_isArray(extMarker.result.value[1])) throw Error(`unknown error when parsing ${VarName.Def} marker`) + + let unsortedSplices = [{ + start: typeMarker.index + 1, + end: typeMarker.result.index - 1, + content: parsedExtensionFile[VarName.Type] ?? '', + firstLineOffset: Math.max(typeMarker.result.value[1].length - 1, 0), + offset: Math.max(typeMarker.result.value[1].length - 3, 0), + }, { + start: defMarker.index + 1, + end: defMarker.result.index - 1, + content: parsedExtensionFile[VarName.Def] ?? '', + firstLineOffset: Math.max(defMarker.result.value[1].length - 1, 0), + offset: Math.max(defMarker.result.value[1].length - 3, 0), + }, { + start: extMarker.index + 1, + end: extMarker.result.index - 1, + content: parsedExtensionFile[VarName.Ext] ?? '', + firstLineOffset: Math.max(extMarker.result.value[1].length - 1, 0), + offset: Math.max(extMarker.result.value[1].length - 3, 0), + }] as const satisfies Splice[] + + let splices = unsortedSplices.sort(spliceComparator) + + return splice3(source, ...splices) +} + +function splice1(source: string, x: Splice) { + return '' + + source.slice(0, x.start) + + '\n' + ' '.repeat(x.firstLineOffset) + x.content.split('\n').map((_) => ' '.repeat(x.offset) + _).join().trimStart() + + source.slice(x.end) +} + +function splice2(source: string, first: Splice, second: Splice) { + return '' + + source.slice(0, first.start) + + '\n' + ' '.repeat(first.firstLineOffset) + first.content.split('\n').map((_) => ' '.repeat(first.offset) + _).join().trimStart() + + source.slice(first.end, second.start) + + '\n' + ' '.repeat(second.firstLineOffset) + second.content.split('\n').map((_) => ' '.repeat(second.offset) + _).join('\n').trimStart() + + source.slice(second.end) +} diff --git a/packages/schema-generator/src/version.ts b/packages/schema-generator/src/version.ts new file mode 100644 index 00000000..660ff1ca --- /dev/null +++ b/packages/schema-generator/src/version.ts @@ -0,0 +1,3 @@ +import pkg from './__generated__/__manifest__.js' +export const VERSION = `${pkg.name}@${pkg.version}` as const +export type VERSION = typeof VERSION diff --git a/packages/schema-generator/test/__e2e.test.ts__ b/packages/schema-generator/test/__e2e.test.ts__ new file mode 100644 index 00000000..fbc1d511 --- /dev/null +++ b/packages/schema-generator/test/__e2e.test.ts__ @@ -0,0 +1,332 @@ +import * as vi from 'vitest' + +import * as t from './namespace.js' +import { configure } from '@traversable/schema-core' +import { mut } from '@traversable/registry' + +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/schema-generator❳', () => { + + vi.it('〖️⛳️〗› ❲generated❳: integer schema', () => { + vi.assert.isTrue(t.integer(0)) + vi.assert.isFalse(t.integer(0.1)) + vi.assert.isFalse(t.integer('')) + }) + + vi.it('〖️⛳️〗› ❲generated❳: number schema', () => { + vi.assert.isTrue(t.number(0)) + vi.assert.isTrue(t.number(0.1)) + vi.assert.isFalse(t.number('')) + }) + + vi.it('〖️⛳️〗› ❲generated❳: string schema', () => { + vi.assert.isTrue(t.string('')) + vi.assert.isFalse(t.string(0.1)) + }) + + vi.it('〖️⛳️〗› ❲generated❳: optional schema', () => { + vi.assert.isTrue(t.optional(t.integer)(void 0)) + vi.assert.isTrue(t.optional(t.integer)(0)) + vi.assert.isTrue(t.object({ a: t.optional(t.integer) })({})) + vi.assert.isTrue(t.object({ a: t.optional(t.integer), b: t.optional(t.object({ c: t.number })) })({ b: { c: 0 } })) + vi.assert.isTrue(t.object({ a: t.optional(t.integer), b: t.optional(t.object({ c: t.number })) })({ a: 0, b: { c: 1 } })) + vi.assert.isTrue(t.object({ a: t.optional(t.integer), b: t.optional(t.object({ c: t.number })) })({})) + vi.assert.isFalse(t.optional(t.integer)('')) + + void configure({ schema: { optionalTreatment: 'presentButUndefinedIsOK' } }) + vi.assert.isTrue(t.object({ a: t.optional(t.integer), b: t.optional(t.object({ c: t.number })) })({ b: void 0 })) + + void configure({ schema: { optionalTreatment: 'exactOptional' } }) + vi.assert.isFalse(t.object({ a: t.optional(t.integer), b: t.optional(t.object({ c: t.number })) })({ b: void 0 })) + }) + + vi.it('〖️⛳️〗› ❲generated❳: array schema', () => { + vi.assert.isTrue(t.array(t.string)([])) + vi.assert.isTrue(t.array(t.string)([''])) + vi.assert.isFalse(t.array(t.string)({})) + vi.assert.isFalse(t.array(t.string)([0])) + }) + + vi.it('〖️⛳️〗› ❲generated❳: record schema', () => { + vi.assert.isTrue(t.record(t.integer)({})) + vi.assert.isTrue(t.record(t.integer)({ '': 0 })) + vi.assert.isFalse(t.record(t.integer)({ '': false })) + vi.assert.isFalse(t.record(t.integer)([])) + }) + + vi.it('〖️⛳️〗› ❲generated❳: union schema', () => { + vi.assert.isTrue(t.union(t.integer, t.string)(0)) + vi.assert.isTrue(t.union(t.integer, t.string)('')) + vi.assert.isFalse(t.union(t.integer, t.string)(false)) + }) + + vi.it('〖️⛳️〗› ❲generated❳: intersect schema', () => { + vi.assert.isTrue(t.intersect(t.integer)(0)) + vi.assert.isTrue(t.intersect(t.tuple())([])) + vi.assert.isTrue(t.intersect(t.object({}))({})) + vi.assert.isTrue(t.intersect(t.object({ a: t.integer }), t.object({ b: t.string }))({ a: 0, b: '' })) + vi.assert.isFalse(t.intersect(t.object({ a: t.integer }), t.object({ b: t.string }))({})) + vi.assert.isFalse(t.intersect(t.object({ a: t.integer }), t.object({ b: t.string }))({ a: 0 })) + vi.assert.isFalse(t.intersect(t.object({ a: t.integer }), t.object({ b: t.string }))({ b: '' })) + }) + + vi.it('〖️⛳️〗› ❲generated❳: tuple schema', () => { + vi.assert.isTrue(t.tuple()([])) + vi.assert.isTrue(t.tuple(t.integer, t.string)([0, ''])) + vi.assert.isFalse(t.tuple(t.integer, t.string)(['', 0])) + vi.assert.isFalse(t.tuple(t.integer, t.string)([0])) + vi.assert.isFalse(t.tuple(t.integer, t.string)({})) + }) + + vi.it('〖️⛳️〗› ❲generated❳: object schema', () => { + vi.assert.isTrue(t.object({})({})) + vi.assert.isTrue(t.object({ '': t.integer })({ '': 0 })) + vi.assert.isFalse(t.object({ '': t.integer })({})) + vi.assert.isFalse(t.object({})([])) + }) +}) + +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/schema-generator❳: .toString', () => { + + vi.it('〖️⛳️〗› ❲generated❳: integer.toString()', () => { + vi.expect(t.integer.toString()).toMatchInlineSnapshot(`"number"`) + vi.expectTypeOf(t.integer.toString()).toEqualTypeOf('number' as const) + }) + + vi.it('〖️⛳️〗› ❲generated❳: number.toString()', () => { + vi.expect(t.number.toString()).toMatchInlineSnapshot(`"number"`) + vi.expectTypeOf(t.number.toString()).toEqualTypeOf('number' as const) + }) + + vi.it('〖️⛳️〗› ❲generated❳: string.toString()', () => { + vi.expect(t.string.toString()).toMatchInlineSnapshot(`"string"`) + vi.expectTypeOf(t.string.toString()).toEqualTypeOf('string' as const) + }) + + vi.it('〖️⛳️〗› ❲generated❳: array(...).toString()', () => { + vi.expect(t.array(t.string).toString()).toMatchInlineSnapshot(`"(\${string})[]"`) + vi.expect(t.array(t.array(t.string)).toString()).toMatchInlineSnapshot(`"(\${string})[]"`) + vi.expectTypeOf(t.array(t.string).toString()).toEqualTypeOf('(string)[]' as const) + vi.expectTypeOf(t.array(t.array(t.string)).toString()).toEqualTypeOf('((string)[])[]' as const) + }) + + vi.it('〖️⛳️〗› ❲generated❳: record(...).toString()', () => { + vi.expect(t.record(t.string).toString()).toMatchInlineSnapshot(`"Record"`) + vi.expect(t.record(t.record(t.string)).toString()).toMatchInlineSnapshot(`"Record>"`) + vi.expectTypeOf(t.record(t.string).toString()).toEqualTypeOf('Record' as const) + vi.expectTypeOf(t.record(t.array(t.string)).toString()).toEqualTypeOf('Record' as const) + }) + + vi.it('〖️⛳️〗› ❲generated❳: union(...).toString()', () => { + vi.expect(t.union(t.string).toString()).toMatchInlineSnapshot(`"(string)"`) + vi.expect(t.union(t.array(t.string), t.array(t.number)).toString()).toMatchInlineSnapshot(`"((\${string})[] | (\${string})[])"`) + vi.expectTypeOf(t.union().toString()).toEqualTypeOf('never' as const) + vi.expectTypeOf(t.union(t.integer).toString()).toEqualTypeOf('(number)' as const) + vi.expectTypeOf(t.union(t.integer, t.string).toString()).toEqualTypeOf('(number | string)' as const) + vi.expectTypeOf(t.union(t.array(t.string), t.array(t.number)).toString()).toEqualTypeOf('((string)[] | (number)[])' as const) + }) + + vi.it('〖️⛳️〗› ❲generated❳: intersect(...).toString()', () => { + vi.expect(t.intersect(t.string).toString()).toMatchInlineSnapshot(`"(string)"`) + vi.expect(t.intersect(t.intersect(t.string)).toString()).toMatchInlineSnapshot(`"((string))"`) + vi.expectTypeOf(t.intersect().toString()).toEqualTypeOf('unknown' as const) + vi.expectTypeOf(t.intersect(t.integer).toString()).toEqualTypeOf('(number)' as const) + vi.expectTypeOf(t.intersect(t.object({}), t.tuple()).toString()).toEqualTypeOf('({} & [])' as const) + }) + + vi.it('〖️⛳️〗› ❲generated❳: tuple(...).toString()', () => { + vi.expect(t.tuple().toString()).toMatchInlineSnapshot(`"[]"`) + vi.expect(t.tuple(t.tuple(t.number), t.string).toString()).toMatchInlineSnapshot(`"[[number], string]"`) + vi.expectTypeOf(t.tuple().toString()).toEqualTypeOf('[]' as const) + vi.expectTypeOf(t.tuple(t.tuple(t.number), t.tuple(t.string)).toString()).toEqualTypeOf('[[number], [string]]' as const) + }) + + vi.it('〖️⛳️〗› ❲generated❳: object(...).toString()', () => { + vi.expect(t.object({}).toString()).toMatchInlineSnapshot(`"{}"`) + vi.expect(t.object({ a: t.integer }).toString()).toMatchInlineSnapshot(`"{ 'a': number }"`) + vi.expectTypeOf(t.object({}).toString()).toEqualTypeOf('{}' as const) + vi.expectTypeOf(t.object({ a: t.number }).toString()).toEqualTypeOf(`{ 'a': number }` as const) + vi.expectTypeOf(t.object({ a: t.object({ b: t.number }) }).toString()).toEqualTypeOf(`{ 'a': { 'b': number } }` as const) + }) + + vi.it('〖️⛳️〗› ❲generated❳: optional(...).toString()', () => { + vi.expect(t.optional(t.integer).toString()).toMatchInlineSnapshot(`"(number | undefined)"`) + vi.expect(t.optional(t.string).toString()).toMatchInlineSnapshot(`"(string | undefined)"`) + vi.expectTypeOf(t.optional(t.integer).toString()).toEqualTypeOf('(number | undefined)' as const) + vi.expectTypeOf(t.object({ a: t.optional(t.integer) }).toString()).toEqualTypeOf(`{ 'a'?: (number | undefined) }` as const) + vi.expectTypeOf(t.object({ a: t.optional(t.object({ b: t.optional(t.integer) })) }).toString()) + .toEqualTypeOf(`{ 'a'?: ({ 'b'?: (number | undefined) } | undefined) }` as const) + }) +}) + +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/schema-generator❳: .toJsonSchema', () => { + + vi.it('〖️⛳️〗› ❲generated❳: integer.toJsonSchema()', () => { + vi.expectTypeOf(t.integer.toJsonSchema()).toEqualTypeOf(mut({ type: 'integer' })) + vi.expectTypeOf(t.integer.min(0).toJsonSchema()).toEqualTypeOf(mut({ type: 'integer', minimum: 0 })) + vi.expectTypeOf(t.integer.max(0).toJsonSchema()).toEqualTypeOf(mut({ type: 'integer', maximum: 0 })) + vi.expectTypeOf(t.integer.between(0, 1).toJsonSchema()).toEqualTypeOf(mut({ type: 'integer', minimum: 0, maximum: 1 })) + }) + + vi.it('〖️⛳️〗› ❲generated❳: number.toJsonSchema()', () => { + vi.expectTypeOf(t.number.toJsonSchema()).toEqualTypeOf(mut({ type: 'number' })) + vi.expectTypeOf(t.number.min(0).toJsonSchema()).toEqualTypeOf(mut({ type: 'number', minimum: 0, })) + vi.expectTypeOf(t.number.max(0).toJsonSchema()).toEqualTypeOf(mut({ type: 'number', maximum: 0, })) + vi.expectTypeOf(t.number.between(0, 1).toJsonSchema()).toEqualTypeOf(mut({ type: 'number', minimum: 0, maximum: 1, })) + vi.expectTypeOf(t.number.moreThan(0).toJsonSchema()).toEqualTypeOf(mut({ type: 'number', exclusiveMinimum: 0, })) + vi.expectTypeOf(t.number.lessThan(0).toJsonSchema()).toEqualTypeOf(mut({ type: 'number', exclusiveMaximum: 0, })) + }) + + vi.it('〖️⛳️〗› ❲generated❳: string.toJsonSchema()', () => { + vi.expectTypeOf(t.string.toJsonSchema()).toEqualTypeOf(mut({ type: 'string' })) + vi.expectTypeOf(t.string.min(0).toJsonSchema()).toEqualTypeOf(mut({ type: 'string', minLength: 0, })) + vi.expectTypeOf(t.string.max(0).toJsonSchema()).toEqualTypeOf(mut({ type: 'string', maxLength: 0, })) + vi.expectTypeOf(t.string.between(0, 1).toJsonSchema()).toEqualTypeOf(mut({ type: 'string', minLength: 0, maxLength: 1, })) + }) + + vi.it('〖️⛳️〗› ❲generated❳: array(...).toJsonSchema()', () => { + vi.expectTypeOf(t.array(t.integer).toJsonSchema()).toEqualTypeOf<{ type: 'array', items: { type: 'integer' } }>() + vi.expectTypeOf(t.array(t.number).toJsonSchema()).toEqualTypeOf<{ type: 'array', items: { type: 'number' } }>() + vi.expectTypeOf(t.array(t.array(t.integer)).toJsonSchema()).toEqualTypeOf<{ type: 'array', items: { type: 'array', items: { type: 'integer' } } }>() + vi.expectTypeOf(t.array(t.integer).min(0).toJsonSchema()).toEqualTypeOf<{ type: 'array', items: { type: 'integer' }, minLength: 0 }>() + vi.expectTypeOf(t.array(t.integer).max(0).toJsonSchema()).toEqualTypeOf<{ type: 'array', items: { type: 'integer' }, maxLength: 0 }>() + vi.expectTypeOf(t.array(t.integer).between(0, 1).toJsonSchema()).toEqualTypeOf<{ type: 'array', items: { type: 'integer' }, minLength: 0, maxLength: 1 }>() + }) + + vi.it('〖️⛳️〗› ❲generated❳: record(...).toJsonSchema()', () => { + vi.expectTypeOf(t.record(t.integer).toJsonSchema()).toEqualTypeOf<{ type: 'object', additionalProperties: { type: 'integer' } }>() + vi.expectTypeOf( + t.record(t.record(t.integer)).toJsonSchema() + ).toEqualTypeOf<{ + type: 'object' + additionalProperties: { + type: 'object' + additionalProperties: { type: 'integer' } + } + }>() + }) + + vi.it('〖️⛳️〗› ❲generated❳: tuple(...).toJsonSchema()', () => { + vi.expectTypeOf(t.tuple().toJsonSchema()) + .toEqualTypeOf<{ type: 'array', items: [], additionalItems: false, minItems: 0, maxItems: 0 }>() + vi.expectTypeOf(t.tuple(t.integer).toJsonSchema()) + .toEqualTypeOf<{ + type: 'array' + items: [{ type: 'integer' }] + additionalItems: false + minItems: 1 + maxItems: 1 + }>() + }) + + vi.it('〖️⛳️〗› ❲generated❳: object(...).toJsonSchema()', () => { + vi.expectTypeOf(t.object({}).toJsonSchema()).toEqualTypeOf<{ type: 'object', required: [], properties: {} }>() + vi.expectTypeOf(t.object({ a: t.string }).toJsonSchema()) + .toEqualTypeOf<{ type: 'object', required: 'a'[], properties: { a: { type: 'string' } } }>() + vi.expectTypeOf(t.object({ a: t.optional(t.string) }).toJsonSchema()) + .toEqualTypeOf<{ type: 'object', required: [], properties: { a: { type: 'string', nullable: true } } }>() + vi.expectTypeOf(t.object({ a: t.optional(t.string), b: t.integer }).toJsonSchema()) + .toEqualTypeOf<{ type: 'object', required: 'b'[], properties: { a: { type: 'string', nullable: true }, b: { type: 'integer' } } }>() + }) + + vi.it('〖️⛳️〗› ❲generated❳: intersect(...).toJsonSchema()', () => { + vi.expectTypeOf(t.intersect(t.object({})).toJsonSchema()) + .toEqualTypeOf<{ allOf: [{ type: 'object', required: [], properties: {} }] }>() + + vi.expectTypeOf( + t.intersect( + t.object({ a: t.number }), + t.object({ b: t.string }) + ).toJsonSchema() + ) + .toEqualTypeOf<{ + allOf: [ + { type: 'object', required: 'a'[], properties: { a: { type: 'number' } } }, + { type: 'object', required: 'b'[], properties: { b: { type: 'string' } } } + ] + }>() + }) + + vi.it('〖️⛳️〗› ❲generated❳: union(...).toJsonSchema()', () => { + vi.expectTypeOf(t.union(t.object({})).toJsonSchema()) + .toEqualTypeOf<{ anyOf: [{ type: 'object', required: [], properties: {} }] }>() + + vi.expectTypeOf( + t.union( + t.object({ a: t.number }), + t.object({ b: t.string }) + ).toJsonSchema() + ) + .toEqualTypeOf<{ + anyOf: [ + { type: 'object', required: 'a'[], properties: { a: { type: 'number' } } }, + { type: 'object', required: 'b'[], properties: { b: { type: 'string' } } } + ] + }>() + }) + +}) + +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/schema-generator❳: .equals', () => { + + vi.it('〖️⛳️〗› ❲generated❳: tuple(...).equals()', () => { + vi.assert.isTrue(t.tuple(t.integer, t.tuple(t.string)).equals([0, ['']], [0, ['']])) + vi.assert.isFalse(t.tuple(t.integer, t.tuple(t.string)).equals([0, ['']], [1, ['']])) + vi.assert.isFalse(t.tuple(t.integer, t.tuple(t.string)).equals([0, ['']], [0, ['-']])) + }) + + vi.it('〖️⛳️〗› ❲generated❳: object(...).equals()', () => { + vi.assert.isTrue(t.object({ a: t.integer, b: t.object({ c: t.optional(t.string) }) }).equals({ a: 0, b: { c: 'hey' } }, { a: 0, b: { c: 'hey' } })) + vi.assert.isFalse(t.object({ a: t.integer, b: t.object({ c: t.optional(t.string) }) }).equals({ a: 0, b: { c: 'hey' } }, { a: 0, b: { c: 'ho' } })) + }) +}) + +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/schema-generator❳: .validate', () => { + vi.it('〖️⛳️〗› ❲generated❳: object(...).validate()', () => { + vi.expect(t.object({}).validate([])).toMatchInlineSnapshot(` + [ + { + "got": [], + "kind": "TYPE_MISMATCH", + "msg": "Expected object", + "path": [], + }, + ] + `) + }) + + vi.it('〖️⛳️〗› ❲generated❳: object(...).validate()', () => { + + vi.assert.isTrue(t.object({ a: t.integer }).validate({ a: 0 })) + vi.assert.isTrue(t.object({ a: t.optional(t.integer) }).validate({ a: 0 })) + vi.expect(t.object({ a: t.optional(t.integer) }).validate({ a: '' })).toMatchInlineSnapshot(` + [ + { + "expected": "number", + "got": "", + "kind": "TYPE_MISMATCH", + "msg": "Expected an integer", + "path": [ + "a", + ], + }, + ] + `) + + configure({ schema: { optionalTreatment: 'presentButUndefinedIsOK' } }) + vi.assert.isTrue(t.object({ a: t.optional(t.integer) }).validate({ a: void 0 })) + + configure({ schema: { optionalTreatment: 'exactOptional' } }) + vi.expect(t.object({ a: t.optional(t.integer) }).validate({ a: void 0 })).toMatchInlineSnapshot(` + [ + { + "got": undefined, + "kind": "TYPE_MISMATCH", + "msg": "Expected optional", + "path": [ + "a", + ], + }, + ] + `) + }) +}) \ No newline at end of file diff --git a/packages/schema-generator/test/generate.test.ts b/packages/schema-generator/test/generate.test.ts new file mode 100644 index 00000000..3d2c98c7 --- /dev/null +++ b/packages/schema-generator/test/generate.test.ts @@ -0,0 +1,227 @@ +import * as vi from 'vitest' +import * as path from 'node:path' +import * as fs from 'node:fs' + +import { writeSchemas } from '@traversable/schema-generator' + +/** + * ## TODO: + * - [ ] readonlyArray + * - [x] null + * - [x] void + * - [x] never + * - [x] unknown + * - [x] any + * - [x] undefined + * - [x] symbol + * - [x] boolean + * - [x] optional + * - [x] bigint + * - [x] eq + */ +let TODOs = void 0 + +let DIR_PATH = path.join(path.resolve(), 'packages', 'schema-generator', 'test') +let DATA_PATH = path.join(DIR_PATH, 'test-data') + +let PATH = { + __generated__: path.join(DIR_PATH, '__generated__'), + sources: { + never: { + core: path.join(DATA_PATH, 'never', 'core.ts'), + extension: path.join(DATA_PATH, 'never', 'extension.ts'), + equals: path.join(DATA_PATH, 'never', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'never', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'never', 'toString.ts'), + validate: path.join(DATA_PATH, 'never', 'validate.ts'), + }, + unknown: { + core: path.join(DATA_PATH, 'unknown', 'core.ts'), + extension: path.join(DATA_PATH, 'unknown', 'extension.ts'), + equals: path.join(DATA_PATH, 'unknown', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'unknown', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'unknown', 'toString.ts'), + validate: path.join(DATA_PATH, 'unknown', 'validate.ts'), + }, + any: { + core: path.join(DATA_PATH, 'any', 'core.ts'), + extension: path.join(DATA_PATH, 'any', 'extension.ts'), + equals: path.join(DATA_PATH, 'any', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'any', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'any', 'toString.ts'), + validate: path.join(DATA_PATH, 'any', 'validate.ts'), + }, + void: { + core: path.join(DATA_PATH, 'void', 'core.ts'), + extension: path.join(DATA_PATH, 'void', 'extension.ts'), + equals: path.join(DATA_PATH, 'void', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'void', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'void', 'toString.ts'), + validate: path.join(DATA_PATH, 'void', 'validate.ts'), + }, + null: { + core: path.join(DATA_PATH, 'null', 'core.ts'), + extension: path.join(DATA_PATH, 'null', 'extension.ts'), + equals: path.join(DATA_PATH, 'null', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'null', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'null', 'toString.ts'), + validate: path.join(DATA_PATH, 'null', 'validate.ts'), + }, + undefined: { + core: path.join(DATA_PATH, 'undefined', 'core.ts'), + extension: path.join(DATA_PATH, 'undefined', 'extension.ts'), + equals: path.join(DATA_PATH, 'undefined', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'undefined', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'undefined', 'toString.ts'), + validate: path.join(DATA_PATH, 'undefined', 'validate.ts'), + }, + boolean: { + core: path.join(DATA_PATH, 'boolean', 'core.ts'), + extension: path.join(DATA_PATH, 'boolean', 'extension.ts'), + equals: path.join(DATA_PATH, 'boolean', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'boolean', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'boolean', 'toString.ts'), + validate: path.join(DATA_PATH, 'boolean', 'validate.ts'), + }, + symbol: { + core: path.join(DATA_PATH, 'symbol', 'core.ts'), + extension: path.join(DATA_PATH, 'symbol', 'extension.ts'), + equals: path.join(DATA_PATH, 'symbol', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'symbol', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'symbol', 'toString.ts'), + validate: path.join(DATA_PATH, 'symbol', 'validate.ts'), + }, + integer: { + core: path.join(DATA_PATH, 'integer', 'core.ts'), + extension: path.join(DATA_PATH, 'integer', 'extension.ts'), + equals: path.join(DATA_PATH, 'integer', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'integer', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'integer', 'toString.ts'), + validate: path.join(DATA_PATH, 'integer', 'validate.ts'), + }, + bigint: { + core: path.join(DATA_PATH, 'bigint', 'core.ts'), + extension: path.join(DATA_PATH, 'bigint', 'extension.ts'), + equals: path.join(DATA_PATH, 'bigint', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'bigint', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'bigint', 'toString.ts'), + validate: path.join(DATA_PATH, 'bigint', 'validate.ts'), + }, + number: { + core: path.join(DATA_PATH, 'number', 'core.ts'), + extension: path.join(DATA_PATH, 'number', 'extension.ts'), + equals: path.join(DATA_PATH, 'number', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'number', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'number', 'toString.ts'), + validate: path.join(DATA_PATH, 'number', 'validate.ts'), + }, + string: { + core: path.join(DATA_PATH, 'string', 'core.ts'), + extension: path.join(DATA_PATH, 'string', 'extension.ts'), + equals: path.join(DATA_PATH, 'string', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'string', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'string', 'toString.ts'), + validate: path.join(DATA_PATH, 'string', 'validate.ts'), + }, + eq: { + core: path.join(DATA_PATH, 'eq', 'core.ts'), + extension: path.join(DATA_PATH, 'eq', 'extension.ts'), + equals: path.join(DATA_PATH, 'eq', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'eq', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'eq', 'toString.ts'), + validate: path.join(DATA_PATH, 'eq', 'validate.ts'), + }, + optional: { + core: path.join(DATA_PATH, 'optional', 'core.ts'), + extension: path.join(DATA_PATH, 'optional', 'extension.ts'), + equals: path.join(DATA_PATH, 'optional', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'optional', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'optional', 'toString.ts'), + validate: path.join(DATA_PATH, 'optional', 'validate.ts'), + }, + array: { + core: path.join(DATA_PATH, 'array', 'core.ts'), + extension: path.join(DATA_PATH, 'array', 'extension.ts'), + equals: path.join(DATA_PATH, 'array', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'array', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'array', 'toString.ts'), + validate: path.join(DATA_PATH, 'array', 'validate.ts'), + }, + record: { + core: path.join(DATA_PATH, 'record', 'core.ts'), + extension: path.join(DATA_PATH, 'record', 'extension.ts'), + equals: path.join(DATA_PATH, 'record', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'record', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'record', 'toString.ts'), + validate: path.join(DATA_PATH, 'record', 'validate.ts'), + }, + union: { + core: path.join(DATA_PATH, 'union', 'core.ts'), + extension: path.join(DATA_PATH, 'union', 'extension.ts'), + equals: path.join(DATA_PATH, 'union', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'union', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'union', 'toString.ts'), + validate: path.join(DATA_PATH, 'union', 'validate.ts'), + }, + intersect: { + core: path.join(DATA_PATH, 'intersect', 'core.ts'), + extension: path.join(DATA_PATH, 'intersect', 'extension.ts'), + equals: path.join(DATA_PATH, 'intersect', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'intersect', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'intersect', 'toString.ts'), + validate: path.join(DATA_PATH, 'intersect', 'validate.ts'), + }, + tuple: { + core: path.join(DATA_PATH, 'tuple', 'core.ts'), + extension: path.join(DATA_PATH, 'tuple', 'extension.ts'), + equals: path.join(DATA_PATH, 'tuple', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'tuple', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'tuple', 'toString.ts'), + validate: path.join(DATA_PATH, 'tuple', 'validate.ts'), + }, + object: { + core: path.join(DATA_PATH, 'object', 'core.ts'), + extension: path.join(DATA_PATH, 'object', 'extension.ts'), + equals: path.join(DATA_PATH, 'object', 'equals.ts'), + toJsonSchema: path.join(DATA_PATH, 'object', 'toJsonSchema.ts'), + toString: path.join(DATA_PATH, 'object', 'toString.ts'), + validate: path.join(DATA_PATH, 'object', 'validate.ts'), + }, + }, + targets: { + never: path.join(DIR_PATH, '__generated__', 'never.gen.ts'), + unknown: path.join(DIR_PATH, '__generated__', 'unknown.gen.ts'), + any: path.join(DIR_PATH, '__generated__', 'any.gen.ts'), + void: path.join(DIR_PATH, '__generated__', 'void.gen.ts'), + null: path.join(DIR_PATH, '__generated__', 'null.gen.ts'), + undefined: path.join(DIR_PATH, '__generated__', 'undefined.gen.ts'), + boolean: path.join(DIR_PATH, '__generated__', 'boolean.gen.ts'), + symbol: path.join(DIR_PATH, '__generated__', 'symbol.gen.ts'), + integer: path.join(DIR_PATH, '__generated__', 'integer.gen.ts'), + bigint: path.join(DIR_PATH, '__generated__', 'bigint.gen.ts'), + number: path.join(DIR_PATH, '__generated__', 'number.gen.ts'), + string: path.join(DIR_PATH, '__generated__', 'string.gen.ts'), + eq: path.join(DIR_PATH, '__generated__', 'eq.gen.ts'), + optional: path.join(DIR_PATH, '__generated__', 'optional.gen.ts'), + array: path.join(DIR_PATH, '__generated__', 'array.gen.ts'), + record: path.join(DIR_PATH, '__generated__', 'record.gen.ts'), + union: path.join(DIR_PATH, '__generated__', 'union.gen.ts'), + intersect: path.join(DIR_PATH, '__generated__', 'intersect.gen.ts'), + tuple: path.join(DIR_PATH, '__generated__', 'tuple.gen.ts'), + object: path.join(DIR_PATH, '__generated__', 'object.gen.ts'), + } +} + +vi.describe.skip('〖️⛳️〗‹‹‹ ❲@traversable/schema-generator❳', () => { + vi.it('〖️⛳️〗› ❲writeSchemas❳', () => { + if (!fs.existsSync(DIR_PATH)) fs.mkdirSync(DIR_PATH) + if (!fs.existsSync(DATA_PATH)) fs.mkdirSync(DATA_PATH) + if (!fs.existsSync(PATH.__generated__)) fs.mkdirSync(PATH.__generated__) + + writeSchemas( + PATH.sources, + PATH.targets, + '@traversable/test', + ) + }) +}) diff --git a/packages/schema-generator/test/imports.test.ts b/packages/schema-generator/test/imports.test.ts new file mode 100644 index 00000000..469faea0 --- /dev/null +++ b/packages/schema-generator/test/imports.test.ts @@ -0,0 +1,163 @@ +import * as vi from 'vitest' +import { deduplicateImports, makeImport } from '@traversable/schema-generator' + +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/schema-generator❳', () => { + vi.it('〖️⛳️〗› ❲makeImport❳', () => { + vi.expect( + makeImport('@traversable/schema-core', { term: { named: ['t'], namespace: [] }, type: { named: ['Predicate'], namespace: ['T'] } }).join('\n'), + ).toMatchInlineSnapshot(` + "import type * as T from '@traversable/schema-core' + import type { Predicate } from '@traversable/schema-core' + import { t } from '@traversable/schema-core'" + `) + + vi.expect( + makeImport('@traversable/schema-core', { term: { named: ['t', 'getConfig'], namespace: [] }, type: { named: ['Predicate'], namespace: ['T'] } }).join('\n'), + ).toMatchInlineSnapshot(` + "import type * as T from '@traversable/schema-core' + import type { Predicate } from '@traversable/schema-core' + import { t, getConfig } from '@traversable/schema-core'" + `) + }) + + vi.it('〖️⛳️〗› ❲deduplicateImports❳', () => { + vi.expect(deduplicateImports({ + array: { + core: { + "@traversable/registry": { + type: { + named: ['PickIfDefined as Pick'], + namespace: 'T' + }, + term: { + named: ['pick'] + } + }, + "@traversable/schema-core": { + type: { + named: ['t', 'TypeGuard as Guard', 'Bounds'], + }, + term: { + named: [], + }, + } + }, + toString: { + "@traversable/registry": { + type: { + named: ['Record', 'PickIfDefined'], + }, + term: { + named: ['omit'], + namespace: 'T', + } + }, + "@traversable/schema-core": { + type: { + named: [], + }, + term: { + named: ['t'] + } + } + } + }, + string: { + core: { + "@traversable/registry": { + type: { + named: ['PickIfDefined as Pick'], + namespace: 'T' + }, + term: { + named: ['pick'] + } + }, + }, + equals: {}, + toJsonSchema: { + '@traversable/schema-to-json-schema': { + type: { + named: [], + namespace: 'JsonSchema', + }, + term: { + named: ['stringToJsonSchema'] + } + } + } + } + })).toMatchInlineSnapshot(` + { + "array": { + "@traversable/registry": { + "term": { + "named": Set { + "pick", + "omit", + }, + "namespace": Set { + "T", + }, + }, + "type": { + "named": Set { + "PickIfDefined as Pick", + "Record", + "PickIfDefined", + }, + "namespace": Set {}, + }, + }, + "@traversable/schema-core": { + "term": { + "named": Set { + "t", + }, + "namespace": Set {}, + }, + "type": { + "named": Set { + "TypeGuard as Guard", + "Bounds", + }, + "namespace": Set {}, + }, + }, + }, + "string": { + "@traversable/registry": { + "term": { + "named": Set { + "pick", + }, + "namespace": Set {}, + }, + "type": { + "named": Set { + "PickIfDefined as Pick", + }, + "namespace": Set { + "T", + }, + }, + }, + "@traversable/schema-to-json-schema": { + "term": { + "named": Set { + "stringToJsonSchema", + }, + "namespace": Set {}, + }, + "type": { + "named": Set {}, + "namespace": Set { + "JsonSchema", + }, + }, + }, + }, + } + `) + }) +}) diff --git a/packages/schema-generator/test/namespace.ts b/packages/schema-generator/test/namespace.ts new file mode 100644 index 00000000..9741df90 --- /dev/null +++ b/packages/schema-generator/test/namespace.ts @@ -0,0 +1,10 @@ +// export { array } from './__generated__/array.gen.js' +// export { integer } from './__generated__/integer.gen.js' +// export { intersect } from './__generated__/intersect.gen.js' +// export { number } from './__generated__/number.gen.js' +// export { object } from './__generated__/object.gen.js' +// export { optional } from './__generated__/optional.gen.js' +// export { record } from './__generated__/record.gen.js' +// export { string } from './__generated__/string.gen.js' +// export { tuple } from './__generated__/tuple.gen.js' +// export { union } from './__generated__/union.gen.js' diff --git a/packages/schema-generator/test/parser-combinators.test.ts b/packages/schema-generator/test/parser-combinators.test.ts new file mode 100644 index 00000000..1ef60721 --- /dev/null +++ b/packages/schema-generator/test/parser-combinators.test.ts @@ -0,0 +1,214 @@ +import * as vi from "vitest" + +import { P } from '@traversable/schema-generator' + +/** @internal */ +let Object_values + : (xs: T) => T[keyof T][] + = globalThis.Object.values + +export let key = P.seq( + P.optional(P.char('"')), + P.ident, + P.optional(P.char('"')), +).map(([, k]) => k) + +export let propertyValue = P.char().many({ not: P.alt(P.char(','), P.char('}')) }).map((xs) => xs.join('')) + +export let entry = P.seq( + key, + P.optional(P.whitespace), + P.char(':'), + P.optional(P.whitespace), + propertyValue, +).map(([key, , , , value]) => [key, value] as [k: string, v: string]) + +export let comma = P.seq( + P.spaces, + P.char(','), + P.spaces, +).map((_) => _[1]) + +export let entriesDanglingComma = P.seq( + P.seq(P.trim(entry), P.char(',')).map(([_]) => _).many(), + P.optional(P.trim(entry)), +).map(([xs, x]) => x === null ? xs : [...xs, x]) + +export let parseObjectEntries = P.seq( + P.char('{'), + P.trim(entriesDanglingComma), + P.char('}'), +).map(([, _]) => _ === null ? [] : _) + +vi.describe("〖️⛳️〗‹‹‹ ❲@traversable/schema-generator❳", () => { + vi.it("〖️⛳️〗› ❲P❳", () => { + + vi.expect(P.string('hey jude').run('hey jude')).toMatchInlineSnapshot(` + { + "index": 8, + "success": true, + "value": "hey jude", + } + `) + vi.expect(P.regexp(/type/g).run('type')).toMatchInlineSnapshot(` + { + "index": 4, + "success": true, + "value": "type", + } + `) + + vi.expect(P.ident.run('abc')).toMatchInlineSnapshot(` + { + "index": 3, + "success": true, + "value": "abc", + } + `) + + vi.expect(P.ident.run('1_ab')).toMatchInlineSnapshot(` + { + "index": 0, + "success": false, + } + `) + + vi.expect(P.whitespace.run('')).toMatchInlineSnapshot(` + { + "index": 0, + "success": false, + } + `) + vi.expect(P.whitespace.run(' ')).toMatchInlineSnapshot(` + { + "index": 1, + "success": true, + "value": " ", + } + `) + vi.expect(P.whitespace.run(' ')).toMatchInlineSnapshot(` + { + "index": 2, + "success": true, + "value": " ", + } + `) + + vi.expect(P.alpha.run('')).toMatchInlineSnapshot(` + { + "index": 0, + "success": false, + } + `) + vi.expect(P.alpha.run('a')).toMatchInlineSnapshot(` + { + "index": 1, + "success": true, + "value": "a", + } + `) + vi.expect(P.alpha.run('1')).toMatchInlineSnapshot(` + { + "index": 0, + "success": false, + } + `) + vi.expect(P.alpha.run('ab')).toMatchInlineSnapshot(` + { + "index": 1, + "success": true, + "value": "a", + } + `) + vi.expect(P.alpha.many().run('ab')).toMatchInlineSnapshot(` + { + "index": 2, + "success": true, + "value": [ + "a", + "b", + ], + } + `) + + vi.expect(P.ident.run('hey jude')).toMatchInlineSnapshot(` + { + "index": 3, + "success": true, + "value": "hey", + } + `) + + vi.expect(parseObjectEntries.run('{ "abc": 123, "def": 456 }')).toMatchInlineSnapshot(` + { + "index": 26, + "success": true, + "value": [ + [ + "abc", + "123", + ], + [ + "def", + "456 ", + ], + ], + } + `) + + vi.expect(parseObjectEntries.run('{ "abc": 123 }')).toMatchInlineSnapshot(` + { + "index": 14, + "success": true, + "value": [ + [ + "abc", + "123 ", + ], + ], + } + `) + + vi.expect(entriesDanglingComma.run('abc: 123')).toMatchInlineSnapshot(` + { + "index": 8, + "success": true, + "value": [ + [ + "abc", + "123", + ], + ], + } + `) + + vi.expect(parseObjectEntries.run('{}')).toMatchInlineSnapshot(` + { + "index": 2, + "success": true, + "value": [], + } + `) + + vi.expect(parseObjectEntries.run("{\ + type: `equals: equals`,\ + term: 'equals: equals(x)',\ + } as const")).toMatchInlineSnapshot(` + { + "index": 82, + "success": true, + "value": [ + [ + "type", + "\`equals: equals\`", + ], + [ + "term", + "'equals: equals(x)'", + ], + ], + } + `) + + }) +}) diff --git a/packages/schema-generator/test/test-data/any/core.ts b/packages/schema-generator/test/test-data/any/core.ts new file mode 100644 index 00000000..8e9de433 --- /dev/null +++ b/packages/schema-generator/test/test-data/any/core.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { any_ as any } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface any_ extends any_.core { + //<%= Types %> +} + +function AnySchema(src: unknown): src is any { return src === void 0 } +AnySchema.tag = URI.any +AnySchema.def = void 0 as any + +const any_ = Object_assign( + AnySchema, + userDefinitions, +) as any_ + +Object_assign(any_, userExtensions) + +declare namespace any_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.any + _type: any + def: this['_type'] + } +} diff --git a/packages/schema-generator/test/test-data/any/equals.ts b/packages/schema-generator/test/test-data/any/equals.ts new file mode 100644 index 00000000..f94e5ab5 --- /dev/null +++ b/packages/schema-generator/test/test-data/any/equals.ts @@ -0,0 +1,7 @@ +import type { Equal } from "@traversable/registry" +import { Object_is } from "@traversable/registry" + +export type equals = Equal +export function equals(left: unknown, right: unknown): boolean { + return Object_is(left, right) +} \ No newline at end of file diff --git a/packages/schema-generator/test/test-data/any/extension.ts b/packages/schema-generator/test/test-data/any/extension.ts new file mode 100644 index 00000000..c4ecbb88 --- /dev/null +++ b/packages/schema-generator/test/test-data/any/extension.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/test/test-data/any/toJsonSchema.ts b/packages/schema-generator/test/test-data/any/toJsonSchema.ts new file mode 100644 index 00000000..25336fc6 --- /dev/null +++ b/packages/schema-generator/test/test-data/any/toJsonSchema.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): { type: 'object', properties: {}, nullable: true } } +export function toJsonSchema(): toJsonSchema { + function unknownToJsonSchema() { return { type: 'object', properties: {}, nullable: true } as const } + return unknownToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/any/toString.ts b/packages/schema-generator/test/test-data/any/toString.ts new file mode 100644 index 00000000..f70aa050 --- /dev/null +++ b/packages/schema-generator/test/test-data/any/toString.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'any' } +export function toString(): 'any' { return 'any' } diff --git a/packages/schema-generator/test/test-data/any/validate.ts b/packages/schema-generator/test/test-data/any/validate.ts new file mode 100644 index 00000000..9c4298c0 --- /dev/null +++ b/packages/schema-generator/test/test-data/any/validate.ts @@ -0,0 +1,10 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(_?: t.unknown): validate { + validateUnknown.tag = URI.unknown + function validateUnknown() { return true as const } + return validateUnknown +} diff --git a/packages/schema-generator/test/test-data/array/core.ts b/packages/schema-generator/test/test-data/array/core.ts new file mode 100644 index 00000000..e4580dc7 --- /dev/null +++ b/packages/schema-generator/test/test-data/array/core.ts @@ -0,0 +1,128 @@ +import type { + Bounds, + Integer, + Unknown, +} from '@traversable/registry' +import { + Array_isArray, + array as arrayOf, + bindUserExtensions, + carryover, + within, + _isPredicate, + has, + Math_max, + Math_min, + Number_isSafeInteger, + Object_assign, + URI, +} from '@traversable/registry' + +import type { Guarded, Schema, SchemaLike } from '@traversable/schema-core/namespace' + +import type { of } from '../of/index.js' + +/** @internal */ +function boundedArray(schema: S, bounds: Bounds, carry?: Partial>): ((u: unknown) => boolean) & Bounds & array +function boundedArray(schema: S, bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & array +function boundedArray(schema: S, bounds: Bounds, carry?: {}): ((u: unknown) => boolean) & Bounds & array { + return Object_assign(function BoundedArraySchema(u: unknown) { + return Array_isArray(u) && within(bounds)(u.length) + }, carry, array(schema)) +} + +export interface array extends array.core { + //<%= Types %> +} + +export function array(schema: S, readonly: 'readonly'): readonlyArray +export function array(schema: S): array +export function array(schema: S): array>> +export function array(schema: S): array { + return array.def(schema) +} + +export namespace array { + export let userDefinitions: Record = { + //<%= Definitions %> + } as array + export function def(x: S, prev?: array): array + export function def(x: S, prev?: unknown): array + export function def(x: S, prev?: array): array + /* v8 ignore next 1 */ + export function def(x: unknown, prev?: unknown): {} { + let userExtensions: Record = { + //<%= Extensions %> + } + const predicate = _isPredicate(x) ? arrayOf(x) : Array_isArray + function ArraySchema(src: unknown) { return predicate(src) } + ArraySchema.tag = URI.array + ArraySchema.def = x + ArraySchema.min = function arrayMin(minLength: Min) { + return Object_assign( + boundedArray(x, { gte: minLength }, carryover(this, 'minLength' as never)), + { minLength }, + ) + } + ArraySchema.max = function arrayMax(maxLength: Max) { + return Object_assign( + boundedArray(x, { lte: maxLength }, carryover(this, 'maxLength' as never)), + { maxLength }, + ) + } + ArraySchema.between = function arrayBetween( + min: Min, + max: Max, + minLength = Math_min(min, max), + maxLength = Math_max(min, max) + ) { + return Object_assign( + boundedArray(x, { gte: minLength, lte: maxLength }), + { minLength, maxLength }, + ) + } + if (has('minLength', Number_isSafeInteger)(prev)) ArraySchema.minLength = prev.minLength + if (has('maxLength', Number_isSafeInteger)(prev)) ArraySchema.maxLength = prev.maxLength + Object_assign(ArraySchema, userDefinitions) + return Object_assign(ArraySchema, bindUserExtensions(ArraySchema, userExtensions)) + } +} + +export declare namespace array { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.array + get def(): S + _type: S['_type' & keyof S][] + minLength?: number + maxLength?: number + min>(minLength: Min): array.Min + max>(maxLength: Max): array.Max + between, Max extends Integer>(minLength: Min, maxLength: Max): array.between<[min: Min, max: Max], S> + } + type Min + = [Self] extends [{ maxLength: number }] + ? array.between<[min: Min, max: Self['maxLength']], Self['def' & keyof Self]> + : array.min + ; + type Max + = [Self] extends [{ minLength: number }] + ? array.between<[min: Self['minLength'], max: Max], Self['def' & keyof Self]> + : array.max + ; + interface min extends array { minLength: Min } + interface max extends array { maxLength: Max } + interface between extends array { minLength: Bounds[0], maxLength: Bounds[1] } + type type = never | T +} + +export const readonlyArray: { + (schema: S): readonlyArray + (schema: S): readonlyArray> +} = array +export interface readonlyArray { + (u: unknown): u is this['_type'] + tag: URI.array + def: S + _type: ReadonlyArray +} diff --git a/packages/schema-generator/test/test-data/array/equals.ts b/packages/schema-generator/test/test-data/array/equals.ts new file mode 100644 index 00000000..3f53621d --- /dev/null +++ b/packages/schema-generator/test/test-data/array/equals.ts @@ -0,0 +1,23 @@ +import type { Equal } from '@traversable/registry' +import { has, Array_isArray, Object_is } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = never | Equal + +export function equals(arraySchema: t.array): equals +export function equals(arraySchema: t.array): equals +export function equals({ def }: t.array<{ equals: Equal }>): Equal { + let equals = has('equals', (x): x is Equal => typeof x === 'function')(def) ? def.equals : Object_is + function arrayEquals(l: unknown[], r: unknown[]): boolean { + if (Object_is(l, r)) return true + if (Array_isArray(l)) { + if (!Array_isArray(r)) return false + let len = l.length + if (len !== r.length) return false + for (let ix = len; ix-- !== 0;) + if (!equals(l[ix], r[ix])) return false + return true + } else return false + } + return arrayEquals +} diff --git a/packages/schema-generator/test/test-data/array/extension.ts b/packages/schema-generator/test/test-data/array/extension.ts new file mode 100644 index 00000000..da791be3 --- /dev/null +++ b/packages/schema-generator/test/test-data/array/extension.ts @@ -0,0 +1,20 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + toString: toString + equals: equals + toJsonSchema: toJsonSchema + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toJsonSchema, + validate, + toString, + equals, +} diff --git a/packages/schema-generator/test/test-data/array/toJsonSchema.ts b/packages/schema-generator/test/test-data/array/toJsonSchema.ts new file mode 100644 index 00000000..cbdda659 --- /dev/null +++ b/packages/schema-generator/test/test-data/array/toJsonSchema.ts @@ -0,0 +1,36 @@ +import type { t } from '@traversable/schema-core' +import type * as T from '@traversable/registry' +import type { SizeBounds } from '@traversable/schema-to-json-schema' +import { hasSchema } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { + (): never | T.Force< + & { type: 'array', items: T.Returns } + & T.PickIfDefined + > +} + +export function toJsonSchema>(arraySchema: T): toJsonSchema +export function toJsonSchema(arraySchema: T): toJsonSchema +export function toJsonSchema( + { def, minLength, maxLength }: { def: unknown, minLength?: number, maxLength?: number }, +): () => { + type: 'array' + items: unknown + minLength?: number + maxLength?: number +} { + function arrayToJsonSchema() { + let items = hasSchema(def) ? def.toJsonSchema() : def + let out = { + type: 'array' as const, + items, + minLength, + maxLength, + } + if (typeof minLength !== 'number') delete out.minLength + if (typeof maxLength !== 'number') delete out.maxLength + return out + } + return arrayToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/array/toString.ts b/packages/schema-generator/test/test-data/array/toString.ts new file mode 100644 index 00000000..5114ef0b --- /dev/null +++ b/packages/schema-generator/test/test-data/array/toString.ts @@ -0,0 +1,22 @@ +import type { t } from '@traversable/schema-core' + +export interface toString { + /* @ts-expect-error */ + (): never | `(${ReturnType})[]` +} + +export function toString(arraySchema: t.array): toString +export function toString(arraySchema: t.array): toString +export function toString({ def }: { def: unknown }) { + function arrayToString() { + let body = ( + !!def + && typeof def === 'object' + && 'toString' in def + && typeof def.toString === 'function' + ) ? def.toString() + : '${string}' + return ('(' + body + ')[]') + } + return arrayToString +} diff --git a/packages/schema-generator/test/test-data/array/validate.ts b/packages/schema-generator/test/test-data/array/validate.ts new file mode 100644 index 00000000..4d8e7958 --- /dev/null +++ b/packages/schema-generator/test/test-data/array/validate.ts @@ -0,0 +1,27 @@ +import { URI } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { ValidationError, ValidationFn, Validator } from '@traversable/derive-validators' +import { Errors, NullaryErrors } from '@traversable/derive-validators' + +export type validate = never | ValidationFn +export function validate(arraySchema: t.array): validate +export function validate(arraySchema: t.array): validate +export function validate( + { def: { validate = () => true }, minLength, maxLength }: t.array +) { + validateArray.tag = URI.array + function validateArray(u: unknown, path = Array.of()) { + if (!Array.isArray(u)) return [NullaryErrors.array(u, path)] + let errors = Array.of() + if (typeof minLength === 'number' && u.length < minLength) errors.push(Errors.arrayMinLength(u, path, minLength)) + if (typeof maxLength === 'number' && u.length > maxLength) errors.push(Errors.arrayMaxLength(u, path, maxLength)) + for (let i = 0, len = u.length; i < len; i++) { + let y = u[i] + let results = validate(y, [...path, i]) + if (results === true) continue + else errors.push(...results) + } + return errors.length === 0 || errors + } + return validateArray +} diff --git a/packages/schema-generator/test/test-data/bigint/core.ts b/packages/schema-generator/test/test-data/bigint/core.ts new file mode 100644 index 00000000..5c70c86c --- /dev/null +++ b/packages/schema-generator/test/test-data/bigint/core.ts @@ -0,0 +1,97 @@ +import type { Bounds, Unknown } from '@traversable/registry' +import { + bindUserExtensions, + carryover, + Object_assign, + URI, + withinBig as within, +} from '@traversable/registry' + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface bigint_ extends bigint_.core { + //<%= Types %> +} + +export { bigint_ as bigint } + +function BigIntSchema(src: unknown) { return typeof src === 'bigint' } +BigIntSchema.tag = URI.bigint +BigIntSchema.def = 0n + +const bigint_ = Object_assign( + BigIntSchema, + userDefinitions, +) as bigint_ + +bigint_.min = function bigIntMin(minimum) { + return Object_assign( + boundedBigInt({ gte: minimum }, carryover(this, 'minimum')), + { minimum }, + ) +} +bigint_.max = function bigIntMax(maximum) { + return Object_assign( + boundedBigInt({ lte: maximum }, carryover(this, 'maximum')), + { maximum }, + ) +} +bigint_.between = function bigIntBetween( + min, + max, + minimum = (max < min ? max : min), + maximum = (max < min ? min : max), +) { + return Object_assign( + boundedBigInt({ gte: minimum, lte: maximum }), + { minimum, maximum } + ) +} + +Object_assign( + bigint_, + bindUserExtensions(bigint_, userExtensions), +) + +declare namespace bigint_ { + interface core extends bigint_.methods { + (u: this['_type'] | Unknown): u is this['_type'] + _type: bigint + tag: URI.bigint + def: this['_type'] + minimum?: bigint + maximum?: bigint + } + type Min + = [Self] extends [{ maximum: bigint }] + ? bigint_.between<[min: X, max: Self['maximum']]> + : bigint_.min + ; + type Max + = [Self] extends [{ minimum: bigint }] + ? bigint_.between<[min: Self['minimum'], max: X]> + : bigint_.max + ; + interface methods { + min(minimum: Min): bigint_.Min + max(maximum: Max): bigint_.Max + between(minimum: Min, maximum: Max): bigint_.between<[min: Min, max: Max]> + } + interface min extends bigint_ { minimum: Min } + interface max extends bigint_ { maximum: Max } + interface between extends bigint_ { minimum: Bounds[0], maximum: Bounds[1] } +} + +function boundedBigInt(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & bigint_ +function boundedBigInt(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & bigint_ +function boundedBigInt(bounds: Bounds, carry?: {}): {} { + return Object_assign(function BoundedBigIntSchema(u: unknown) { + return bigint_(u) && within(bounds)(u) + }, carry, bigint_) +} diff --git a/packages/schema-generator/test/test-data/bigint/equals.ts b/packages/schema-generator/test/test-data/bigint/equals.ts new file mode 100644 index 00000000..3f38a8a5 --- /dev/null +++ b/packages/schema-generator/test/test-data/bigint/equals.ts @@ -0,0 +1,7 @@ +import type { Equal } from "@traversable/registry" +import { Object_is } from "@traversable/registry" + +export type equals = Equal +export function equals(left: bigint, right: bigint): boolean { + return Object_is(left, right) +} diff --git a/packages/schema-generator/test/test-data/bigint/extension.ts b/packages/schema-generator/test/test-data/bigint/extension.ts new file mode 100644 index 00000000..c4ecbb88 --- /dev/null +++ b/packages/schema-generator/test/test-data/bigint/extension.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/test/test-data/bigint/toJsonSchema.ts b/packages/schema-generator/test/test-data/bigint/toJsonSchema.ts new file mode 100644 index 00000000..f6c7bc5b --- /dev/null +++ b/packages/schema-generator/test/test-data/bigint/toJsonSchema.ts @@ -0,0 +1,7 @@ +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function bigintToJsonSchema(): void { + return void 0 + } + return bigintToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/bigint/toString.ts b/packages/schema-generator/test/test-data/bigint/toString.ts new file mode 100644 index 00000000..793c903e --- /dev/null +++ b/packages/schema-generator/test/test-data/bigint/toString.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'bigint' } +export function toString(): 'bigint' { return 'bigint' } diff --git a/packages/schema-generator/test/test-data/bigint/validate.ts b/packages/schema-generator/test/test-data/bigint/validate.ts new file mode 100644 index 00000000..7e3284da --- /dev/null +++ b/packages/schema-generator/test/test-data/bigint/validate.ts @@ -0,0 +1,13 @@ +import type { ValidationError, ValidationFn } from '@traversable/derive-validators' +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(bigIntSchema: S): validate { + validateBigInt.tag = URI.bigint + function validateBigInt(u: unknown, path = Array.of()): true | ValidationError[] { + return bigIntSchema(u) || [NullaryErrors.bigint(u, path)] + } + return validateBigInt +} diff --git a/packages/schema-generator/test/test-data/boolean/core.ts b/packages/schema-generator/test/test-data/boolean/core.ts new file mode 100644 index 00000000..3e27edf7 --- /dev/null +++ b/packages/schema-generator/test/test-data/boolean/core.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { boolean_ as boolean } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface boolean_ extends boolean_.core { + //<%= Types %> +} +function BooleanSchema(src: unknown): src is boolean { return src === void 0 } + +BooleanSchema.tag = URI.boolean +BooleanSchema.def = false + +const boolean_ = Object_assign( + BooleanSchema, + userDefinitions, +) as boolean_ + +Object_assign(boolean_, userExtensions) + +declare namespace boolean_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.boolean + _type: boolean + def: this['_type'] + } +} diff --git a/packages/schema-generator/test/test-data/boolean/equals.ts b/packages/schema-generator/test/test-data/boolean/equals.ts new file mode 100644 index 00000000..b32f3f3e --- /dev/null +++ b/packages/schema-generator/test/test-data/boolean/equals.ts @@ -0,0 +1,7 @@ +import type { Equal } from "@traversable/registry" +import { Object_is } from "@traversable/registry" + +export type equals = Equal +export function equals(left: boolean, right: boolean): boolean { + return Object_is(left, right) +} \ No newline at end of file diff --git a/packages/schema-generator/test/test-data/boolean/extension.ts b/packages/schema-generator/test/test-data/boolean/extension.ts new file mode 100644 index 00000000..c4ecbb88 --- /dev/null +++ b/packages/schema-generator/test/test-data/boolean/extension.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/test/test-data/boolean/toJsonSchema.ts b/packages/schema-generator/test/test-data/boolean/toJsonSchema.ts new file mode 100644 index 00000000..d1b86f70 --- /dev/null +++ b/packages/schema-generator/test/test-data/boolean/toJsonSchema.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): { type: 'boolean' } } +export function toJsonSchema(): toJsonSchema { + function booleanToJsonSchema() { return { type: 'boolean' as const } } + return booleanToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/boolean/toString.ts b/packages/schema-generator/test/test-data/boolean/toString.ts new file mode 100644 index 00000000..3c408e57 --- /dev/null +++ b/packages/schema-generator/test/test-data/boolean/toString.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'boolean' } +export function toString(): 'boolean' { return 'boolean' } diff --git a/packages/schema-generator/test/test-data/boolean/validate.ts b/packages/schema-generator/test/test-data/boolean/validate.ts new file mode 100644 index 00000000..76cee261 --- /dev/null +++ b/packages/schema-generator/test/test-data/boolean/validate.ts @@ -0,0 +1,12 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import { NullaryErrors, type ValidationFn } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(booleanSchema: t.boolean): validate { + validateBoolean.tag = URI.boolean + function validateBoolean(u: unknown, path = Array.of()) { + return booleanSchema(true as const) || [NullaryErrors.null(u, path)] + } + return validateBoolean +} diff --git a/packages/schema-generator/test/test-data/eq/core.ts b/packages/schema-generator/test/test-data/eq/core.ts new file mode 100644 index 00000000..2cf9c9f7 --- /dev/null +++ b/packages/schema-generator/test/test-data/eq/core.ts @@ -0,0 +1,39 @@ +import type { Mut, Mutable, SchemaOptions as Options, Unknown } from '@traversable/registry' +import { applyOptions, bindUserExtensions, _isPredicate, Object_assign, URI } from '@traversable/registry' + +export interface eq extends eq.core { + //<%= Types %> +} + +export function eq>(value: V, options?: Options): eq> +export function eq(value: V, options?: Options): eq +export function eq(value: V, options?: Options): eq { + return eq.def(value, options) +} +export namespace eq { + export let userDefinitions: Record = { + //<%= Definitions %> + } + export function def(value: T, options?: Options): eq + export function def(x: T, $?: Options): {} { + let userExtensions: Record = { + //<%= Extensions %> + } + const options = applyOptions($) + const predicate = _isPredicate(x) ? x : (y: unknown) => options.eq.equalsFn(x, y) + function EqSchema(src: unknown) { return predicate(src) } + EqSchema.tag = URI.tag + EqSchema.def = x + Object_assign(EqSchema, eq.userDefinitions) + return Object_assign(EqSchema, bindUserExtensions(EqSchema, userExtensions)) + } +} + +export declare namespace eq { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.eq + _type: V + def: V + } +} diff --git a/packages/schema-generator/test/test-data/eq/equals.ts b/packages/schema-generator/test/test-data/eq/equals.ts new file mode 100644 index 00000000..9df41231 --- /dev/null +++ b/packages/schema-generator/test/test-data/eq/equals.ts @@ -0,0 +1,8 @@ +import type { Equal } from '@traversable/registry' +import { t } from '@traversable/schema-core' + +export type equals = never | Equal +export function equals(eqSchema: t.eq): equals +export function equals(): Equal { + return (left: unknown, right: unknown) => t.eq(left)(right) +} diff --git a/packages/schema-generator/test/test-data/eq/extension.ts b/packages/schema-generator/test/test-data/eq/extension.ts new file mode 100644 index 00000000..0e7c4478 --- /dev/null +++ b/packages/schema-generator/test/test-data/eq/extension.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} + diff --git a/packages/schema-generator/test/test-data/eq/toJsonSchema.ts b/packages/schema-generator/test/test-data/eq/toJsonSchema.ts new file mode 100644 index 00000000..8edfd03a --- /dev/null +++ b/packages/schema-generator/test/test-data/eq/toJsonSchema.ts @@ -0,0 +1,8 @@ +import type { t } from '@traversable/schema-core' + +export interface toJsonSchema { (): { const: T } } +export function toJsonSchema(eqSchema: t.eq): toJsonSchema +export function toJsonSchema({ def }: t.eq) { + function eqToJsonSchema() { return { const: def } } + return eqToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/eq/toString.ts b/packages/schema-generator/test/test-data/eq/toString.ts new file mode 100644 index 00000000..3c224bea --- /dev/null +++ b/packages/schema-generator/test/test-data/eq/toString.ts @@ -0,0 +1,17 @@ +import type { Key } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { stringify } from '@traversable/schema-to-string' + +export interface toString { + (): [Key] extends [never] + ? [T] extends [symbol] ? 'symbol' : 'symbol' + : [T] extends [string] ? `'${T}'` : Key +} + +export function toString(eqSchema: t.eq): toString +export function toString({ def }: t.eq): () => string { + function eqToString(): string { + return typeof def === 'symbol' ? 'symbol' : stringify(def) + } + return eqToString +} diff --git a/packages/schema-generator/test/test-data/eq/validate.ts b/packages/schema-generator/test/test-data/eq/validate.ts new file mode 100644 index 00000000..2f02ba7d --- /dev/null +++ b/packages/schema-generator/test/test-data/eq/validate.ts @@ -0,0 +1,17 @@ +import { Equal, getConfig, URI } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { Validate } from '@traversable/derive-validators' +import { Errors } from '@traversable/derive-validators' + +export type validate = Validate +export function validate(eqSchema: t.eq): validate +export function validate({ def }: t.eq): validate { + validateEq.tag = URI.eq + function validateEq(u: unknown, path = Array.of()) { + let options = getConfig().schema + let equals = options?.eq?.equalsFn || Equal.lax + if (equals(def, u)) return true + else return [Errors.eq(u, path, def)] + } + return validateEq +} diff --git a/packages/schema-generator/test/test-data/integer/core.ts b/packages/schema-generator/test/test-data/integer/core.ts new file mode 100644 index 00000000..f2bf6f95 --- /dev/null +++ b/packages/schema-generator/test/test-data/integer/core.ts @@ -0,0 +1,97 @@ +import type { Bounds, Integer, Unknown } from '@traversable/registry' +import { + carryover, + Math_min, + Math_max, + Number_isSafeInteger, + Object_assign, + URI, + bindUserExtensions, + within, +} from '@traversable/registry' + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +export { integer } +interface integer extends integer.core { + //<%= Types %> +} + +function IntegerSchema(src: unknown) { return Number_isSafeInteger(src) } +IntegerSchema.tag = URI.integer +IntegerSchema.def = 0 + +const integer = Object_assign( + IntegerSchema, + userDefinitions, +) as integer + +integer.min = function integerMin(minimum) { + return Object_assign( + boundedInteger({ gte: minimum }, carryover(this, 'minimum')), + { minimum }, + ) +} +integer.max = function integerMax(maximum) { + return Object_assign( + boundedInteger({ lte: maximum }, carryover(this, 'maximum')), + { maximum }, + ) +} +integer.between = function integerBetween( + min, + max, + minimum = Math_min(min, max), + maximum = Math_max(min, max), +) { + return Object_assign( + boundedInteger({ gte: minimum, lte: maximum }), + { minimum, maximum }, + ) +} + +Object_assign( + integer, + bindUserExtensions(integer, userExtensions), +) + +declare namespace integer { + interface core extends integer.methods { + (u: this['_type'] | Unknown): u is this['_type'] + _type: number + tag: URI.integer + def: this['_type'] + minimum?: number + maximum?: number + } + interface methods { + min>(minimum: Min): integer.Min + max>(maximum: Max): integer.Max + between, Max extends Integer>(minimum: Min, maximum: Max): integer.between<[min: Min, max: Max]> + } + type Min + = [Self] extends [{ maximum: number }] + ? integer.between<[min: X, max: Self['maximum']]> + : integer.min + type Max + = [Self] extends [{ minimum: number }] + ? integer.between<[min: Self['minimum'], max: X]> + : integer.max + interface min extends integer { minimum: Min } + interface max extends integer { maximum: Max } + interface between extends integer { minimum: Bounds[0], maximum: Bounds[1] } +} + +function boundedInteger(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & integer +function boundedInteger(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & integer +function boundedInteger(bounds: Bounds, carry?: {}): {} { + return Object_assign(function BoundedIntegerSchema(u: unknown) { + return integer(u) && within(bounds)(u) + }, carry, integer) +} diff --git a/packages/schema-generator/test/test-data/integer/equals.ts b/packages/schema-generator/test/test-data/integer/equals.ts new file mode 100644 index 00000000..a599f588 --- /dev/null +++ b/packages/schema-generator/test/test-data/integer/equals.ts @@ -0,0 +1,7 @@ +import type { Equal } from '@traversable/registry' +import { SameValueNumber } from "@traversable/registry" + +export type equals = Equal +export function equals(left: number, right: number): boolean { + return SameValueNumber(left, right) +} diff --git a/packages/schema-generator/test/test-data/integer/extension.ts b/packages/schema-generator/test/test-data/integer/extension.ts new file mode 100644 index 00000000..c0990a36 --- /dev/null +++ b/packages/schema-generator/test/test-data/integer/extension.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toString, +} + +export let Extensions = { + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/test/test-data/integer/toJsonSchema.ts b/packages/schema-generator/test/test-data/integer/toJsonSchema.ts new file mode 100644 index 00000000..d531e292 --- /dev/null +++ b/packages/schema-generator/test/test-data/integer/toJsonSchema.ts @@ -0,0 +1,23 @@ +import type { Force, PickIfDefined } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { NumericBounds } from '@traversable/schema-to-json-schema' +import { getNumericBounds } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { (): Force<{ type: 'integer' } & PickIfDefined> } + +export function toJsonSchema(schema: S): toJsonSchema +export function toJsonSchema(schema: t.integer): toJsonSchema { + function integerToJsonSchema() { + const { exclusiveMaximum, exclusiveMinimum, maximum, minimum } = getNumericBounds(schema) + let bounds: NumericBounds = {} + if (typeof exclusiveMinimum === 'number') bounds.exclusiveMinimum = exclusiveMinimum + if (typeof exclusiveMaximum === 'number') bounds.exclusiveMaximum = exclusiveMaximum + if (typeof minimum === 'number') bounds.minimum = minimum + if (typeof maximum === 'number') bounds.maximum = maximum + return { + type: 'integer' as const, + ...bounds, + } + } + return integerToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/integer/toString.ts b/packages/schema-generator/test/test-data/integer/toString.ts new file mode 100644 index 00000000..912565e6 --- /dev/null +++ b/packages/schema-generator/test/test-data/integer/toString.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'number' } +export function toString(): 'number' { return 'number' } diff --git a/packages/schema-generator/test/test-data/integer/validate.ts b/packages/schema-generator/test/test-data/integer/validate.ts new file mode 100644 index 00000000..5f044c5d --- /dev/null +++ b/packages/schema-generator/test/test-data/integer/validate.ts @@ -0,0 +1,13 @@ +import type { ValidationError, ValidationFn } from '@traversable/derive-validators' +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(integerSchema: S): validate { + validateInteger.tag = URI.integer + function validateInteger(u: unknown, path = Array.of()): true | ValidationError[] { + return integerSchema(u) || [NullaryErrors.integer(u, path)] + } + return validateInteger +} diff --git a/packages/schema-generator/test/test-data/intersect/core.ts b/packages/schema-generator/test/test-data/intersect/core.ts new file mode 100644 index 00000000..916aa265 --- /dev/null +++ b/packages/schema-generator/test/test-data/intersect/core.ts @@ -0,0 +1,50 @@ +import type { Unknown } from '@traversable/registry' +import { + _isPredicate, + bindUserExtensions, + intersect as intersect$, + isUnknown as isAny, + Object_assign, + URI, +} from '@traversable/registry' + +import type { Entry, IntersectType, Schema, SchemaLike } from '@traversable/schema-core/namespace' + +export function intersect(...schemas: S): intersect +export function intersect }>(...schemas: S): intersect +export function intersect(...schemas: readonly unknown[]) { + return intersect.def(schemas) +} + +export interface intersect extends intersect.core { + //<%= Types %> +} + +export namespace intersect { + export let userDefinitions: Record = { + //<%= Definitions %> + } as intersect + export function def(xs: readonly [...T]): intersect + /* v8 ignore next 1 */ + export function def(xs: readonly unknown[]): {} { + let userExtensions: Record = { + //<%= Extensions %> + } + const predicate = xs.every(_isPredicate) ? intersect$(xs) : isAny + function IntersectSchema(src: unknown) { return predicate(src) } + IntersectSchema.tag = URI.intersect + IntersectSchema.def = xs + Object_assign(IntersectSchema, intersect.userDefinitions) + return Object_assign(IntersectSchema, bindUserExtensions(IntersectSchema, userExtensions)) + } +} + +export declare namespace intersect { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.intersect + get def(): S + _type: IntersectType + } + type type> = never | T +} diff --git a/packages/schema-generator/test/test-data/intersect/equals.ts b/packages/schema-generator/test/test-data/intersect/equals.ts new file mode 100644 index 00000000..ce880aa1 --- /dev/null +++ b/packages/schema-generator/test/test-data/intersect/equals.ts @@ -0,0 +1,16 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = Equal +export function equals(intersectSchema: t.intersect<[...S]>): equals +export function equals(intersectSchema: t.intersect<[...S]>): equals +export function equals({ def }: t.intersect<{ equals: Equal }[]>): Equal { + function intersectEquals(l: unknown, r: unknown): boolean { + if (Object_is(l, r)) return true + for (let ix = def.length; ix-- !== 0;) + if (!def[ix].equals(l, r)) return false + return true + } + return intersectEquals +} diff --git a/packages/schema-generator/test/test-data/intersect/extension.ts b/packages/schema-generator/test/test-data/intersect/extension.ts new file mode 100644 index 00000000..0927d4dd --- /dev/null +++ b/packages/schema-generator/test/test-data/intersect/extension.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + toString: toString + equals: equals + toJsonSchema: toJsonSchema + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toJsonSchema, + validate, + toString, + equals, +} diff --git a/packages/schema-generator/test/test-data/intersect/toJsonSchema.ts b/packages/schema-generator/test/test-data/intersect/toJsonSchema.ts new file mode 100644 index 00000000..d3942a94 --- /dev/null +++ b/packages/schema-generator/test/test-data/intersect/toJsonSchema.ts @@ -0,0 +1,20 @@ +import type { Returns } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { getSchema } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { + (): { + allOf: { [I in keyof T]: Returns } + } +} + +export function toJsonSchema(intersectSchema: t.intersect): toJsonSchema +export function toJsonSchema(intersectSchema: t.intersect): toJsonSchema +export function toJsonSchema({ def }: t.intersect): () => {} { + function intersectToJsonSchema() { + return { + allOf: def.map(getSchema) + } + } + return intersectToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/intersect/toString.ts b/packages/schema-generator/test/test-data/intersect/toString.ts new file mode 100644 index 00000000..2e179159 --- /dev/null +++ b/packages/schema-generator/test/test-data/intersect/toString.ts @@ -0,0 +1,18 @@ +import type { Join } from '@traversable/registry' +import { Array_isArray } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { callToString } from '@traversable/schema-to-string' + +export interface toString { + (): never | [T] extends [readonly []] ? 'unknown' + /* @ts-expect-error */ + : `(${Join<{ [I in keyof T]: Returns }, ' & '>})` +} + +export function toString(intersectSchema: t.intersect): toString +export function toString({ def }: t.intersect): () => string { + function intersectToString() { + return Array_isArray(def) ? def.length === 0 ? 'never' : `(${def.map(callToString).join(' & ')})` : 'unknown' + } + return intersectToString +} diff --git a/packages/schema-generator/test/test-data/intersect/validate.ts b/packages/schema-generator/test/test-data/intersect/validate.ts new file mode 100644 index 00000000..8a586df2 --- /dev/null +++ b/packages/schema-generator/test/test-data/intersect/validate.ts @@ -0,0 +1,21 @@ +import { URI } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { ValidationError, Validate, Validator } from '@traversable/derive-validators' + +export type validate = Validate + +export function validate(intersectSchema: t.intersect): validate +export function validate(intersectSchema: t.intersect): validate +export function validate({ def }: t.intersect) { + validateIntersect.tag = URI.intersect + function validateIntersect(u: unknown, path = Array.of()): true | ValidationError[] { + let errors = Array.of() + for (let i = 0; i < def.length; i++) { + let results = def[i].validate(u, path) + if (results !== true) + for (let j = 0; j < results.length; j++) errors.push(results[j]) + } + return errors.length === 0 || errors + } + return validateIntersect +} diff --git a/packages/schema-generator/test/test-data/never/core.ts b/packages/schema-generator/test/test-data/never/core.ts new file mode 100644 index 00000000..1f444290 --- /dev/null +++ b/packages/schema-generator/test/test-data/never/core.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { never_ as never } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface never_ extends never_.core { + //<%= Types %> +} + +function NeverSchema(src: unknown): src is never { return src === void 0 } +NeverSchema.tag = URI.never +NeverSchema.def = void 0 as never + +const never_ = Object_assign( + NeverSchema, + userDefinitions, +) as never_ + +Object_assign(never_, userExtensions) + +declare namespace never_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.never + _type: never + def: this['_type'] + } +} diff --git a/packages/schema-generator/test/test-data/never/equals.ts b/packages/schema-generator/test/test-data/never/equals.ts new file mode 100644 index 00000000..5377668e --- /dev/null +++ b/packages/schema-generator/test/test-data/never/equals.ts @@ -0,0 +1,6 @@ +import type { Equal } from "@traversable/registry" + +export type equals = Equal +export function equals(left: never, right: never): boolean { + return false +} \ No newline at end of file diff --git a/packages/schema-generator/test/test-data/never/extension.ts b/packages/schema-generator/test/test-data/never/extension.ts new file mode 100644 index 00000000..c4ecbb88 --- /dev/null +++ b/packages/schema-generator/test/test-data/never/extension.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/test/test-data/never/toJsonSchema.ts b/packages/schema-generator/test/test-data/never/toJsonSchema.ts new file mode 100644 index 00000000..d22338df --- /dev/null +++ b/packages/schema-generator/test/test-data/never/toJsonSchema.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): never } +export function toJsonSchema(): toJsonSchema { + function neverToJsonSchema() { return void 0 as never } + return neverToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/never/toString.ts b/packages/schema-generator/test/test-data/never/toString.ts new file mode 100644 index 00000000..aaabf80d --- /dev/null +++ b/packages/schema-generator/test/test-data/never/toString.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'never' } +export function toString(): 'never' { return 'never' } diff --git a/packages/schema-generator/test/test-data/never/validate.ts b/packages/schema-generator/test/test-data/never/validate.ts new file mode 100644 index 00000000..52ce9ee5 --- /dev/null +++ b/packages/schema-generator/test/test-data/never/validate.ts @@ -0,0 +1,10 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import { NullaryErrors, type ValidationFn } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(_?: t.never): validate { + validateNever.tag = URI.never + function validateNever(u: unknown, path = Array.of()) { return [NullaryErrors.never(u, path)] } + return validateNever +} diff --git a/packages/schema-generator/test/test-data/null/core.ts b/packages/schema-generator/test/test-data/null/core.ts new file mode 100644 index 00000000..f263c284 --- /dev/null +++ b/packages/schema-generator/test/test-data/null/core.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { null_ as null, null_ } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface null_ extends null_.core { + //<%= Types %> +} + +function NullSchema(src: unknown): src is null { return src === void 0 } +NullSchema.def = null +NullSchema.tag = URI.null + +const null_ = Object_assign( + NullSchema, + userDefinitions, +) as null_ + +Object_assign(null_, userExtensions) + +declare namespace null_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.null + _type: null + def: this['_type'] + } +} diff --git a/packages/schema-generator/test/test-data/null/equals.ts b/packages/schema-generator/test/test-data/null/equals.ts new file mode 100644 index 00000000..963c47f5 --- /dev/null +++ b/packages/schema-generator/test/test-data/null/equals.ts @@ -0,0 +1,7 @@ +import type { Equal } from "@traversable/registry" +import { Object_is } from "@traversable/registry" + +export type equals = Equal +export function equals(left: null, right: null): boolean { + return Object_is(left, right) +} \ No newline at end of file diff --git a/packages/schema-generator/test/test-data/null/extension.ts b/packages/schema-generator/test/test-data/null/extension.ts new file mode 100644 index 00000000..c4ecbb88 --- /dev/null +++ b/packages/schema-generator/test/test-data/null/extension.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/test/test-data/null/toJsonSchema.ts b/packages/schema-generator/test/test-data/null/toJsonSchema.ts new file mode 100644 index 00000000..7a3b7c3a --- /dev/null +++ b/packages/schema-generator/test/test-data/null/toJsonSchema.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): { type: 'null', enum: [null] } } +export function toJsonSchema(): toJsonSchema { + function nullToJsonSchema() { return { type: 'null' as const, enum: [null] satisfies [any] } } + return nullToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/null/toString.ts b/packages/schema-generator/test/test-data/null/toString.ts new file mode 100644 index 00000000..35c3aef8 --- /dev/null +++ b/packages/schema-generator/test/test-data/null/toString.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'null' } +export function toString(): 'null' { return 'null' } diff --git a/packages/schema-generator/test/test-data/null/validate.ts b/packages/schema-generator/test/test-data/null/validate.ts new file mode 100644 index 00000000..b2abe36b --- /dev/null +++ b/packages/schema-generator/test/test-data/null/validate.ts @@ -0,0 +1,13 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(nullSchema: t.null): validate { + validateNull.tag = URI.null + function validateNull(u: unknown, path = Array.of()) { + return nullSchema(u) || [NullaryErrors.null(u, path)] + } + return validateNull +} diff --git a/packages/schema-generator/test/test-data/number/core.ts b/packages/schema-generator/test/test-data/number/core.ts new file mode 100644 index 00000000..6575fcac --- /dev/null +++ b/packages/schema-generator/test/test-data/number/core.ts @@ -0,0 +1,138 @@ +import type { Bounds, Unknown } from '@traversable/registry' +import { + bindUserExtensions, + carryover, + Math_min, + Math_max, + Object_assign, + URI, + within, +} from '@traversable/registry' + +export { number_ as number } + +interface number_ extends number_.core { + //<%= Types %> +} + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +function NumberSchema(src: unknown) { return typeof src === 'number' } +NumberSchema.tag = URI.number +NumberSchema.def = 0 + +const number_ = Object_assign( + NumberSchema, + userDefinitions, +) as number_ + +number_.min = function numberMin(minimum) { + return Object_assign( + boundedNumber({ gte: minimum }, carryover(this, 'minimum')), + { minimum }, + ) +} +number_.max = function numberMax(maximum) { + return Object_assign( + boundedNumber({ lte: maximum }, carryover(this, 'maximum')), + { maximum }, + ) +} +number_.moreThan = function numberMoreThan(exclusiveMinimum) { + return Object_assign( + boundedNumber({ gt: exclusiveMinimum }, carryover(this, 'exclusiveMinimum')), + { exclusiveMinimum }, + ) +} +number_.lessThan = function numberLessThan(exclusiveMaximum) { + return Object_assign( + boundedNumber({ lt: exclusiveMaximum }, carryover(this, 'exclusiveMaximum')), + { exclusiveMaximum }, + ) +} +number_.between = function numberBetween( + min, + max, + minimum = Math_min(min, max), + maximum = Math_max(min, max), +) { + return Object_assign( + boundedNumber({ gte: minimum, lte: maximum }), + { minimum, maximum }, + ) +} + +Object_assign( + number_, + bindUserExtensions(number_, userExtensions), +) + +declare namespace number_ { + interface core extends number_.methods { + (u: this['_type'] | Unknown): u is this['_type'] + _type: number + tag: URI.number + def: this['_type'] + minimum?: number + maximum?: number + exclusiveMinimum?: number + exclusiveMaximum?: number + } + interface methods { + min(minimum: Min): number_.Min + max(maximum: Max): number_.Max + moreThan(moreThan: Min): ExclusiveMin + lessThan(lessThan: Max): ExclusiveMax + between(minimum: Min, maximum: Max): number_.between<[min: Min, max: Max]> + } + type Min + = [Self] extends [{ exclusiveMaximum: number }] + ? number_.minStrictMax<[min: X, max: Self['exclusiveMaximum']]> + : [Self] extends [{ maximum: number }] + ? number_.between<[min: X, max: Self['maximum']]> + : number_.min + ; + type Max + = [Self] extends [{ exclusiveMinimum: number }] + ? number_.maxStrictMin<[Self['exclusiveMinimum'], X]> + : [Self] extends [{ minimum: number }] + ? number_.between<[min: Self['minimum'], max: X]> + : number_.max + ; + type ExclusiveMin + = [Self] extends [{ exclusiveMaximum: number }] + ? number_.strictlyBetween<[X, Self['exclusiveMaximum']]> + : [Self] extends [{ maximum: number }] + ? number_.maxStrictMin<[min: X, Self['maximum']]> + : number_.moreThan + ; + type ExclusiveMax + = [Self] extends [{ exclusiveMinimum: number }] + ? number_.strictlyBetween<[Self['exclusiveMinimum'], X]> + : [Self] extends [{ minimum: number }] + ? number_.minStrictMax<[Self['minimum'], min: X]> + : number_.lessThan + ; + interface min extends number_ { minimum: Min } + interface max extends number_ { maximum: Max } + interface moreThan extends number_ { exclusiveMinimum: Min } + interface lessThan extends number_ { exclusiveMaximum: Max } + interface between extends number_ { minimum: Bounds[0], maximum: Bounds[1] } + interface minStrictMax extends number_ { minimum: Bounds[0], exclusiveMaximum: Bounds[1] } + interface maxStrictMin extends number_ { maximum: Bounds[1], exclusiveMinimum: Bounds[0] } + interface strictlyBetween extends number_ { exclusiveMinimum: Bounds[0], exclusiveMaximum: Bounds[1] } +} + +function boundedNumber(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & number_ +function boundedNumber(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & number_ +function boundedNumber(bounds: Bounds, carry?: {}): {} { + return Object_assign(function BoundedNumberSchema(u: unknown) { + return typeof u === 'number' && within(bounds)(u) + }, carry, number_) +} diff --git a/packages/schema-generator/test/test-data/number/equals.ts b/packages/schema-generator/test/test-data/number/equals.ts new file mode 100644 index 00000000..a599f588 --- /dev/null +++ b/packages/schema-generator/test/test-data/number/equals.ts @@ -0,0 +1,7 @@ +import type { Equal } from '@traversable/registry' +import { SameValueNumber } from "@traversable/registry" + +export type equals = Equal +export function equals(left: number, right: number): boolean { + return SameValueNumber(left, right) +} diff --git a/packages/schema-generator/test/test-data/number/extension.ts b/packages/schema-generator/test/test-data/number/extension.ts new file mode 100644 index 00000000..81c311ce --- /dev/null +++ b/packages/schema-generator/test/test-data/number/extension.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + toString: toString + equals: equals + toJsonSchema: toJsonSchema + validate: validate +} + +export let Definitions = { + toString, + equals, +} + +export let Extensions = { + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/test/test-data/number/toJsonSchema.ts b/packages/schema-generator/test/test-data/number/toJsonSchema.ts new file mode 100644 index 00000000..7146d478 --- /dev/null +++ b/packages/schema-generator/test/test-data/number/toJsonSchema.ts @@ -0,0 +1,23 @@ +import type { Force, PickIfDefined } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { NumericBounds } from '@traversable/schema-to-json-schema' +import { getNumericBounds } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { (): Force<{ type: 'number' } & PickIfDefined> } + +export function toJsonSchema(schema: S): toJsonSchema +export function toJsonSchema(schema: t.number): toJsonSchema { + function numberToJsonSchema() { + const { exclusiveMaximum, exclusiveMinimum, maximum, minimum } = getNumericBounds(schema) + let bounds: NumericBounds = {} + if (typeof exclusiveMinimum === 'number') bounds.exclusiveMinimum = exclusiveMinimum + if (typeof exclusiveMaximum === 'number') bounds.exclusiveMaximum = exclusiveMaximum + if (typeof minimum === 'number') bounds.minimum = minimum + if (typeof maximum === 'number') bounds.maximum = maximum + return { + type: 'number' as const, + ...bounds, + } + } + return numberToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/number/toString.ts b/packages/schema-generator/test/test-data/number/toString.ts new file mode 100644 index 00000000..912565e6 --- /dev/null +++ b/packages/schema-generator/test/test-data/number/toString.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'number' } +export function toString(): 'number' { return 'number' } diff --git a/packages/schema-generator/test/test-data/number/validate.ts b/packages/schema-generator/test/test-data/number/validate.ts new file mode 100644 index 00000000..416f639b --- /dev/null +++ b/packages/schema-generator/test/test-data/number/validate.ts @@ -0,0 +1,13 @@ +import type { ValidationError, ValidationFn } from '@traversable/derive-validators' +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(numberSchema: S): validate { + validateNumber.tag = URI.number + function validateNumber(u: unknown, path: (keyof any)[] = []): true | ValidationError[] { + return numberSchema(u) || [NullaryErrors.number(u, path)] + } + return validateNumber +} diff --git a/packages/schema-generator/test/test-data/object/core.ts b/packages/schema-generator/test/test-data/object/core.ts new file mode 100644 index 00000000..4aa57c18 --- /dev/null +++ b/packages/schema-generator/test/test-data/object/core.ts @@ -0,0 +1,79 @@ +import type { Force, SchemaOptions as Options, Unknown } from '@traversable/registry' +import { + applyOptions, + Array_isArray, + bindUserExtensions, + has, + _isPredicate, + Object_assign, + Object_keys, + record as record$, + object as object$, + isAnyObject, + symbol, + URI, +} from '@traversable/registry' + +import type { Entry, Optional, Required, Schema, SchemaLike } from '@traversable/schema-core/namespace' + +export { object_ as object } + +function object_< + S extends { [x: string]: Schema }, + T extends { [K in keyof S]: Entry } +>(schemas: S, options?: Options): object_ +function object_< + S extends { [x: string]: SchemaLike }, + T extends { [K in keyof S]: Entry } +>(schemas: S, options?: Options): object_ +function object_(schemas: { [x: string]: Schema }, options?: Options) { + return object_.def(schemas, options) +} + +interface object_ extends object_.core { + //<%= Types %> +} + +namespace object_ { + export let userDefinitions: Record = { + //<%= Definitions %> + } as object_ + export function def(xs: T, $?: Options, opt?: string[]): object_ + /* v8 ignore next 1 */ + export function def(xs: { [x: string]: unknown }, $?: Options, opt_?: string[]): {} { + let userExtensions: Record = { + //<%= Extensions %> + } + const keys = Object_keys(xs) + const opt = Array_isArray(opt_) ? opt_ : keys.filter((k) => has(symbol.optional)(xs[k])) + const req = keys.filter((k) => !has(symbol.optional)(xs[k])) + const predicate = !record$(_isPredicate)(xs) ? isAnyObject : object$(xs, applyOptions($)) + function ObjectSchema(src: unknown) { return predicate(src) } + ObjectSchema.tag = URI.object + ObjectSchema.def = xs + ObjectSchema.opt = opt + ObjectSchema.req = req + Object_assign(ObjectSchema, userDefinitions) + return Object_assign(ObjectSchema, bindUserExtensions(ObjectSchema, userExtensions)) + } +} + +declare namespace object_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + _type: object_.type + tag: URI.object + get def(): S + opt: Optional + req: Required + } + type type< + S, + Opt extends Optional = Optional, + Req extends Required = Required, + T = Force< + & { [K in Req]-?: S[K]['_type' & keyof S[K]] } + & { [K in Opt]+?: S[K]['_type' & keyof S[K]] } + > + > = never | T +} diff --git a/packages/schema-generator/test/test-data/object/equals.ts b/packages/schema-generator/test/test-data/object/equals.ts new file mode 100644 index 00000000..1e91cfbc --- /dev/null +++ b/packages/schema-generator/test/test-data/object/equals.ts @@ -0,0 +1,29 @@ +import type * as T from '@traversable/registry' +import { Array_isArray, Object_hasOwn, Object_is } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = never | T.Equal +export function equals(objectSchema: t.object): equals> +export function equals(objectSchema: t.object): equals> +export function equals({ def }: t.object): equals> { + function objectEquals(l: { [x: string]: unknown }, r: { [x: string]: unknown }) { + if (Object_is(l, r)) return true + if (!l || typeof l !== 'object' || Array_isArray(l)) return false + if (!r || typeof r !== 'object' || Array_isArray(r)) return false + for (const k in def) { + const lHas = Object_hasOwn(l, k) + const rHas = Object_hasOwn(r, k) + if (lHas) { + if (!rHas) return false + if (!def[k].equals(l[k], r[k])) return false + } + if (rHas) { + if (!lHas) return false + if (!def[k].equals(l[k], r[k])) return false + } + if (!def[k].equals(l[k], r[k])) return false + } + return true + } + return objectEquals +} diff --git a/packages/schema-generator/test/test-data/object/extension.ts b/packages/schema-generator/test/test-data/object/extension.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema-generator/test/test-data/object/extension.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/test/test-data/object/toJsonSchema.ts b/packages/schema-generator/test/test-data/object/toJsonSchema.ts new file mode 100644 index 00000000..bc79c90c --- /dev/null +++ b/packages/schema-generator/test/test-data/object/toJsonSchema.ts @@ -0,0 +1,27 @@ +import type { Returns } from '@traversable/registry' +import { fn, Object_keys } from '@traversable/registry' +import type { RequiredKeys } from '@traversable/schema-to-json-schema' +import { isRequired, property } from '@traversable/schema-to-json-schema' +import { t } from '@traversable/schema-core' + +export interface toJsonSchema = RequiredKeys> { + (): { + type: 'object' + required: { [I in keyof KS]: KS[I] & string } + properties: { [K in keyof T]: Returns } + } +} + +export function toJsonSchema(objectSchema: t.object): toJsonSchema +export function toJsonSchema(objectSchema: t.object): toJsonSchema +export function toJsonSchema({ def }: { def: { [x: string]: unknown } }): () => { type: 'object', required: string[], properties: {} } { + const required = Object_keys(def).filter(isRequired(def)) + function objectToJsonSchema() { + return { + type: 'object' as const, + required, + properties: fn.map(def, (v, k) => property(required)(v, k as number | string)), + } + } + return objectToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/object/toString.ts b/packages/schema-generator/test/test-data/object/toString.ts new file mode 100644 index 00000000..ad084ffa --- /dev/null +++ b/packages/schema-generator/test/test-data/object/toString.ts @@ -0,0 +1,41 @@ +import type { Join, UnionToTuple } from '@traversable/registry' +import { symbol } from '@traversable/registry' +import { t } from '@traversable/schema-core' + +/** @internal */ +type Symbol_optional = typeof Symbol_optional +const Symbol_optional: typeof symbol.optional = symbol.optional + +/** @internal */ +const hasOptionalSymbol = (u: unknown): u is { toString(): T } => + !!u && typeof u === 'function' + && Symbol_optional in u + && typeof u[Symbol_optional] === 'number' + +/** @internal */ +const hasToString = (x: unknown): x is { toString(): string } => + !!x && typeof x === 'function' && 'toString' in x && typeof x.toString === 'function' + +export interface toString> { + (): never + | [keyof T] extends [never] ? '{}' + /* @ts-expect-error */ + : `{ ${Join<{ [I in keyof _]: `'${_[I]}${T[_[I]] extends { [Symbol_optional]: any } ? `'?` : `'`}: ${ReturnType}` }, ', '>} }` +} + +export function toString>(objectSchema: t.object): toString +export function toString({ def }: t.object) { + function objectToString() { + if (!!def && typeof def === 'object') { + const entries = Object.entries(def) + if (entries.length === 0) return '{}' + else return `{ ${entries.map(([k, x]) => `'${k}${hasOptionalSymbol(x) ? "'?" : "'" + }: ${hasToString(x) ? x.toString() : 'unknown' + }`).join(', ') + } }` + } + else return '{ [x: string]: unknown }' + } + + return objectToString +} diff --git a/packages/schema-generator/test/test-data/object/validate.ts b/packages/schema-generator/test/test-data/object/validate.ts new file mode 100644 index 00000000..da8e64f1 --- /dev/null +++ b/packages/schema-generator/test/test-data/object/validate.ts @@ -0,0 +1,108 @@ +import { + Array_isArray, + Object_keys, + Object_hasOwn, + typeName, + URI, +} from '@traversable/registry' +import { t, getConfig } from '@traversable/schema-core' +import type { ValidationError, Validator, ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors, Errors, UnaryErrors } from '@traversable/derive-validators' + +/** @internal */ +let isObject = (u: unknown): u is { [x: string]: unknown } => + !!u && typeof u === 'object' && !Array_isArray(u) + +/** @internal */ +let isKeyOf = (k: keyof any, u: T): k is keyof T => + !!u && (typeof u === 'function' || typeof u === 'object') && k in u + +/** @internal */ +let isOptional = t.has('tag', t.eq(URI.optional)) + + +export type validate = never | ValidationFn + +export function validate(objectSchema: t.object): validate +export function validate(objectSchema: t.object): validate +export function validate(objectSchema: t.object): validate<{ [x: string]: unknown }> { + validateObject.tag = URI.object + function validateObject(u: unknown, path_ = Array.of()) { + // if (objectSchema(u)) return true + if (!isObject(u)) return [Errors.object(u, path_)] + let errors = Array.of() + let { schema: { optionalTreatment } } = getConfig() + let keys = Object_keys(objectSchema.def) + if (optionalTreatment === 'exactOptional') { + for (let i = 0, len = keys.length; i < len; i++) { + let k = keys[i] + let path = [...path_, k] + if (Object_hasOwn(u, k) && u[k] === undefined) { + if (isOptional(objectSchema.def[k].validate)) { + let tag = typeName(objectSchema.def[k].validate) + if (isKeyOf(tag, NullaryErrors)) { + let args = [u[k], path, tag] as never as [unknown, (keyof any)[]] + errors.push(NullaryErrors[tag](...args)) + } + else if (isKeyOf(tag, UnaryErrors)) { + errors.push(UnaryErrors[tag as keyof typeof UnaryErrors].invalid(u[k], path)) + } + } + let results = objectSchema.def[k].validate(u[k], path) + if (results === true) continue + let tag = typeName(objectSchema.def[k].validate) + if (isKeyOf(tag, NullaryErrors)) { + errors.push(NullaryErrors[tag](u[k], path, tag)) + } + else if (isKeyOf(tag, UnaryErrors)) { + errors.push(UnaryErrors[tag].invalid(u[k], path)) + } + errors.push(...results) + } + else if (Object_hasOwn(u, k)) { + let results = objectSchema.def[k].validate(u[k], path) + if (results === true) continue + errors.push(...results) + continue + } else { + errors.push(UnaryErrors.object.missing(u, path)) + continue + } + } + } + else { + for (let i = 0, len = keys.length; i < len; i++) { + let k = keys[i] + let path = [...path_, k] + if (!Object_hasOwn(u, k)) { + if (!isOptional(objectSchema.def[k].validate)) { + errors.push(UnaryErrors.object.missing(u, path)) + continue + } + else { + if (!Object_hasOwn(u, k)) continue + if (isOptional(objectSchema.def[k].validate) && Object_hasOwn(u, k)) { + if (u[k] === undefined) continue + let results = objectSchema.def[k].validate(u[k], path) + if (results === true) continue + for (let j = 0; j < results.length; j++) { + let result = results[j] + errors.push(result) + continue + } + } + } + } + let results = objectSchema.def[k].validate(u[k], path) + if (results === true) continue + for (let l = 0; l < results.length; l++) { + let result = results[l] + errors.push(result) + } + } + } + return errors.length === 0 || errors + } + + return validateObject +} diff --git a/packages/schema-generator/test/test-data/of/index.ts b/packages/schema-generator/test/test-data/of/index.ts new file mode 100644 index 00000000..8f6f9030 --- /dev/null +++ b/packages/schema-generator/test/test-data/of/index.ts @@ -0,0 +1,44 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +import type { + Entry, + Guard, + Guarded, + SchemaLike, +} from '@traversable/schema-core/namespace' + +export interface of extends of.core { + //<%= Types %> +} + +export function of(typeguard: S): Entry +export function of(typeguard: S): of +export function of(typeguard: (Guard) & { tag?: URI.inline, def?: Guard }) { + typeguard.def = typeguard + return Object_assign(typeguard, of.prototype) +} + +export namespace of { + export let userDefinitions: Record = { + //<%= Definitions %> + } + export function def(guard: T): of + /* v8 ignore next 6 */ + export function def(guard: T) { + function InlineSchema(src: unknown) { return guard(src) } + InlineSchema.tag = URI.inline + InlineSchema.def = guard + return InlineSchema + } +} + +export declare namespace of { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + _type: Guarded + tag: URI.inline + get def(): S + } + type type> = never | T +} diff --git a/packages/schema-generator/test/test-data/optional/core.ts b/packages/schema-generator/test/test-data/optional/core.ts new file mode 100644 index 00000000..ba7120bc --- /dev/null +++ b/packages/schema-generator/test/test-data/optional/core.ts @@ -0,0 +1,55 @@ +import type { Unknown } from '@traversable/registry' +import { + bindUserExtensions, + has, + _isPredicate, + optional as optional$, + Object_assign, + symbol, + URI, + isUnknown as isAny, +} from '@traversable/registry' + +import type { Entry, Schema, SchemaLike } from '@traversable/schema-core/namespace' + +export function optional(schema: S): optional +export function optional(schema: S): optional> +export function optional(schema: S): optional { return optional.def(schema) } + +export interface optional extends optional.core { + //<%= Types %> +} + +export namespace optional { + export let userDefinitions: Record = { + //<%= Definitions %> + } + export function def(x: T): optional + export function def(x: T) { + let userExtensions: Record = { + //<%= Extensions %> + } + const predicate = _isPredicate(x) ? optional$(x) : isAny + function OptionalSchema(src: unknown) { return predicate(src) } + OptionalSchema.tag = URI.optional + OptionalSchema.def = x + OptionalSchema[symbol.optional] = 1 + Object_assign(OptionalSchema, { ...optional.userDefinitions, get def() { return x } }) + return Object_assign(OptionalSchema, bindUserExtensions(OptionalSchema, userExtensions)) + } + export const is + : (u: unknown) => u is optional + /* v8 ignore next 1 */ + = has('tag', (u) => u === URI.optional) +} + +export declare namespace optional { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.optional + _type: undefined | S['_type' & keyof S] + def: S + [symbol.optional]: number + } + export type type = never | T +} diff --git a/packages/schema-generator/test/test-data/optional/equals.ts b/packages/schema-generator/test/test-data/optional/equals.ts new file mode 100644 index 00000000..25944376 --- /dev/null +++ b/packages/schema-generator/test/test-data/optional/equals.ts @@ -0,0 +1,13 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = never | Equal +export function equals(optionalSchema: t.optional): equals +export function equals(optionalSchema: t.optional): equals +export function equals({ def }: t.optional<{ equals: Equal }>): Equal { + return function optionalEquals(l: unknown, r: unknown): boolean { + if (Object_is(l, r)) return true + return def.equals(l, r) + } +} diff --git a/packages/schema-generator/test/test-data/optional/extension.ts b/packages/schema-generator/test/test-data/optional/extension.ts new file mode 100644 index 00000000..0e7c4478 --- /dev/null +++ b/packages/schema-generator/test/test-data/optional/extension.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} + diff --git a/packages/schema-generator/test/test-data/optional/toJsonSchema.ts b/packages/schema-generator/test/test-data/optional/toJsonSchema.ts new file mode 100644 index 00000000..82bff553 --- /dev/null +++ b/packages/schema-generator/test/test-data/optional/toJsonSchema.ts @@ -0,0 +1,19 @@ +import type { Force } from '@traversable/registry' +import type { Returns } from '@traversable/registry' +import { symbol } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { getSchema, wrapOptional } from '@traversable/schema-to-json-schema' + +type Nullable = Force + +export interface toJsonSchema { + (): Nullable> + [symbol.optional]: number +} + +export function toJsonSchema(optionalSchema: t.optional): toJsonSchema +export function toJsonSchema({ def }: t.optional) { + function optionalToJsonSchema() { return getSchema(def) } + optionalToJsonSchema[symbol.optional] = wrapOptional(def) + return optionalToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/optional/toString.ts b/packages/schema-generator/test/test-data/optional/toString.ts new file mode 100644 index 00000000..f4c96cc8 --- /dev/null +++ b/packages/schema-generator/test/test-data/optional/toString.ts @@ -0,0 +1,15 @@ +import type { t } from '@traversable/schema-core' +import { callToString } from '@traversable/schema-to-string' + +export interface toString { + /* @ts-expect-error */ + (): never | `(${ReturnType} | undefined)` +} + +export function toString(optionalSchema: t.optional): toString +export function toString({ def }: t.optional): () => string { + function optionalToString(): string { + return '(' + callToString(def) + ' | undefined)' + } + return optionalToString +} diff --git a/packages/schema-generator/test/test-data/optional/validate.ts b/packages/schema-generator/test/test-data/optional/validate.ts new file mode 100644 index 00000000..feb93d53 --- /dev/null +++ b/packages/schema-generator/test/test-data/optional/validate.ts @@ -0,0 +1,17 @@ +import { URI } from '@traversable/registry' +import { t } from '@traversable/schema-core' +import type { Validate, Validator, ValidationFn } from '@traversable/derive-validators' + +export type validate = Validate + +export function validate(optionalSchema: t.optional): validate +export function validate(optionalSchema: t.optional): validate +export function validate({ def }: t.optional): ValidationFn { + validateOptional.tag = URI.optional + validateOptional.optional = 1 + function validateOptional(u: unknown, path = Array.of()) { + if (u === void 0) return true + return def.validate(u, path) + } + return validateOptional +} diff --git a/packages/schema-generator/test/test-data/record/core.ts b/packages/schema-generator/test/test-data/record/core.ts new file mode 100644 index 00000000..b5838df0 --- /dev/null +++ b/packages/schema-generator/test/test-data/record/core.ts @@ -0,0 +1,50 @@ +import type { Unknown } from '@traversable/registry' +import { + isAnyObject, + record as record$, + bindUserExtensions, + _isPredicate, + Object_assign, + URI, +} from '@traversable/registry' + +import type { Entry, Schema, SchemaLike } from '@traversable/schema-core/namespace' + +export function record(schema: S): record +export function record(schema: S): record> +export function record(schema: Schema) { + return record.def(schema) +} + +export interface record extends record.core { + //<%= Types %> +} + +export namespace record { + export let userDefinitions: Record = { + //<%= Definitions %> + } + export function def(x: T): record + /* v8 ignore next 1 */ + export function def(x: unknown): {} { + let userExtensions: Record = { + //<%= Extensions %> + } + const predicate = _isPredicate(x) ? record$(x) : isAnyObject + function RecordSchema(src: unknown) { return predicate(src) } + RecordSchema.tag = URI.record + RecordSchema.def = x + Object_assign(RecordSchema, record.userDefinitions) + return Object_assign(RecordSchema, bindUserExtensions(RecordSchema, userExtensions)) + } +} + +export declare namespace record { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.record + get def(): S + _type: Record + } + export type type> = never | T +} diff --git a/packages/schema-generator/test/test-data/record/equals.ts b/packages/schema-generator/test/test-data/record/equals.ts new file mode 100644 index 00000000..07addff1 --- /dev/null +++ b/packages/schema-generator/test/test-data/record/equals.ts @@ -0,0 +1,32 @@ +import type { Equal } from '@traversable/registry' +import { Array_isArray, Object_is, Object_keys, Object_hasOwn } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = never | Equal +export function equals(recordSchema: t.record): equals +export function equals(recordSchema: t.record): equals +export function equals({ def }: t.record<{ equals: Equal }>): Equal> { + function recordEquals(l: Record, r: Record): boolean { + if (Object_is(l, r)) return true + if (!l || typeof l !== 'object' || Array_isArray(l)) return false + if (!r || typeof r !== 'object' || Array_isArray(r)) return false + const lhs = Object_keys(l) + const rhs = Object_keys(r) + let len = lhs.length + let k: string + if (len !== rhs.length) return false + for (let ix = len; ix-- !== 0;) { + k = lhs[ix] + if (!Object_hasOwn(r, k)) return false + if (!(def.equals(l[k], r[k]))) return false + } + len = rhs.length + for (let ix = len; ix-- !== 0;) { + k = rhs[ix] + if (!Object_hasOwn(l, k)) return false + if (!(def.equals(l[k], r[k]))) return false + } + return true + } + return recordEquals +} diff --git a/packages/schema-generator/test/test-data/record/extension.ts b/packages/schema-generator/test/test-data/record/extension.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema-generator/test/test-data/record/extension.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/test/test-data/record/toJsonSchema.ts b/packages/schema-generator/test/test-data/record/toJsonSchema.ts new file mode 100644 index 00000000..2503d568 --- /dev/null +++ b/packages/schema-generator/test/test-data/record/toJsonSchema.ts @@ -0,0 +1,21 @@ +import type { t } from '@traversable/schema-core' +import type * as T from '@traversable/registry' +import { getSchema } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { + (): { + type: 'object' + additionalProperties: T.Returns + } +} + +export function toJsonSchema(recordSchema: t.record): toJsonSchema +export function toJsonSchema(recordSchema: t.record): toJsonSchema +export function toJsonSchema({ def }: { def: unknown }): () => { type: 'object', additionalProperties: unknown } { + return function recordToJsonSchema() { + return { + type: 'object' as const, + additionalProperties: getSchema(def), + } + } +} diff --git a/packages/schema-generator/test/test-data/record/toString.ts b/packages/schema-generator/test/test-data/record/toString.ts new file mode 100644 index 00000000..868f3544 --- /dev/null +++ b/packages/schema-generator/test/test-data/record/toString.ts @@ -0,0 +1,17 @@ +import type { Returns } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { callToString } from '@traversable/schema-to-string' + +export interface toString { + /* @ts-expect-error */ + (): never | `Record}>` +} + +export function toString>(recordSchema: S): toString +export function toString(recordSchema: t.record): toString +export function toString({ def }: { def: unknown }): () => string { + function recordToString() { + return `Record` + } + return recordToString +} diff --git a/packages/schema-generator/test/test-data/record/validate.ts b/packages/schema-generator/test/test-data/record/validate.ts new file mode 100644 index 00000000..6d958004 --- /dev/null +++ b/packages/schema-generator/test/test-data/record/validate.ts @@ -0,0 +1,24 @@ +import type { t } from '@traversable/schema-core' +import { Array_isArray, Object_keys, URI } from '@traversable/registry' +import type { ValidationError, ValidationFn, Validator } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = never | ValidationFn +export function validate(recordSchema: t.record): validate +export function validate(recordSchema: t.record): validate +export function validate({ def: { validate = () => true } }: t.record) { + validateRecord.tag = URI.record + function validateRecord(u: unknown, path = Array.of()) { + if (!u || typeof u !== 'object' || Array_isArray(u)) return [NullaryErrors.record(u, path)] + let errors = Array.of() + let keys = Object_keys(u) + for (let k of keys) { + let y = u[k] + let results = validate(y, [...path, k]) + if (results === true) continue + else errors.push(...results) + } + return errors.length === 0 || errors + } + return validateRecord +} diff --git a/packages/schema-generator/test/test-data/string/core.ts b/packages/schema-generator/test/test-data/string/core.ts new file mode 100644 index 00000000..4791ab8a --- /dev/null +++ b/packages/schema-generator/test/test-data/string/core.ts @@ -0,0 +1,102 @@ +import type { Bounds, Integer, Unknown } from '@traversable/registry' +import { + bindUserExtensions, + carryover, + Math_min, + Math_max, + Object_assign, + URI, + within, +} from '@traversable/registry' + + +interface string_ extends string_.core { + //<%= Types %> +} + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +export { string_ as string } + +function StringSchema(src: unknown) { return typeof src === 'string' } +StringSchema.tag = URI.string +StringSchema.def = '' + +const string_ = Object_assign( + StringSchema, + userDefinitions, +) as string_ + +string_.min = function stringMinLength(minLength) { + return Object_assign( + boundedString({ gte: minLength }, carryover(this, 'minLength')), + { minLength }, + ) +} +string_.max = function stringMaxLength(maxLength) { + return Object_assign( + boundedString({ lte: maxLength }, carryover(this, 'maxLength')), + { maxLength }, + ) +} +string_.between = function stringBetween( + min, + max, + minLength = Math_min(min, max), + maxLength = Math_max(min, max)) { + return Object_assign( + boundedString({ gte: minLength, lte: maxLength }), + { minLength, maxLength }, + ) +} + +Object_assign( + string_, + bindUserExtensions(string_, userExtensions), +) + +declare namespace string_ { + interface core extends string_.methods { + (u: this['_type'] | Unknown): u is this['_type'] + _type: string + tag: URI.string + def: this['_type'] + } + interface methods { + minLength?: number + maxLength?: number + min>(minLength: Min): string_.Min + max>(maxLength: Max): string_.Max + between, Max extends Integer>(minLength: Min, maxLength: Max): string_.between<[min: Min, max: Max]> + } + type Min + = [Self] extends [{ maxLength: number }] + ? string_.between<[min: Min, max: Self['maxLength']]> + : string_.min + ; + type Max + = [Self] extends [{ minLength: number }] + ? string_.between<[min: Self['minLength'], max: Max]> + : string_.max + ; + interface min extends string_ { minLength: Min } + interface max extends string_ { maxLength: Max } + interface between extends string_ { + minLength: Bounds[0] + maxLength: Bounds[1] + } +} + +function boundedString(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & string_ +function boundedString(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & string_ +function boundedString(bounds: Bounds, carry?: {}): {} { + return Object_assign(function BoundedStringSchema(u: unknown) { + return string_(u) && within(bounds)(u.length) + }, carry, string_) +} diff --git a/packages/schema-generator/test/test-data/string/equals.ts b/packages/schema-generator/test/test-data/string/equals.ts new file mode 100644 index 00000000..b9444108 --- /dev/null +++ b/packages/schema-generator/test/test-data/string/equals.ts @@ -0,0 +1,6 @@ +import type { Equal } from '@traversable/registry' + +export type equals = Equal +export function equals(left: string, right: string): boolean { + return left === right +} diff --git a/packages/schema-generator/test/test-data/string/extension.ts b/packages/schema-generator/test/test-data/string/extension.ts new file mode 100644 index 00000000..63b7046c --- /dev/null +++ b/packages/schema-generator/test/test-data/string/extension.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + toString, + equals, +} + +export let Extensions = { + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/test/test-data/string/toJsonSchema.ts b/packages/schema-generator/test/test-data/string/toJsonSchema.ts new file mode 100644 index 00000000..2956c069 --- /dev/null +++ b/packages/schema-generator/test/test-data/string/toJsonSchema.ts @@ -0,0 +1,22 @@ +import type { Force, PickIfDefined } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { has } from '@traversable/registry' +import type { SizeBounds } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { + (): Force<{ type: 'string' } & PickIfDefined> +} + +export function toJsonSchema(schema: S): toJsonSchema +export function toJsonSchema(schema: t.string): () => { type: 'string' } & Partial { + function stringToJsonSchema() { + const minLength = has('minLength', (u: any) => typeof u === 'number')(schema) ? schema.minLength : null + const maxLength = has('maxLength', (u: any) => typeof u === 'number')(schema) ? schema.maxLength : null + let out: { type: 'string' } & Partial = { type: 'string' } + minLength !== null && void (out.minLength = minLength) + maxLength !== null && void (out.maxLength = maxLength) + + return out + } + return stringToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/string/toString.ts b/packages/schema-generator/test/test-data/string/toString.ts new file mode 100644 index 00000000..86a98e16 --- /dev/null +++ b/packages/schema-generator/test/test-data/string/toString.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'string' } +export function toString(): 'string' { return 'string' } diff --git a/packages/schema-generator/test/test-data/string/validate.ts b/packages/schema-generator/test/test-data/string/validate.ts new file mode 100644 index 00000000..8ea63e07 --- /dev/null +++ b/packages/schema-generator/test/test-data/string/validate.ts @@ -0,0 +1,14 @@ +import type { ValidationError, ValidationFn } from '@traversable/derive-validators' +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(stringSchema: S): validate { + validateString.tag = URI.string + function validateString(u: unknown, path = Array.of()): true | ValidationError[] { + return stringSchema(u) || [NullaryErrors.number(u, path)] + } + return validateString +} + diff --git a/packages/schema-generator/test/test-data/symbol/core.ts b/packages/schema-generator/test/test-data/symbol/core.ts new file mode 100644 index 00000000..366e80e0 --- /dev/null +++ b/packages/schema-generator/test/test-data/symbol/core.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { symbol_ as symbol } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface symbol_ extends symbol_.core { + //<%= Types %> +} + +function SymbolSchema(src: unknown): src is symbol { return src === void 0 } +SymbolSchema.tag = URI.symbol +SymbolSchema.def = Symbol() + +const symbol_ = Object_assign( + SymbolSchema, + userDefinitions, +) as symbol_ + +Object_assign(symbol_, userExtensions) + +declare namespace symbol_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.symbol + _type: symbol + def: this['_type'] + } +} diff --git a/packages/schema-generator/test/test-data/symbol/equals.ts b/packages/schema-generator/test/test-data/symbol/equals.ts new file mode 100644 index 00000000..4793732f --- /dev/null +++ b/packages/schema-generator/test/test-data/symbol/equals.ts @@ -0,0 +1,7 @@ +import type { Equal } from "@traversable/registry" +import { Object_is } from "@traversable/registry" + +export type equals = Equal +export function equals(left: symbol, right: symbol): boolean { + return Object_is(left, right) +} \ No newline at end of file diff --git a/packages/schema-generator/test/test-data/symbol/extension.ts b/packages/schema-generator/test/test-data/symbol/extension.ts new file mode 100644 index 00000000..c4ecbb88 --- /dev/null +++ b/packages/schema-generator/test/test-data/symbol/extension.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/test/test-data/symbol/toJsonSchema.ts b/packages/schema-generator/test/test-data/symbol/toJsonSchema.ts new file mode 100644 index 00000000..7046b08e --- /dev/null +++ b/packages/schema-generator/test/test-data/symbol/toJsonSchema.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function symbolToJsonSchema() { return void 0 } + return symbolToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/symbol/toString.ts b/packages/schema-generator/test/test-data/symbol/toString.ts new file mode 100644 index 00000000..5651fe27 --- /dev/null +++ b/packages/schema-generator/test/test-data/symbol/toString.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'symbol' } +export function toString(): 'symbol' { return 'symbol' } diff --git a/packages/schema-generator/test/test-data/symbol/validate.ts b/packages/schema-generator/test/test-data/symbol/validate.ts new file mode 100644 index 00000000..302e11f2 --- /dev/null +++ b/packages/schema-generator/test/test-data/symbol/validate.ts @@ -0,0 +1,13 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(symbolSchema: t.symbol): validate { + validateSymbol.tag = URI.symbol + function validateSymbol(u: unknown, path = Array.of()) { + return symbolSchema(true as const) || [NullaryErrors.symbol(u, path)] + } + return validateSymbol +} diff --git a/packages/schema-generator/test/test-data/tuple/core.ts b/packages/schema-generator/test/test-data/tuple/core.ts new file mode 100644 index 00000000..2b277799 --- /dev/null +++ b/packages/schema-generator/test/test-data/tuple/core.ts @@ -0,0 +1,86 @@ +import type { + SchemaOptions as Options, + TypeError, + Unknown +} from '@traversable/registry' + +import { + Array_isArray, + bindUserExtensions, + getConfig, + has, + _isPredicate, + Object_assign, + parseArgs, + symbol, + tuple as tuple$, + URI, +} from '@traversable/registry' + +import type { + Entry, + FirstOptionalItem, + invalid, + Schema, + SchemaLike, + TupleType, + ValidateTuple +} from '@traversable/schema-core/namespace' + +import type { optional } from '../optional/core.js' + +export { tuple } + +function tuple }>(...schemas: tuple.validate): tuple, T>> +function tuple(...schemas: tuple.validate): tuple, S>> +function tuple }>(...args: [...schemas: tuple.validate, options: Options]): tuple, T>> +function tuple(...args: [...schemas: tuple.validate, options: Options]): tuple, S>> +function tuple }>(...schemas: tuple.validate): tuple, T>> +function tuple(...schemas: tuple.validate): tuple, S>> +function tuple(...args: [...SchemaLike[]] | [...SchemaLike[], Options]) { + return tuple.def(...parseArgs(getConfig().schema, args)) +} + +interface tuple extends tuple.core { + //<%= Types %> +} + +namespace tuple { + export let userDefinitions: Record = { + //<%= Definitions %> + } as tuple + export function def(xs: readonly [...T], $?: Options, opt_?: number): tuple + /* v8 ignore next 1 */ + export function def(xs: readonly unknown[], $: Options = getConfig().schema, opt_?: number): {} { + let userExtensions: Record = { + //<%= Extensions %> + } + const opt = opt_ || xs.findIndex(has(symbol.optional)) + const options = { + ...$, minLength: $.optionalTreatment === 'treatUndefinedAndOptionalAsTheSame' ? -1 : xs.findIndex(has(symbol.optional)) + } satisfies tuple.InternalOptions + const predicate = !xs.every(_isPredicate) ? Array_isArray : tuple$(xs, options) + function TupleSchema(src: unknown) { return predicate(src) } + TupleSchema.tag = URI.tuple + TupleSchema.def = xs + TupleSchema.opt = opt + Object_assign(TupleSchema, tuple.userDefinitions) + return Object_assign(TupleSchema, bindUserExtensions(TupleSchema, userExtensions)) + } +} + +declare namespace tuple { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.tuple + _type: TupleType + opt: FirstOptionalItem + def: S + } + type type> = never | T + type InternalOptions = { minLength?: number } + type validate = ValidateTuple> + + type from + = TypeError extends V[number] ? { [I in keyof V]: V[I] extends TypeError ? invalid> : V[I] } : T +} diff --git a/packages/schema-generator/test/test-data/tuple/equals.ts b/packages/schema-generator/test/test-data/tuple/equals.ts new file mode 100644 index 00000000..4d9de15c --- /dev/null +++ b/packages/schema-generator/test/test-data/tuple/equals.ts @@ -0,0 +1,27 @@ +import type { Equal } from '@traversable/registry' +import { Array_isArray, Object_hasOwn, Object_is } from '@traversable/registry' +import { t } from '@traversable/schema-core' + +export type equals = Equal + +export function equals(tupleSchema: t.tuple): equals +export function equals(tupleSchema: t.tuple): equals +export function equals(tupleSchema: t.tuple) { + function tupleEquals(l: typeof tupleSchema['_type'], r: typeof tupleSchema['_type']): boolean { + if (Object_is(l, r)) return true + if (Array_isArray(l)) { + if (!Array_isArray(r)) return false + for (let ix = tupleSchema.def.length; ix-- !== 0;) { + if (!Object_hasOwn(l, ix) && !Object_hasOwn(r, ix)) continue + if (Object_hasOwn(l, ix) && !Object_hasOwn(r, ix)) return false + if (!Object_hasOwn(l, ix) && Object_hasOwn(r, ix)) return false + if (Object_hasOwn(l, ix) && Object_hasOwn(r, ix)) { + if (!tupleSchema.def[ix].equals(l[ix], r[ix])) return false + } + } + return true + } + return false + } + return tupleEquals +} diff --git a/packages/schema-generator/test/test-data/tuple/extension.ts b/packages/schema-generator/test/test-data/tuple/extension.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema-generator/test/test-data/tuple/extension.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/test/test-data/tuple/toJsonSchema.ts b/packages/schema-generator/test/test-data/tuple/toJsonSchema.ts new file mode 100644 index 00000000..ed4cf900 --- /dev/null +++ b/packages/schema-generator/test/test-data/tuple/toJsonSchema.ts @@ -0,0 +1,37 @@ +import type { Returns } from '@traversable/registry' +import { t } from '@traversable/schema-core' +import { applyTupleOptionality, minItems } from '@traversable/schema-to-json-schema' +import type { MinItems } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { + (): { + type: 'array', + items: { [I in keyof T]: Returns } + additionalItems: false + minItems: MinItems + maxItems: T['length' & keyof T] + } +} + +export function toJsonSchema(tupleSchema: t.tuple): toJsonSchema +export function toJsonSchema({ def }: t.tuple): () => { + type: 'array' + items: unknown + additionalItems: false + minItems?: {} + maxItems?: number +} { + function tupleToJsonSchema() { + let min = minItems(def) + let max = def.length + let items = applyTupleOptionality(def, { min, max }) + return { + type: 'array' as const, + additionalItems: false as const, + items, + minItems: min, + maxItems: max, + } + } + return tupleToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/tuple/toString.ts b/packages/schema-generator/test/test-data/tuple/toString.ts new file mode 100644 index 00000000..b04c3381 --- /dev/null +++ b/packages/schema-generator/test/test-data/tuple/toString.ts @@ -0,0 +1,26 @@ +import type { Join } from '@traversable/registry' +import { Array_isArray } from '@traversable/registry' +import { t } from '@traversable/schema-core' +import { hasToString } from '@traversable/schema-to-string' + +export interface toString { + (): never | `[${Join<{ + [I in keyof T]: `${ + /* @ts-expect-error */ + T[I] extends { [Symbol_optional]: any } ? `_?: ${ReturnType}` : ReturnType + }` + }, ', '>}]` +} + +export function toString(tupleSchema: t.tuple): toString +export function toString(tupleSchema: t.tuple): () => string { + function stringToString() { + return Array_isArray(tupleSchema.def) + ? `[${tupleSchema.def.map( + (x) => t.optional.is(x) + ? `_?: ${hasToString(x) ? x.toString() : 'unknown'}` + : hasToString(x) ? x.toString() : 'unknown' + ).join(', ')}]` : 'unknown[]' + } + return stringToString +} diff --git a/packages/schema-generator/test/test-data/tuple/validate.ts b/packages/schema-generator/test/test-data/tuple/validate.ts new file mode 100644 index 00000000..d84e3650 --- /dev/null +++ b/packages/schema-generator/test/test-data/tuple/validate.ts @@ -0,0 +1,34 @@ +import { URI, Array_isArray } from '@traversable/registry' +import { t } from '@traversable/schema-core' +import type { ValidationError, Validate, Validator } from '@traversable/derive-validators' +import { Errors } from '@traversable/derive-validators' + +export type validate = Validate +export function validate(tupleSchema: t.tuple<[...S]>): validate +export function validate(tupleSchema: t.tuple<[...S]>): validate +export function validate(tupleSchema: t.tuple<[...S]>): Validate { + validateTuple.tag = URI.tuple + function validateTuple(u: unknown, path = Array.of()) { + let errors = Array.of() + if (!Array_isArray(u)) return [Errors.array(u, path)] + for (let i = 0; i < tupleSchema.def.length; i++) { + if (!(i in u) && !(t.optional.is(tupleSchema.def[i].validate))) { + errors.push(Errors.missingIndex(u, [...path, i])) + continue + } + let results = tupleSchema.def[i].validate(u[i], [...path, i]) + if (results !== true) { + for (let j = 0; j < results.length; j++) errors.push(results[j]) + results.push(Errors.arrayElement(u[i], [...path, i])) + } + } + if (u.length > tupleSchema.def.length) { + for (let k = tupleSchema.def.length; k < u.length; k++) { + let excess = u[k] + errors.push(Errors.excessItems(excess, [...path, k])) + } + } + return errors.length === 0 || errors + } + return validateTuple +} diff --git a/packages/schema-generator/test/test-data/undefined/core.ts b/packages/schema-generator/test/test-data/undefined/core.ts new file mode 100644 index 00000000..fb8b60bd --- /dev/null +++ b/packages/schema-generator/test/test-data/undefined/core.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { undefined_ as undefined } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface undefined_ extends undefined_.core { + //<%= Types %> +} + +function UndefinedSchema(src: unknown): src is undefined { return src === void 0 } +UndefinedSchema.tag = URI.undefined +UndefinedSchema.def = void 0 as undefined + +const undefined_ = Object_assign( + UndefinedSchema, + userDefinitions, +) as undefined_ + +Object_assign(undefined_, userExtensions) + +declare namespace undefined_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.undefined + _type: undefined + def: this['_type'] + } +} diff --git a/packages/schema-generator/test/test-data/undefined/equals.ts b/packages/schema-generator/test/test-data/undefined/equals.ts new file mode 100644 index 00000000..2836cd51 --- /dev/null +++ b/packages/schema-generator/test/test-data/undefined/equals.ts @@ -0,0 +1,7 @@ +import type { Equal } from "@traversable/registry" +import { Object_is } from "@traversable/registry" + +export type equals = Equal +export function equals(left: undefined, right: undefined): boolean { + return Object_is(left, right) +} diff --git a/packages/schema-generator/test/test-data/undefined/extension.ts b/packages/schema-generator/test/test-data/undefined/extension.ts new file mode 100644 index 00000000..c4ecbb88 --- /dev/null +++ b/packages/schema-generator/test/test-data/undefined/extension.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/test/test-data/undefined/toJsonSchema.ts b/packages/schema-generator/test/test-data/undefined/toJsonSchema.ts new file mode 100644 index 00000000..be46c306 --- /dev/null +++ b/packages/schema-generator/test/test-data/undefined/toJsonSchema.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function undefinedToJsonSchema(): void { return void 0 } + return undefinedToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/undefined/toString.ts b/packages/schema-generator/test/test-data/undefined/toString.ts new file mode 100644 index 00000000..a48b744b --- /dev/null +++ b/packages/schema-generator/test/test-data/undefined/toString.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'undefined' } +export function toString(): 'undefined' { return 'undefined' } diff --git a/packages/schema-generator/test/test-data/undefined/validate.ts b/packages/schema-generator/test/test-data/undefined/validate.ts new file mode 100644 index 00000000..d69c9d9e --- /dev/null +++ b/packages/schema-generator/test/test-data/undefined/validate.ts @@ -0,0 +1,13 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(undefinedSchema: t.undefined): validate { + validateUndefined.tag = URI.undefined + function validateUndefined(u: unknown, path = Array.of()) { + return undefinedSchema(u) || [NullaryErrors.undefined(u, path)] + } + return validateUndefined +} diff --git a/packages/schema-generator/test/test-data/union/core.ts b/packages/schema-generator/test/test-data/union/core.ts new file mode 100644 index 00000000..33698c38 --- /dev/null +++ b/packages/schema-generator/test/test-data/union/core.ts @@ -0,0 +1,50 @@ +import type { Unknown } from '@traversable/registry' +import { + _isPredicate, + bindUserExtensions, + isUnknown as isAny, + Object_assign, + union as union$, + URI, +} from '@traversable/registry' + +import type { Entry, Schema, SchemaLike } from '@traversable/schema-core/namespace' + +export function union(...schemas: S): union +export function union }>(...schemas: S): union +export function union(...schemas: unknown[]) { + return union.def(schemas) +} + +export interface union extends union.core { + //<%= Types %> +} + +export namespace union { + export let userDefinitions: Record = { + //<%= Definitions %> + } as Partial> + export function def(xs: T): union + /* v8 ignore next 1 */ + export function def(xs: unknown[]) { + let userExtensions: Record = { + //<%= Extensions %> + } + const predicate = xs.every(_isPredicate) ? union$(xs) : isAny + function UnionSchema(src: unknown): src is unknown { return predicate(src) } + UnionSchema.tag = URI.union + UnionSchema.def = xs + Object_assign(UnionSchema, union.userDefinitions) + return Object_assign(UnionSchema, bindUserExtensions(UnionSchema, userExtensions)) + } +} + +export declare namespace union { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.union + _type: union.type + get def(): S + } + type type = never | T +} diff --git a/packages/schema-generator/test/test-data/union/equals.ts b/packages/schema-generator/test/test-data/union/equals.ts new file mode 100644 index 00000000..c0275e69 --- /dev/null +++ b/packages/schema-generator/test/test-data/union/equals.ts @@ -0,0 +1,16 @@ +import type { Equal } from '@traversable/registry' +import { Object_is } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +export type equals = Equal +export function equals(unionSchema: t.union<[...S]>): equals +export function equals(unionSchema: t.union<[...S]>): equals +export function equals({ def }: t.union<{ equals: Equal }[]>): Equal { + function unionEquals(l: unknown, r: unknown): boolean { + if (Object_is(l, r)) return true + for (let ix = def.length; ix-- !== 0;) + if (def[ix].equals(l, r)) return true + return false + } + return unionEquals +} diff --git a/packages/schema-generator/test/test-data/union/extension.ts b/packages/schema-generator/test/test-data/union/extension.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema-generator/test/test-data/union/extension.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema-generator/test/test-data/union/toJsonSchema.ts b/packages/schema-generator/test/test-data/union/toJsonSchema.ts new file mode 100644 index 00000000..850f9f66 --- /dev/null +++ b/packages/schema-generator/test/test-data/union/toJsonSchema.ts @@ -0,0 +1,17 @@ +import type { Returns } from '@traversable/registry' +import { t } from '@traversable/schema-core' +import { getSchema } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { + (): { anyOf: { [I in keyof T]: Returns } } +} + +export function toJsonSchema(unionSchema: t.union): toJsonSchema +export function toJsonSchema(unionSchema: t.union): toJsonSchema +export function toJsonSchema({ def }: t.union): () => {} { + return function unionToJsonSchema() { + return { + anyOf: def.map(getSchema) + } + } +} diff --git a/packages/schema-generator/test/test-data/union/toString.ts b/packages/schema-generator/test/test-data/union/toString.ts new file mode 100644 index 00000000..6429d3e1 --- /dev/null +++ b/packages/schema-generator/test/test-data/union/toString.ts @@ -0,0 +1,18 @@ +import type { Join } from '@traversable/registry' +import { Array_isArray } from '@traversable/registry' +import { t } from '@traversable/schema-core' +import { callToString } from '@traversable/schema-to-string' + +export interface toString { + (): never | [T] extends [readonly []] ? 'never' + /* @ts-expect-error */ + : `(${Join<{ [I in keyof T]: ReturnType }, ' | '>})` +} + +export function toString(unionSchema: t.union): toString +export function toString({ def }: t.union): () => string { + function unionToString() { + return Array_isArray(def) ? def.length === 0 ? 'never' : `(${def.map(callToString).join(' | ')})` : 'unknown' + } + return unionToString +} diff --git a/packages/schema-generator/test/test-data/union/validate.ts b/packages/schema-generator/test/test-data/union/validate.ts new file mode 100644 index 00000000..ba69fccf --- /dev/null +++ b/packages/schema-generator/test/test-data/union/validate.ts @@ -0,0 +1,26 @@ +import { URI } from '@traversable/registry' +import { t } from '@traversable/schema-core' +import type { ValidationError, Validate, Validator } from '@traversable/derive-validators' + +export type validate = Validate + +export function validate(unionSchema: t.union): validate +export function validate(unionSchema: t.union): validate +export function validate({ def }: t.union) { + validateUnion.tag = URI.union + function validateUnion(u: unknown, path = Array.of()): true | ValidationError[] { + // if (this.def.every((x) => t.optional.is(x.validate))) validateUnion.optional = 1; + let errors = Array.of() + for (let i = 0; i < def.length; i++) { + let results = def[i].validate(u, path) + if (results === true) { + // validateUnion.optional = 0 + return true + } + for (let j = 0; j < results.length; j++) errors.push(results[j]) + } + // validateUnion.optional = 0 + return errors.length === 0 || errors + } + return validateUnion +} diff --git a/packages/schema-generator/test/test-data/unknown/core.ts b/packages/schema-generator/test/test-data/unknown/core.ts new file mode 100644 index 00000000..7c70ea24 --- /dev/null +++ b/packages/schema-generator/test/test-data/unknown/core.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { unknown_ as unknown } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface unknown_ extends unknown_.core { + //<%= Types %> +} + +function UnknownSchema(src: unknown): src is unknown { return src === void 0 } +UnknownSchema.tag = URI.unknown +UnknownSchema.def = void 0 as unknown + +const unknown_ = Object_assign( + UnknownSchema, + userDefinitions, +) as unknown_ + +Object_assign(unknown_, userExtensions) + +declare namespace unknown_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.unknown + _type: unknown + def: this['_type'] + } +} diff --git a/packages/schema-generator/test/test-data/unknown/equals.ts b/packages/schema-generator/test/test-data/unknown/equals.ts new file mode 100644 index 00000000..41b30fd5 --- /dev/null +++ b/packages/schema-generator/test/test-data/unknown/equals.ts @@ -0,0 +1,7 @@ +import type { Equal } from "@traversable/registry" +import { Object_is } from "@traversable/registry" + +export type equals = Equal +export function equals(left: any, right: any): boolean { + return Object_is(left, right) +} diff --git a/packages/schema-generator/test/test-data/unknown/extension.ts b/packages/schema-generator/test/test-data/unknown/extension.ts new file mode 100644 index 00000000..c4ecbb88 --- /dev/null +++ b/packages/schema-generator/test/test-data/unknown/extension.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/test/test-data/unknown/toJsonSchema.ts b/packages/schema-generator/test/test-data/unknown/toJsonSchema.ts new file mode 100644 index 00000000..8d5be5a0 --- /dev/null +++ b/packages/schema-generator/test/test-data/unknown/toJsonSchema.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): { type: 'object', properties: {}, nullable: true } } +export function toJsonSchema(): toJsonSchema { + function anyToJsonSchema() { return { type: 'object', properties: {}, nullable: true } as const } + return anyToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/unknown/toString.ts b/packages/schema-generator/test/test-data/unknown/toString.ts new file mode 100644 index 00000000..417e1048 --- /dev/null +++ b/packages/schema-generator/test/test-data/unknown/toString.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'unknown' } +export function toString(): 'unknown' { return 'unknown' } diff --git a/packages/schema-generator/test/test-data/unknown/validate.ts b/packages/schema-generator/test/test-data/unknown/validate.ts new file mode 100644 index 00000000..d286c4b5 --- /dev/null +++ b/packages/schema-generator/test/test-data/unknown/validate.ts @@ -0,0 +1,10 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(_?: t.any): validate { + validateAny.tag = URI.any + function validateAny() { return true as const } + return validateAny +} diff --git a/packages/schema-generator/test/test-data/void/core.ts b/packages/schema-generator/test/test-data/void/core.ts new file mode 100644 index 00000000..11fce4ab --- /dev/null +++ b/packages/schema-generator/test/test-data/void/core.ts @@ -0,0 +1,36 @@ +import type { Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' + +export { void_ as void, void_ } + +export let userDefinitions: Record = { + //<%= Definitions %> +} + +export let userExtensions: Record = { + //<%= Extensions %> +} + +interface void_ extends void_.core { + //<%= Types %> +} + +function VoidSchema(src: unknown): src is void { return src === void 0 } +VoidSchema.tag = URI.void +VoidSchema.def = void 0 as void + +const void_ = Object_assign( + VoidSchema, + userDefinitions, +) as void_ + +Object_assign(void_, userExtensions) + +declare namespace void_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.void + _type: void + def: this['_type'] + } +} diff --git a/packages/schema-generator/test/test-data/void/equals.ts b/packages/schema-generator/test/test-data/void/equals.ts new file mode 100644 index 00000000..6f7b9779 --- /dev/null +++ b/packages/schema-generator/test/test-data/void/equals.ts @@ -0,0 +1,7 @@ +import type { Equal } from "@traversable/registry" +import { Object_is } from "@traversable/registry" + +export type equals = Equal +export function equals(left: void, right: void): boolean { + return Object_is(left, right) +} diff --git a/packages/schema-generator/test/test-data/void/extension.ts b/packages/schema-generator/test/test-data/void/extension.ts new file mode 100644 index 00000000..c4ecbb88 --- /dev/null +++ b/packages/schema-generator/test/test-data/void/extension.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema-generator/test/test-data/void/toJsonSchema.ts b/packages/schema-generator/test/test-data/void/toJsonSchema.ts new file mode 100644 index 00000000..d636b569 --- /dev/null +++ b/packages/schema-generator/test/test-data/void/toJsonSchema.ts @@ -0,0 +1,7 @@ +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function voidToJsonSchema(): void { + return void 0 + } + return voidToJsonSchema +} diff --git a/packages/schema-generator/test/test-data/void/toString.ts b/packages/schema-generator/test/test-data/void/toString.ts new file mode 100644 index 00000000..487d08b3 --- /dev/null +++ b/packages/schema-generator/test/test-data/void/toString.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'void' } +export function toString(): 'void' { return 'void' } diff --git a/packages/schema-generator/test/test-data/void/validate.ts b/packages/schema-generator/test/test-data/void/validate.ts new file mode 100644 index 00000000..a67fc4e4 --- /dev/null +++ b/packages/schema-generator/test/test-data/void/validate.ts @@ -0,0 +1,13 @@ +import type { t } from '@traversable/schema-core' +import { URI } from '@traversable/registry' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + +export type validate = ValidationFn +export function validate(voidSchema: t.void): validate { + validateVoid.tag = URI.void + function validateVoid(u: unknown, path = Array.of()) { + return voidSchema(u) || [NullaryErrors.void(u, path)] + } + return validateVoid +} diff --git a/packages/schema-generator/test/version.test.ts b/packages/schema-generator/test/version.test.ts new file mode 100644 index 00000000..7ad20cb6 --- /dev/null +++ b/packages/schema-generator/test/version.test.ts @@ -0,0 +1,10 @@ +import * as vi from 'vitest' +import pkg from '../package.json' with { type: 'json' } +import { VERSION } from '@traversable/schema-generator' + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-generator❳', () => { + vi.it('〖⛳️〗› ❲schemaGenerator#VERSION❳', () => { + const expected = `${pkg.name}@${pkg.version}` + vi.assert.equal(VERSION, expected) + }) +}) \ No newline at end of file diff --git a/packages/schema-generator/tsconfig.build.json b/packages/schema-generator/tsconfig.build.json new file mode 100644 index 00000000..e7875781 --- /dev/null +++ b/packages/schema-generator/tsconfig.build.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "types": ["node"], + "declarationDir": "build/dts", + "outDir": "build/esm", + "stripInternal": true + }, + "references": [ + { "path": "../derive-validators" }, + { "path": "../derive-equals" }, + { "path": "../registry" }, + { "path": "../schema-core" }, + { "path": "../schema-to-json-schema" }, + { "path": "../schema-to-string" } + ] +} \ No newline at end of file diff --git a/packages/schema-generator/tsconfig.json b/packages/schema-generator/tsconfig.json new file mode 100644 index 00000000..2c291d21 --- /dev/null +++ b/packages/schema-generator/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/packages/schema-generator/tsconfig.src.json b/packages/schema-generator/tsconfig.src.json new file mode 100644 index 00000000..384d2be2 --- /dev/null +++ b/packages/schema-generator/tsconfig.src.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "types": ["node"], + "outDir": "build/src" + }, + "references": [ + { "path": "../derive-validators" }, + { "path": "../derive-equals" }, + { "path": "../registry" }, + { "path": "../schema-core" }, + { "path": "../schema-to-json-schema" }, + { "path": "../schema-to-string" } + ], + "include": ["src"] +} diff --git a/packages/schema-generator/tsconfig.test.json b/packages/schema-generator/tsconfig.test.json new file mode 100644 index 00000000..21695d87 --- /dev/null +++ b/packages/schema-generator/tsconfig.test.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "types": ["node"], + "noEmit": true + }, + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../derive-validators" }, + { "path": "../derive-equals" }, + { "path": "../registry" }, + { "path": "../schema-core" }, + { "path": "../schema-to-json-schema" }, + { "path": "../schema-to-string" } + ], + "include": ["test"] +} diff --git a/packages/schema-generator/vite.config.ts b/packages/schema-generator/vite.config.ts new file mode 100644 index 00000000..64dba4ad --- /dev/null +++ b/packages/schema-generator/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import sharedConfig from '../../vite.config.js' + +const localConfig = defineConfig({}) + +export default mergeConfig(sharedConfig, localConfig) \ No newline at end of file diff --git a/packages/schema-jit-compiler/README.md b/packages/schema-jit-compiler/README.md new file mode 100644 index 00000000..4e10e27d --- /dev/null +++ b/packages/schema-jit-compiler/README.md @@ -0,0 +1,64 @@ +
+

ᯓ𝘁𝗿𝗮𝘃𝗲𝗿𝘀𝗮𝗯𝗹𝗲/𝘀𝗰𝗵𝗲𝗺𝗮-𝗷𝗶𝘁-𝗰𝗼𝗺𝗽𝗶𝗹𝗲𝗿

+
+ +

+ This package contains the code for installing "JIT compiled" validators to your schemas. +

+ +
+ NPM Version +   + TypeScript +   + Static Badge +   + npm +   +
+ +
+ npm bundle size (scoped) +   + Static Badge +   + Static Badge +   +
+ +
+ Demo (StackBlitz) +   •   + TypeScript Playground +   •   + npm +
+
+
+
+ +## Getting started + +Users can consume this package in one of several ways: + +### Import side effect + module augmentation + +To install the `.compile` method on all schemas, simply import `@traversable/schema-jit-compiler/install`. + +Once you do, all schemas come with a `.compile` method you can use. + +### As a standalone function + +To compile a single schema, import `Jit` from `@traversable/schema-jit-compiler/recursive`, and pass the +schema you'd like to compile to `Jit.fromSchema`. + +### As a transitive dependency + +Schemas created using `@traversable/schema` include the `.compile` method out of the box, so if you've +installed that package, you don't need to do anything extra to take advantage of this feature. + +If you don't need this feature and would prefer not to have it installed, you can either: + +1. use a lower-level package like `@traversable/schema-core`, which does not install the `.compile` method + +2. configure `@traversable/schema` to not include it when building your schemas diff --git a/packages/schema-jit-compiler/package.json b/packages/schema-jit-compiler/package.json new file mode 100644 index 00000000..0fd2ab8d --- /dev/null +++ b/packages/schema-jit-compiler/package.json @@ -0,0 +1,52 @@ +{ + "name": "@traversable/schema-jit-compiler", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/schema-jit-compiler" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { "include": ["**/*.ts"] }, + "generateIndex": { "include": ["**/*.ts"] } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "bench": "echo NOTHING TO BENCH", + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "test": "vitest" + }, + "peerDependencies": { + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^" + }, + "peerDependenciesMeta": { + "@traversable/registry": { "optional": false }, + "@traversable/schema-core": { "optional": false } + }, + "devDependencies": { + "@traversable/registry": "workspace:^", + "@traversable/schema-arbitrary": "workspace:^", + "@traversable/schema-core": "workspace:^", + "@traversable/schema-seed": "workspace:^" + } +} diff --git a/packages/schema-jit-compiler/src/__generated__/__manifest__.ts b/packages/schema-jit-compiler/src/__generated__/__manifest__.ts new file mode 100644 index 00000000..8cc538c4 --- /dev/null +++ b/packages/schema-jit-compiler/src/__generated__/__manifest__.ts @@ -0,0 +1,52 @@ +export default { + "name": "@traversable/schema-jit-compiler", + "type": "module", + "version": "0.0.0", + "private": false, + "description": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/traversable/schema.git", + "directory": "packages/schema-jit-compiler" + }, + "bugs": { + "url": "https://github.com/traversable/schema/issues", + "email": "ahrjarrett@gmail.com" + }, + "@traversable": { + "generateExports": { "include": ["**/*.ts"] }, + "generateIndex": { "include": ["**/*.ts"] } + }, + "publishConfig": { + "access": "public", + "directory": "dist", + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "bench": "echo NOTHING TO BENCH", + "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "build:esm": "tsc -b tsconfig.build.json", + "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "check": "tsc -b tsconfig.json", + "clean": "pnpm run \"/^clean:.*/\"", + "clean:build": "rm -rf .tsbuildinfo dist build", + "clean:deps": "rm -rf node_modules", + "test": "vitest" + }, + "peerDependencies": { + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^" + }, + "peerDependenciesMeta": { + "@traversable/registry": { "optional": false }, + "@traversable/schema-core": { "optional": false } + }, + "devDependencies": { + "@traversable/registry": "workspace:^", + "@traversable/schema-arbitrary": "workspace:^", + "@traversable/schema-core": "workspace:^", + "@traversable/schema-seed": "workspace:^" + } +} as const \ No newline at end of file diff --git a/packages/schema-jit-compiler/src/exports.ts b/packages/schema-jit-compiler/src/exports.ts new file mode 100644 index 00000000..aefa61fa --- /dev/null +++ b/packages/schema-jit-compiler/src/exports.ts @@ -0,0 +1,13 @@ +export { VERSION } from './version.js' +export { + generate, + compile, + WeightByTypeName, +} from './jit.js' +export * as Json from './json.js' +export type { Index } from './functor.js' +export { defaultIndex } from './functor.js' +export { + indexAccessor, + keyAccessor, +} from './shared.js' diff --git a/packages/schema-jit-compiler/src/functor.ts b/packages/schema-jit-compiler/src/functor.ts new file mode 100644 index 00000000..37eec719 --- /dev/null +++ b/packages/schema-jit-compiler/src/functor.ts @@ -0,0 +1,147 @@ +import type * as T from '@traversable/registry' +import { fn, symbol, URI } from '@traversable/registry' +import { t } from '@traversable/schema-core' + +import { indexAccessor, keyAccessor } from './shared.js' + +export type Index = { + siblingCount: number + offset: number + dataPath: (string | number)[] + isOptional: boolean + isRoot: boolean + schemaPath: (keyof any)[] + varName: string +} + +export interface Free extends T.HKT { [-1]: IR } +export type Algebra = T.IndexedAlgebra + +export type IR = + | t.Leaf + | t.eq + | t.array + | t.record + | t.optional + | t.union + | t.intersect + | t.tuple + | t.object<[k: string, T][]> + +export let defaultIndex: Index = { + siblingCount: 0, + offset: 2, + dataPath: [], + schemaPath: [], + varName: 'value', + isRoot: true, + isOptional: false, +} + + +let map: T.Functor['map'] = (f) => (x) => { + switch (true) { + default: return fn.exhaustive(x) + case t.isNullary(x): return x + case t.isBoundable(x): return x + case x.tag === URI.eq: return t.eq.def(x.def as never) + case x.tag === URI.optional: return t.optional.def(f(x.def)) + case x.tag === URI.array: return t.array.def(f(x.def)) + case x.tag === URI.record: return t.record.def(f(x.def)) + case x.tag === URI.union: return t.union.def(fn.map(x.def, f)) + case x.tag === URI.intersect: return t.intersect.def(fn.map(x.def, f)) + case x.tag === URI.tuple: return t.tuple.def(fn.map(x.def, f)) + case x.tag === URI.object: return t.object.def( + fn.map(x.def, ([k, v]) => [k, f(v)] satisfies [any, any]), + undefined, + x.opt, + ) + } +} + +let mapWithIndex: T.Functor.Ix['mapWithIndex'] = (f) => (xs, ix) => { + switch (true) { + default: return fn.exhaustive(xs) + case t.isNullary(xs): return xs + case t.isBoundable(xs): return xs + case xs.tag === URI.eq: return xs as never + case xs.tag === URI.optional: return t.optional.def(f(xs.def, { + dataPath: ix.dataPath, + isOptional: true, + isRoot: false, + offset: ix.offset + 2, + schemaPath: [...ix.schemaPath, symbol.optional], + siblingCount: 0, + varName: ix.varName, + })) + case xs.tag === URI.array: return t.array.def(f(xs.def, { + dataPath: ix.dataPath, + isOptional: ix.isOptional, + isRoot: false, + offset: ix.offset + 2, + schemaPath: [...ix.schemaPath, symbol.array], + siblingCount: 0, + varName: 'value', + })) + case xs.tag === URI.record: return t.record.def(f(xs.def, { + dataPath: ix.dataPath, + isOptional: ix.isOptional, + isRoot: false, + offset: ix.offset + 2, + schemaPath: [...ix.schemaPath, symbol.array], + siblingCount: 0, + varName: 'value', + })) + case xs.tag === URI.union: return t.union.def(fn.map(xs.def, (x, i) => f(x, { + dataPath: ix.dataPath, + isOptional: ix.isOptional, + isRoot: false, + offset: ix.offset + 2, + schemaPath: [...ix.schemaPath, i], + siblingCount: Math.max(xs.def.length - 1, 0), + varName: ix.varName, + }))) + case xs.tag === URI.intersect: return t.intersect.def(fn.map(xs.def, (x, i) => f(x, { + dataPath: ix.dataPath, + isOptional: ix.isOptional, + isRoot: false, + offset: ix.offset + 2, + schemaPath: [...ix.schemaPath, i], + siblingCount: Math.max(xs.def.length - 1, 0), + varName: ix.varName, + }))) + case xs.tag === URI.tuple: + return t.tuple.def(fn.map(xs.def, (x, i) => f(x, { + dataPath: [...ix.dataPath, i], + isOptional: ix.isOptional, + isRoot: false, + offset: ix.offset + 2, + schemaPath: [...ix.schemaPath, i], + siblingCount: Math.max(xs.def.length - 1, 0), + /** + * Passing `x` to `indexAccessor` is a hack to make sure + * we preserve the original order of the tuple while we're + * applying a sorting optimization + */ + varName: ix.varName + indexAccessor(i, ix, x), + }))) + case xs.tag === URI.object: { + return t.object.def( + fn.map(xs.def, ([k, v]) => [k, f(v, { + dataPath: [...ix.dataPath, k], + isOptional: ix.isOptional, + isRoot: false, + offset: ix.offset + 2, + schemaPath: [...ix.schemaPath, k], + siblingCount: Math.max(Object.keys(xs.def).length - 1, 0), + varName: ix.varName + keyAccessor(k, ix), + })] satisfies [any, any]), + undefined, + xs.opt, + ) + } + } +} + +export let Functor: T.Functor.Ix = { map, mapWithIndex } +export let fold = (algebra: Algebra) => (x: IR) => fn.cataIx(Functor)(algebra)(x, defaultIndex) diff --git a/packages/schema-jit-compiler/src/index.ts b/packages/schema-jit-compiler/src/index.ts new file mode 100644 index 00000000..96cd0f4f --- /dev/null +++ b/packages/schema-jit-compiler/src/index.ts @@ -0,0 +1,2 @@ +export * from './exports.js' +export * as Jit from './exports.js' diff --git a/packages/schema-jit-compiler/src/jit.ts b/packages/schema-jit-compiler/src/jit.ts new file mode 100644 index 00000000..927aaa14 --- /dev/null +++ b/packages/schema-jit-compiler/src/jit.ts @@ -0,0 +1,362 @@ +import type * as T from '@traversable/registry' +import { fn, getConfig, parseKey, typeName, URI } from '@traversable/registry' +import { t } from '@traversable/schema-core' + +import type { Algebra, IR } from './functor.js' +import { fold } from './functor.js' +import * as Json from './json.js' +import { buildContext } from './shared.js' + +export let MAX_WIDTH = 120 + +export let WeightByTypeName = { + never: 0, + any: 10, + unknown: 20, + void: 30, + undefined: 40, + null: 50, + symbol: 60, + boolean: 70, + integer: 80, + bigint: 90, + number: 100, + string: 110, + optional: 120, + intersect: 130, + union: 140, + tuple: 150, + object: 160, + array: 170, + record: 180, + eq: 190, +} as const + +export let interpreter: Algebra = (x, ix) => { + let ctx = buildContext(ix) + let { VAR, indent } = ctx + let { schema: $ } = getConfig() + let NON_ARRAY_CHECK = $.treatArraysAsObjects ? '' : ` && !Array.isArray(${VAR})` + let IS_EXACT_OPTIONAL = $.optionalTreatment === 'exactOptional' + switch (true) { + default: return fn.exhaustive(x) + case x.tag === URI.never: return 'false' + case x.tag === URI.any: return 'true' + case x.tag === URI.unknown: return 'true' + case x.tag === URI.void: return `${VAR} === void 0` + case x.tag === URI.null: return `${VAR} === null` + case x.tag === URI.undefined: return `${VAR} === undefined` + case x.tag === URI.symbol: return `typeof ${VAR} === "symbol"` + case x.tag === URI.boolean: return `typeof ${VAR} === "boolean"` + case x.tag === URI.integer: { + let CHECK = `Number.isSafeInteger(${VAR})` + let MIN_CHECK = t.number(x.minimum) ? ` && ${x.minimum} <= ${VAR}` : '' + let MAX_CHECK = t.number(x.maximum) ? ` && ${VAR} <= ${x.maximum}` : '' + let OPEN = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? '(' : '' + let CLOSE = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? ')' : '' + return '' + + OPEN + + CHECK + + MIN_CHECK + + MAX_CHECK + + CLOSE + } + + case x.tag === URI.bigint: { + let CHECK = `typeof ${VAR} === "bigint"` + let MIN_CHECK = t.bigint(x.minimum) ? ` && ${x.minimum}n <= ${VAR}` : '' + let MAX_CHECK = t.bigint(x.maximum) ? ` && ${VAR} <= ${x.maximum}n` : '' + let OPEN = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? '(' : '' + let CLOSE = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? ')' : '' + return '' + + OPEN + + CHECK + + MIN_CHECK + + MAX_CHECK + + CLOSE + } + + case x.tag === URI.number: { + let CHECK = `Number.isFinite(${VAR})` + let MIN_CHECK = t.number(x.exclusiveMinimum) + ? ` && ${x.exclusiveMinimum} < ${VAR}` + : t.number(x.minimum) ? ` && ${x.minimum} <= ${VAR}` : '' + let MAX_CHECK = t.number(x.exclusiveMaximum) + ? ` && ${VAR} < ${x.exclusiveMaximum}` + : t.number(x.maximum) ? ` && ${VAR} <= ${x.maximum}` : '' + let OPEN = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? '(' : '' + let CLOSE = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? ')' : '' + return '' + + OPEN + + CHECK + + MIN_CHECK + + MAX_CHECK + + CLOSE + } + + case x.tag === URI.string: { + let CHECK = `typeof ${VAR} === "string"` + let MIN_CHECK = t.number(x.minLength) ? ` && ${x.minLength} <= ${VAR}.length` : '' + let MAX_CHECK = t.number(x.maxLength) ? ` && ${VAR}.length <= ${x.maxLength}` : '' + let OPEN = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? '(' : '' + let CLOSE = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? ')' : '' + return '' + + OPEN + + CHECK + + MIN_CHECK + + MAX_CHECK + + CLOSE + } + + case x.tag === URI.eq: { + return Json.generate( + x.def, { + ...ix, + varName: VAR, + offset: ix.offset + 2, + }) + } + + case x.tag === URI.optional: { + if (IS_EXACT_OPTIONAL) return x.def + else { + let CHECK = `${VAR} === undefined` + let WIDTH = ix.offset + CHECK.length + ' || '.length + x.def.length + let SINGLE_LINE = WIDTH < MAX_WIDTH + let OPEN = SINGLE_LINE ? '(' : ('(' + indent(2)) + let CLOSE = SINGLE_LINE ? ')' : (indent(0) + ')') + let BODY = SINGLE_LINE ? (CHECK + ' || ' + x.def) : (CHECK + indent(2) + '|| ' + x.def) + return '' + + OPEN + + BODY + + CLOSE + } + } + + case x.tag === URI.array: { + let MIN_CHECK = t.number(x.minLength) ? `&& ${x.minLength} < ${VAR}.length` : '' + let MAX_CHECK = t.number(x.maxLength) ? `&& ${VAR}.length < ${x.maxLength}` : '' + let OUTER_CHECK = `Array.isArray(${VAR})${MIN_CHECK}${MAX_CHECK} && ` + let INNER_CHECK = `${VAR}.every((value) => ` + let WIDTH = ix.offset + OUTER_CHECK.length + INNER_CHECK.length + x.def.length + let SINGLE_LINE = WIDTH < MAX_WIDTH + let OPEN = SINGLE_LINE ? '' : indent(4) + let CLOSE = SINGLE_LINE ? ')' : (indent(2) + ')') + return '' + + OUTER_CHECK + + INNER_CHECK + + OPEN + + x.def + + CLOSE + } + + case x.tag === URI.record: { + let OUTER_CHECK = '' + + `!!${VAR} && typeof ${VAR} === "object"${NON_ARRAY_CHECK} ${indent(2)}&& ` + + `!(${VAR} instanceof Date) && !(${VAR} instanceof Uint8Array) ${indent(2)}&& ` + let INNER_CHECK = `Object.entries(${VAR}).every(([key, value]) => ` + let KEY_CHECK = 'typeof key === "string" && ' + let WIDTH = ix.offset + OUTER_CHECK.length + INNER_CHECK.length + x.def.length + let SINGLE_LINE = WIDTH < MAX_WIDTH + let OPEN = SINGLE_LINE ? KEY_CHECK : (indent(4) + KEY_CHECK) + let CLOSE = SINGLE_LINE ? ')' : (indent(2) + ')') + return '' + + OUTER_CHECK + + INNER_CHECK + + OPEN + + x.def + + CLOSE + } + + case x.tag === URI.union: { + let CHILD_COUNT = x.def.length + let WIDTH = ix.offset + x.def.join(' || ').length + let SINGLE_LINE = WIDTH < MAX_WIDTH + let OPEN = SINGLE_LINE || ix.isRoot ? '(' : ('(' + indent(2)) + let CLOSE = SINGLE_LINE || ix.isRoot ? ')' : (indent(0) + ')') + let BODY = CHILD_COUNT === 0 ? 'false' + : SINGLE_LINE ? x.def.map((v) => '(' + v + ')').join(' || ') + : x.def.map((v) => '(' + v + ')').join(indent(2) + '|| ') + return '' + + OPEN + + BODY + + CLOSE + } + + case x.tag === URI.intersect: { + let CHILD_COUNT = x.def.length + let WIDTH = ix.offset + x.def.join(' || ').length + let SINGLE_LINE = WIDTH < MAX_WIDTH + let OPEN = SINGLE_LINE || ix.isRoot ? '(' : ('(' + indent(2)) + let CLOSE = SINGLE_LINE || ix.isRoot ? ')' : (indent(0) + ')') + let BODY = CHILD_COUNT === 0 ? 'true' + : SINGLE_LINE ? x.def.join(' && ') + : x.def.join(indent(2) + '&& ') + return '' + + OPEN + + BODY + + CLOSE + } + + case x.tag === URI.tuple: { + let CHILD_COUNT = x.def.length + let CHECK = `Array.isArray(${VAR}) && ${VAR}.length === ${CHILD_COUNT}` + let WIDTH = ix.offset + CHECK.length + x.def.join(' && ').length + let SINGLE_LINE = WIDTH < MAX_WIDTH + let JOIN = SINGLE_LINE ? '' : indent(2) + let BODY = CHILD_COUNT === 0 ? '' : x.def.map((v) => JOIN + (SINGLE_LINE ? ' && ' : '&& ') + v).join('') + return CHECK + BODY + } + + case x.tag === URI.object: { + let CHILD_COUNT = x.def.length + let CHECK = `!!${VAR} && typeof ${VAR} === "object"${NON_ARRAY_CHECK}` + let OPTIONAL_KEYS = Array.of().concat(x.opt) + let CHILDREN = x.def.map( + ([k, v]) => IS_EXACT_OPTIONAL && OPTIONAL_KEYS.includes(k) + ? `(!Object.hasOwn(${VAR}, "${parseKey(k)}") || ${v})` + : v + ) + let WIDTH = ix.offset + CHECK.length + CHILDREN.join(' && ').length + let SINGLE_LINE = WIDTH < MAX_WIDTH + let JOIN = SINGLE_LINE ? '' : indent(2) + let BODY = CHILD_COUNT === 0 ? '' : CHILDREN.map((v) => JOIN + (SINGLE_LINE ? ' && ' : '&& ') + v).join('') + return CHECK + BODY + } + } +} + +let weightComparator: T.Comparator = (l, r) => { + let lw = getWeight(l) + let rw = getWeight(r) + return lw < rw ? -1 : rw < lw ? +1 : 0 +} + +let aggregateWeights + : (acc: number, curr: t.Schema) => number + = (acc, curr) => Math.max(acc, getWeight(curr)) + +function getWeight(x: IR): number +function getWeight(x: t.Schema): number +function getWeight(x: IR): number { + let w = WeightByTypeName[typeName(x)] + switch (true) { + default: return fn.exhaustive(x) + case t.isNullary(x): return w + case t.isBoundable(x): return w + case x.tag === URI.eq: return w + case x.tag === URI.optional: return w + getWeight(x.def) + case x.tag === URI.array: return w + getWeight(x.def) + case x.tag === URI.record: return w + getWeight(x.def) + case x.tag === URI.union: return w + x.def.reduce(aggregateWeights, 0) + case x.tag === URI.intersect: return w + x.def.reduce(aggregateWeights, 0) + case x.tag === URI.tuple: return w + x.def.reduce(aggregateWeights, 0) + case x.tag === URI.object: return w + x.def.map(([, v]) => v).reduce(aggregateWeights, 0) + } +} + +/** + * Binding the element's index to the element itself is a hack to make sure + * we preserve the original order of the tuple, even while sorting + */ +let bindPreSortIndices: (x: T[]) => T[] = (x) => { + for (let ix = 0, len = x.length; ix < len; ix++) + (x[ix] as any).preSortIndex = ix + return x +} + +export let sort: (schema: t.Schema) => IR = fn.flow( + t.fold((x) => + x.tag !== URI.object ? x + : t.object.def( + Object.entries(x.def), + undefined, + Array.of().concat(x.opt), + ) + ), + fold((x) => { + switch (true) { + default: return fn.exhaustive(x) + case t.isNullary(x): return x + case t.isBoundable(x): return x + case x.tag === URI.eq: return x + case x.tag === URI.optional: return t.optional.def(x.def) + case x.tag === URI.array: return t.array.def(x.def) + case x.tag === URI.record: return t.record.def(x.def) + case x.tag === URI.union: return t.union.def(x.def.sort(weightComparator)) + case x.tag === URI.intersect: return t.intersect.def([...x.def].sort(weightComparator)) + case x.tag === URI.tuple: return t.tuple.def(bindPreSortIndices(x.def).sort(weightComparator)) + case x.tag === URI.object: return t.object.def( + x.def.sort(([, l], [, r]) => weightComparator(l, r)), + undefined, + x.opt, + ) + } + }), +) + +export function buildFunctionBody(schema: t.Schema): string { + let BODY = fn.pipe( + sort(schema), + fold(interpreter), + ).trim() + + if (BODY.startsWith('(') && BODY.endsWith(')')) + void (BODY = BODY.slice(1, -1)) + + let SINGLE_LINE = BODY.length < MAX_WIDTH + let OPEN = SINGLE_LINE ? '' : `(\r${' '.repeat(4)}` + let CLOSE = SINGLE_LINE ? '' : `\r${' '.repeat(2)})` + + return '' + + OPEN + + BODY + + CLOSE +} + + +export let generate = (schema: t.Schema): string => ` + +function check(value) { + return ${buildFunctionBody(schema)} +} + +`.trim() + + +export let generateParser = (schema: t.Schema): string => ` + +function check(value) { + return ${buildFunctionBody(schema)} +} +if (check(value)) return value +else throw Error("invalid input") + +`.trim() + + +export function compile(schema: S): ((x: S['_type'] | T.Unknown) => x is S['_type']) +export function compile(schema: t.Schema): Function { + return globalThis.Function( + 'value', + 'return ' + buildFunctionBody(schema) + ) +} + + +export function compileParser(schema: S): ((x: S['_type'] | T.Unknown) => S['_type']) +export function compileParser(schema: t.Schema): Function { + return globalThis.Function( + 'value', + 'return' + ` + +function check(value) { + return ${buildFunctionBody(schema)} +} +if (check(value)) return value +else throw Error("invalid input") + +` + .trim() + ) +} diff --git a/packages/schema-jit-compiler/src/json.ts b/packages/schema-jit-compiler/src/json.ts new file mode 100644 index 00000000..ad71764d --- /dev/null +++ b/packages/schema-jit-compiler/src/json.ts @@ -0,0 +1,183 @@ +import type * as T from '@traversable/registry' +import { escape, fn, isValidIdentifier, Object_entries, Object_values, URI } from '@traversable/registry' +import { Json } from '@traversable/json' + +import type * as F from './functor.js' +import { buildContext } from './shared.js' + +export let isScalar = Json.isScalar +export let isArray = Json.isArray +export let isObject = Json.isObject + +export let WeightByType = { + undefined: 1, + null: 2, + boolean: 4, + number: 8, + string: 16, + array: 128, + object: 256, +} as const + +export interface Free extends T.HKT { [-1]: IR } + +export type IR = + | { tag: URI.bottom, def: Json.Scalar } + | { tag: URI.array, def: T[] } + | { tag: URI.object, def: [k: string, v: T][] } + +export type Fixpoint = + | { tag: URI.bottom, def: Json.Scalar } + | { tag: URI.array, def: Fixpoint[] } + | { tag: URI.object, def: [k: string, v: Fixpoint][] } + +export type Index = Omit +export type Algebra = T.IndexedAlgebra + +export let defaultIndex = { + dataPath: [], + isRoot: true, + offset: 2, + siblingCount: 0, + varName: 'value', +} satisfies Index + +let map + : T.Functor['map'] + = (f) => (xs) => { + switch (true) { + default: return fn.exhaustive(xs) + case xs.tag === URI.bottom: return xs + case xs.tag === URI.array: return { tag: xs.tag, def: fn.map(xs.def, f) } + case xs.tag === URI.object: return { + tag: xs.tag, + def: fn.map(xs.def, ([k, v]) => [k, f(v)] satisfies [any, any]), + } + } + } + +export let Functor: T.Functor.Ix = { + map, + mapWithIndex(f) { + return function mapFn(xs, ix) { + switch (true) { + default: return fn.exhaustive(xs) + case xs.tag === URI.bottom: return xs + case xs.tag === URI.array: return { + tag: xs.tag, def: fn.map(xs.def, (x, i) => f(x, { + dataPath: [...ix.dataPath, i], + isRoot: false, + offset: ix.offset + 2, + siblingCount: xs.def.length, + varName: ix.varName + `[${i}]`, + })) + } + case Json.isObject(xs): return { + tag: xs.tag, def: fn.map(xs.def, ([k, v]) => [k, f(v, { + dataPath: [...ix.dataPath, k], + isRoot: false, + offset: ix.offset + 2, + siblingCount: Object_values(xs).length, + varName: ix.varName + (isValidIdentifier(k) ? `.${k}` : `["${k}"]`), + })] satisfies [any, any]) + } + } + } + }, +} + +export function fold(algebra: T.IndexedAlgebra): (json: IR, ix?: Index) => T +export function fold(algebra: T.IndexedAlgebra) { + return (json: Fixpoint, index = defaultIndex) => fn.cataIx(Functor)(algebra)(json, index) +} + +let comparator: T.Comparator = (l, r) => { + let lw = getWeight(l) + let rw = getWeight(r) + return lw < rw ? -1 : rw < lw ? +1 : 0 +} + +export let getWeight = (x: Json): number => { + switch (true) { + default: return fn.exhaustive(x) + case x === undefined: return WeightByType.undefined + case x === null: return WeightByType.null + case typeof x === 'boolean': return WeightByType.boolean + case typeof x === 'number': return WeightByType.number + case typeof x === 'string': return WeightByType.string + case Json.isArray(x): return WeightByType.array + x.reduce((acc: number, cur) => acc + getWeight(cur), 0) + case Json.isObject(x): return WeightByType.object + Object_values(x).reduce((acc: number, cur) => acc + getWeight(cur), 0) + } +} + +export let sort = fn.flow( + Json.fold((x) => { + switch (true) { + default: return fn.exhaustive(x) + case x === undefined: + case x === null: + case typeof x === 'boolean': + case typeof x === 'number': + case typeof x === 'string': return { tag: URI.bottom, def: x } + case Json.isArray(x): return { tag: URI.array, def: [...x] } + case Json.isObject(x): return { tag: URI.object, def: Object_entries(x) } + } + }), + fold((x) => { + switch (true) { + default: return fn.exhaustive(x) + case x.tag === URI.bottom: return x + case x.tag === URI.array: return { tag: URI.array, def: x.def.sort(comparator) } + case x.tag === URI.object: return { tag: URI.object, def: x.def.sort(([, l], [, r]) => comparator(l, r)) } + } + }), +) + +export function interpreter(x: IR>, ix: Index): IR +export function interpreter(x: IR>, ix: Index): { + tag: URI.bottom | URI.array | URI.object + def: unknown +} { + let { VAR, join } = buildContext(ix) + switch (true) { + default: return fn.exhaustive(x) + case x.tag === URI.bottom: { + let BODY = VAR + ' === ' + switch (true) { + default: return fn.exhaustive(x.def) + case x.def === null: BODY += 'null'; break + case x.def === undefined: BODY += 'undefined'; break + case x.def === true: BODY += 'true'; break + case x.def === false: BODY += 'false'; break + case x.def === 0: BODY += 1 / x.def === Number.NEGATIVE_INFINITY ? '-0' : '+0'; break + case typeof x.def === 'string': BODY += `"${escape(x.def)}"`; break + case typeof x.def === 'number': BODY += String(x.def); break + } + return { + tag: URI.bottom, + def: BODY, + } + } + case x.tag === URI.array: return { + tag: URI.array, + def: '' + + `Array.isArray(${VAR}) && ` + + `${VAR}.length === ${x.def.length}` + + (x.def.length === 0 ? '' : x.def.map((v, i) => i === 0 ? join(0) + v.def : v.def).join(join(0))), + } + case x.tag === URI.object: return { + tag: URI.object, + def: '' + + `!!${VAR} && typeof ${VAR} === "object" && !Array.isArray(${VAR})` + + (x.def.length === 0 ? '' : x.def.map(([, v], i) => i === 0 ? join(0) + v.def : v.def).join(join(0))), + } + } +} + +export function generate(json: Json, index?: Index): string +export function generate(json: Json, index?: Index) { + return fn.pipe( + sort(json), + (sorted) => fold(interpreter)(sorted, index).def, + ) +} diff --git a/packages/schema-jit-compiler/src/shared.ts b/packages/schema-jit-compiler/src/shared.ts new file mode 100644 index 00000000..aac87a14 --- /dev/null +++ b/packages/schema-jit-compiler/src/shared.ts @@ -0,0 +1,78 @@ +import type * as T from '@traversable/registry' +import { fn, isValidIdentifier, parseKey } from '@traversable/registry' +import { t } from '@traversable/schema-core' + +import type { Index } from './functor.js' + +export type F = + | t.Leaf + | t.eq + | t.array + | t.record + | t.optional + | t.union + | t.intersect + | t.tuple + | t.object<[k: string, v: T][]> + +export type Fixpoint = + | t.Leaf + | t.eq + | t.array + | t.record + | t.optional + | t.union + | t.intersect + | t.tuple + | t.object<[k: string, v: Fixpoint][]> + +export interface Free extends T.HKT { [-1]: F } + +export type Context = { + VAR: string + indent(numberOfSpaces: number): string + dedent(numberOfSpaces: number): string + join(numberOfSpaces: number): string +} + +export let makeIndent + : (offset: number) => (numberOfSpaces: number) => string + = (off) => (n) => `\r${' '.repeat(Math.max(off + n, 0))}` + +export let makeDedent + : (offset: number) => (numberOfSpaces: number) => string + = (off) => makeIndent(-off) + +export let makeJoin + : (offset: number) => (numberOfSpaces: number) => string + = (off) => fn.flow(makeIndent(off), (_) => `${_}&& `) + +export let buildContext + : (ix: T.Require) => Context + = ({ offset, varName: VAR }) => ({ + VAR, + indent: makeIndent(offset), + dedent: makeDedent(offset), + join: makeJoin(offset), + }) + +export function keyAccessor(key: keyof any | undefined, $: Index) { + return typeof key === 'string' ? isValidIdentifier(key) ? $.isOptional + ? `?.${key}` + : `.${key}` + : `[${parseKey(key)}]` + : '' +} + +/** + * Reading `x` to access the "preSortIndex" is a hack to make sure + * we preserve the original order of the tuple, even while sorting + */ +export function indexAccessor(index: keyof any | undefined, $: { isOptional?: boolean }, x?: any) { + return 'preSortIndex' in x + ? $.isOptional ? `?.[${x.preSortIndex}]` : `[${x.preSortIndex}]` + : typeof index === 'number' ? $.isOptional + ? `?.[${index}]` + : `[${index}]` + : '' +} diff --git a/packages/schema-jit-compiler/src/version.ts b/packages/schema-jit-compiler/src/version.ts new file mode 100644 index 00000000..660ff1ca --- /dev/null +++ b/packages/schema-jit-compiler/src/version.ts @@ -0,0 +1,3 @@ +import pkg from './__generated__/__manifest__.js' +export const VERSION = `${pkg.name}@${pkg.version}` as const +export type VERSION = typeof VERSION diff --git a/packages/schema-jit-compiler/test/jit.test.ts b/packages/schema-jit-compiler/test/jit.test.ts new file mode 100644 index 00000000..ac5913c1 --- /dev/null +++ b/packages/schema-jit-compiler/test/jit.test.ts @@ -0,0 +1,1733 @@ +import * as vi from 'vitest' +import { fc, test } from '@fast-check/vitest' +import { Seed } from '@traversable/schema-seed' +import { t, configure } from '@traversable/schema-core' + +import { Jit } from '@traversable/schema-jit-compiler' +import { Arbitrary } from '@traversable/schema-arbitrary' + + +vi.describe.skip('〖⛳️〗‹‹‹ ❲@traversable/schema-jit-compiler❳: Jit.compile w/ randomly generated data', () => { + let hardcodedSchema = t.object({ + "#1C": t.object({ + twoC: t.intersect( + t.object({ + '\\3A': t.optional(t.symbol), + '\\3B': t.optional(t.array(t.union(t.eq({ tag: 'left' }), t.eq({ tag: 'right' })))), + }), + t.object({ + g: t.tuple( + t.object({ h: t.any }) + ), + h: t.optional(t.object({ i: t.optional(t.boolean), j: t.union(t.number, t.bigint) })), + }) + ), + twoB: t.eq({ + "#3B": [ + 1, + [2], + [[3]], + ], + "#3A": { + n: 'over 9000', + o: [ + { p: false }, + ], + } + }), + twoA: t.integer, + }), + "#1A": t.union(t.integer), + "#1B": t.tuple( + t.record(t.any), + ), + }) + + let check = Jit.compile(hardcodedSchema) + + test.prop([Arbitrary.fromSchema(hardcodedSchema)], { + // numRuns: 10_000, + endOnFailure: true, + })('〖⛳️〗› ❲Jit.compile❳: randomly generated data', (_) => { + try { vi.assert.isTrue(check(_)) } + catch (e) { + let generated = Jit.generate(hardcodedSchema) + console.group('\r\n ===== Jit.compile property test failed ===== \r\n') + console.error() + console.error('Check for valid, randomly generated data failed') + console.error('Schema:\r\n\n' + generated + '\r\n\n') + console.error('Input:\r\n\n' + JSON.stringify(_, null, 2) + '\r\n\n') + console.groupEnd() + vi.assert.fail(t.has('message', t.string)(e) ? e.message : JSON.stringify(e, null, 2)) + } + }) +}) + + +vi.describe.skip('〖⛳️〗‹‹‹ ❲@traversable/schema-jit-compiler❳: Jit.compile w/ randomly generated schemas', () => { + test.prop([fc.letrec(Seed.seed()).tree], { + endOnFailure: true, + numRuns: 10, + // numRuns: 10_000, + })( + '〖⛳️〗› ❲Jit.compile❳: randomly generated schema', (seed) => { + let schema = Seed.toSchema(seed) + let check = Jit.compile(schema) + let arbitrary = Arbitrary.fromSchema(schema) + let inputs = fc.sample(arbitrary, 100) + + for (let input of inputs) { + try { vi.assert.isTrue(check(input)) } + catch (e) { + let generated = Jit.generate(schema) + console.group('\r\n ===== Jit.compile property test failed ===== \r\n') + console.error() + console.error('Check for valid, randomly generated data failed') + console.error('Schema:\r\n\n' + generated + '\r\n\n') + console.error('Input:\r\n\n' + JSON.stringify(input, null, 2) + '\r\n\n') + console.groupEnd() + vi.assert.fail(t.has('message', t.string)(e) ? e.message : JSON.stringify(e, null, 2)) + } + } + } + ) +}) + + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-jit-compiler❳: Jit.compile', () => { + vi.describe('〖⛳️〗‹‹ ❲Jit.compile❳: eq', () => { + let check = Jit.compile( + t.eq({ + a: false, + }) + ) + + vi.test.concurrent.for([ + {}, + { a: true }, + ])( + '〖⛳️〗› ❲t.eq❳: check fails with bad input (index %#)', + (_) => vi.assert.isFalse(check(_)) + ) + + vi.test.concurrent.for([ + { a: false }, + ])( + '〖⛳️〗› ❲t.eq❳: check succeeds with valid input (index %#)', + (_) => vi.assert.isTrue(check(_)) + ) + }) + + + vi.describe('〖⛳️〗‹‹ ❲Jit.compile❳: array', () => { + let check = Jit.compile( + t.array(t.boolean) + ) + + vi.test.concurrent.for([ + [1], + ])( + '〖⛳️〗› ❲t.array❳: check fails with bad input (index %#)', + (_) => vi.assert.isFalse(check(_)) + ) + + vi.test.concurrent.for([ + [], + [Math.random() > 0.5], + ])( + '〖⛳️〗› ❲t.array❳: check succeeds with valid input (index %#)', + (_) => vi.assert.isTrue(check(_)) + ) + }) + + + vi.describe('〖⛳️〗‹‹ ❲Jit.compile❳: record', () => { + let check = Jit.compile( + t.record(t.boolean) + ) + + vi.test.concurrent.for([ + [], + { a: 0 }, + ])( + '〖⛳️〗› ❲t.record❳: check fails with bad input (index %#)', + (_) => vi.assert.isFalse(check(_)) + ) + + vi.test.concurrent.for([ + {}, + { a: false }, + ])( + '〖⛳️〗› ❲t.record❳: check succeeds with valid input (index %#)', + (_) => vi.assert.isTrue(check(_)) + ) + }) + + + vi.describe('〖⛳️〗‹‹ ❲Jit.compile❳: optional', () => { + let check = Jit.compile( + t.object({ + a: t.optional(t.boolean), + }) + ) + + vi.test.concurrent.for([ + { a: 0 }, + ])( + '〖⛳️〗› ❲t.optional❳: check fails with bad input (index %#)', + (_) => vi.assert.isFalse(check(_)) + ) + + vi.test.concurrent.for([ + {}, + { a: false }, + ])( + '〖⛳️〗› ❲t.optional❳: check succeeds with valid input (index %#)', + (_) => vi.assert.isTrue(check(_)) + ) + }) + + + vi.describe('〖⛳️〗‹‹ ❲Jit.compile❳: tuple', () => { + let check = Jit.compile( + t.tuple( + t.string, + t.number, + ) + ) + + vi.test.concurrent.for([ + [], + [0, ''], + ])( + '〖⛳️〗› ❲t.tuple❳: check fails with bad input (index %#)', + (_) => vi.assert.isFalse(check(_)) + ) + + vi.test.concurrent.for([ + ['', 0], + ])( + '〖⛳️〗› ❲t.tuple❳: check succeeds with valid input (index %#)', + (_) => vi.assert.isTrue(check(_)) + ) + }) + + + vi.describe('〖⛳️〗‹‹ ❲Jit.compile❳: union', () => { + let check = Jit.compile( + t.union( + t.string, + t.number, + ) + ) + + vi.test.concurrent.for([ + false, + ])( + '〖⛳️〗› ❲t.union❳: check fails with bad input (index %#)', + (_) => vi.assert.isFalse(check(_)) + ) + + vi.test.concurrent.for([ + '', + 0, + ])( + '〖⛳️〗› ❲t.union❳: check succeeds with valid input (index %#)', + (_) => vi.assert.isTrue(check(_)) + ) + }) + + + vi.describe('〖⛳️〗‹‹ ❲Jit.compile❳: intersect', () => { + let check = Jit.compile( + t.intersect( + t.object({ + a: t.boolean, + }), + t.object({ + b: t.integer, + }) + ) + ) + + vi.test.concurrent.for([ + {}, + { a: false }, + { b: 0 }, + { a: false, b: '' }, + { a: '', b: 0 }, + ])( + '〖⛳️〗› ❲t.intersect❳: check fails with bad input (index %#)', + (_) => vi.assert.isFalse(check(_)) + ) + + vi.test.concurrent.for([ + { a: false, b: 0 }, + ])( + '〖⛳️〗› ❲t.intersect❳: check succeeds with valid input (index %#)', + (_) => vi.assert.isTrue(check(_)) + ) + }) + + + vi.describe('〖⛳️〗‹‹ ❲Jit.compile❳: object', () => { + let check = Jit.compile( + t.object({ + a: t.boolean, + }) + ) + + vi.test.concurrent.for([ + {}, + { a: 0 }, + { b: false }, + ])( + '〖⛳️〗› ❲t.object❳: check fails with bad input (index %#)', + (_) => vi.assert.isFalse(check(_)) + ) + + vi.test.concurrent.for([ + { a: false }, + ])( + '〖⛳️〗› ❲t.object❳: check succeeds with valid input (index %#)', + (_) => vi.assert.isTrue(check(_)) + ) + }) + +}) + + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-jit-compiler❳: nullary', () => { + + vi.it('〖⛳️〗› ❲jit❳: t.never', () => { + vi.expect(Jit.generate( + t.never + )).toMatchInlineSnapshot + (` + "function check(value) { + return false + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.any', () => { + vi.expect(Jit.generate( + t.any + )).toMatchInlineSnapshot + (` + "function check(value) { + return true + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.unknown', () => { + vi.expect(Jit.generate( + t.unknown + )).toMatchInlineSnapshot + (` + "function check(value) { + return true + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.void', () => { + vi.expect(Jit.generate( + t.void + )).toMatchInlineSnapshot + (` + "function check(value) { + return value === void 0 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.null', () => { + vi.expect(Jit.generate( + t.null + )).toMatchInlineSnapshot + (` + "function check(value) { + return value === null + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.undefined', () => { + vi.expect(Jit.generate( + t.undefined + )).toMatchInlineSnapshot + (` + "function check(value) { + return value === undefined + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.symbol', () => { + vi.expect(Jit.generate( + t.symbol + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "symbol" + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.boolean', () => { + vi.expect(Jit.generate( + t.boolean + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "boolean" + }" + `) + }) +}) + + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-jit-compiler❳: boundable', () => { + + vi.it('〖⛳️〗› ❲jit❳: t.integer', () => { + vi.expect(Jit.generate( + t.integer + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isSafeInteger(value) + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.integer.min(x)', () => { + vi.expect(Jit.generate( + t.integer.min(0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isSafeInteger(value) && 0 <= value + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.integer.max(x)', () => { + vi.expect(Jit.generate( + t.integer.max(1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isSafeInteger(value) && value <= 1 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.integer.min(x).max(y)', () => { + vi.expect(Jit.generate( + t.integer + .min(0) + .max(1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isSafeInteger(value) && 0 <= value && value <= 1 + }" + `) + + vi.expect(Jit.generate( + t.integer + .max(1) + .min(0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isSafeInteger(value) && 0 <= value && value <= 1 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.integer.between(x, y)', () => { + vi.expect(Jit.generate( + t.integer.between(0, 1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isSafeInteger(value) && 0 <= value && value <= 1 + }" + `) + + vi.expect(Jit.generate( + t.integer.between(1, 0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isSafeInteger(value) && 0 <= value && value <= 1 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.bigint', () => { + vi.expect(Jit.generate( + t.bigint + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "bigint" + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.bigint.min(x)', () => { + vi.expect(Jit.generate( + t.bigint.min(0n) + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "bigint" && 0n <= value + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.bigint.max(x)', () => { + vi.expect(Jit.generate( + t.bigint.max(1n) + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "bigint" && value <= 1n + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.bigint.min(x).max(y)', () => { + vi.expect(Jit.generate( + t.bigint + .min(0n) + .max(1n) + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "bigint" && 0n <= value && value <= 1n + }" + `) + + vi.expect(Jit.generate( + t.bigint + .max(1n) + .min(0n) + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "bigint" && 0n <= value && value <= 1n + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.bigint.between(x, y)', () => { + vi.expect(Jit.generate( + t.bigint.between(0n, 1n) + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "bigint" && 0n <= value && value <= 1n + }" + `) + + vi.expect(Jit.generate( + t.bigint.between(1n, 0n) + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "bigint" && 0n <= value && value <= 1n + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.number', () => { + vi.expect(Jit.generate( + t.number + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.number.min(x)', () => { + vi.expect(Jit.generate( + t.number.min(0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && 0 <= value + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.number.max(x)', () => { + vi.expect(Jit.generate( + t.number.max(1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && value <= 1 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.number.min(x).max(y)', () => { + vi.expect(Jit.generate( + t.number + .min(0) + .max(1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && 0 <= value && value <= 1 + }" + `) + + vi.expect(Jit.generate( + t.number + .max(1) + .min(0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && 0 <= value && value <= 1 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.number.between(x, y)', () => { + vi.expect(Jit.generate( + t.number.between(0, 1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && 0 <= value && value <= 1 + }" + `) + + vi.expect(Jit.generate( + t.number.between(1, 0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && 0 <= value && value <= 1 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.number.moreThan(x)', () => { + vi.expect(Jit.generate( + t.number.moreThan(0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && 0 < value + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.number.lessThan(x)', () => { + vi.expect(Jit.generate( + t.number.lessThan(1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && value < 1 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.number.moreThan(x).lessThan(y)', () => { + vi.expect(Jit.generate( + t.number + .moreThan(0) + .lessThan(1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && 0 < value && value < 1 + }" + `) + + vi.expect(Jit.generate( + t.number + .lessThan(1) + .moreThan(0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && 0 < value && value < 1 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.number.min(x).lessThan(y)', () => { + vi.expect(Jit.generate( + t.number + .min(0) + .lessThan(1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && 0 <= value && value < 1 + }" + `) + + vi.expect(Jit.generate( + t.number + .lessThan(1) + .min(0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && 0 <= value && value < 1 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.number.moreThan(x).max(y)', () => { + vi.expect(Jit.generate( + t.number + .moreThan(0) + .max(1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && 0 < value && value <= 1 + }" + `) + + vi.expect(Jit.generate( + t.number + .max(1) + .moreThan(0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Number.isFinite(value) && 0 < value && value <= 1 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.string', () => { + vi.expect(Jit.generate( + t.string + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "string" + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.string.min(x)', () => { + vi.expect(Jit.generate( + t.string.min(0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "string" && 0 <= value.length + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.string.max(x)', () => { + vi.expect(Jit.generate( + t.string.max(1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "string" && value.length <= 1 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.string.min(x).max(y)', () => { + vi.expect(Jit.generate( + t.string + .min(0) + .max(1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "string" && 0 <= value.length && value.length <= 1 + }" + `) + + vi.expect(Jit.generate( + t.string + .max(1) + .min(0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "string" && 0 <= value.length && value.length <= 1 + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.string.between(x, y)', () => { + vi.expect(Jit.generate( + t.string.between(0, 1) + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "string" && 0 <= value.length && value.length <= 1 + }" + `) + + vi.expect(Jit.generate( + t.string.between(1, 0) + )).toMatchInlineSnapshot + (` + "function check(value) { + return typeof value === "string" && 0 <= value.length && value.length <= 1 + }" + `) + }) +}) + + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-jit-compiler❳: unary', () => { + + vi.it('〖⛳️〗› ❲jit❳: t.eq(...)', () => { + vi.expect(Jit.generate( + t.eq({ + l: 'L', + m: 'M' + }) + )).toMatchInlineSnapshot + (` + "function check(value) { + return !!value && typeof value === "object" && !Array.isArray(value) + && value.l === "L" + && value.m === "M" + }" + `) + + vi.expect(Jit.generate( + t.eq( + [ + { + a: 3, + b: 3, + c: [5, 6], + }, + { z: 2 }, + 1 + ] + ) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + Array.isArray(value) && value.length === 3 + && value[0] === 1 + && !!value[1] && typeof value[1] === "object" && !Array.isArray(value[1]) + && value[1].z === 2 + && !!value[2] && typeof value[2] === "object" && !Array.isArray(value[2]) + && value[2].a === 3 + && value[2].b === 3 + && Array.isArray(value[2].c) && value[2].c.length === 2 + && value[2].c[0] === 5 + && value[2].c[1] === 6 + ) + }" + `) + + vi.expect(Jit.generate( + t.eq( + [ + 1, + { z: 2 }, + { + a: 3, + b: 3, + c: [5, 6], + } + ] + ))).toMatchInlineSnapshot + (` + "function check(value) { + return ( + Array.isArray(value) && value.length === 3 + && value[0] === 1 + && !!value[1] && typeof value[1] === "object" && !Array.isArray(value[1]) + && value[1].z === 2 + && !!value[2] && typeof value[2] === "object" && !Array.isArray(value[2]) + && value[2].a === 3 + && value[2].b === 3 + && Array.isArray(value[2].c) && value[2].c.length === 2 + && value[2].c[0] === 5 + && value[2].c[1] === 6 + ) + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.optional(...)', () => { + vi.expect(Jit.generate( + t.optional(t.eq(1)) + )).toMatchInlineSnapshot + (` + "function check(value) { + return value === undefined || value === 1 + }" + `) + + vi.expect(Jit.generate( + t.optional(t.optional(t.eq(1))) + )).toMatchInlineSnapshot + (` + "function check(value) { + return value === undefined || (value === undefined || value === 1) + }" + `) + + vi.expect(Jit.generate( + t.optional( + t.union( + t.eq(1000), + t.eq(2000), + t.eq(3000), + t.eq(4000), + t.eq(5000), + t.eq(6000), + ) + ) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + + value === undefined + || ((value === 1000) || (value === 2000) || (value === 3000) || (value === 4000) || (value === 5000) || (value === 6000)) + + ) + }" + `) + + vi.expect(Jit.generate( + t.optional( + t.union( + t.eq(1000), + t.eq(2000), + t.eq(3000), + t.eq(4000), + t.eq(5000), + t.eq(6000), + t.eq(9000), + ) + ) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + + value === undefined + || ( + (value === 1000) + || (value === 2000) + || (value === 3000) + || (value === 4000) + || (value === 5000) + || (value === 6000) + || (value === 9000) + ) + + ) + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.array(...)', () => { + vi.expect(Jit.generate( + t.array(t.eq(1)) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Array.isArray(value) && value.every((value) => value === 1) + }" + `) + + vi.expect(Jit.generate( + t.array(t.array(t.eq(2))) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Array.isArray(value) && value.every((value) => Array.isArray(value) && value.every((value) => value === 2)) + }" + `) + + vi.expect(Jit.generate( + t.array(t.array(t.array(t.array(t.array(t.array(t.eq(3))))))) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + Array.isArray(value) && value.every((value) => + Array.isArray(value) && value.every((value) => + Array.isArray(value) && value.every((value) => + Array.isArray(value) && value.every((value) => + Array.isArray(value) && value.every((value) => Array.isArray(value) && value.every((value) => value === 3)) + ) + ) + ) + ) + ) + }" + `) + }) + + vi.it('〖⛳️〗› ❲jit❳: t.record(...)', () => { + vi.expect(Jit.generate( + t.record(t.eq(1)) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && !(value instanceof Date) && !(value instanceof Uint8Array) + && Object.entries(value).every(([key, value]) => + typeof key === "string" && value === 1 + ) + ) + }" + `) + + vi.expect(Jit.generate( + t.record(t.record(t.eq(2))) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && !(value instanceof Date) && !(value instanceof Uint8Array) + && Object.entries(value).every(([key, value]) => + typeof key === "string" && !!value && typeof value === "object" && !Array.isArray(value) + && !(value instanceof Date) && !(value instanceof Uint8Array) + && Object.entries(value).every(([key, value]) => + typeof key === "string" && value === 2 + ) + ) + ) + }" + `) + + }) + + vi.it('〖⛳️〗› ❲jit❳: t.union(...)', () => { + + vi.expect(Jit.generate( + t.union() + )).toMatchInlineSnapshot + (` + "function check(value) { + return false + }" + `) + + vi.expect(Jit.generate( + t.union(t.never) + )).toMatchInlineSnapshot + (` + "function check(value) { + return (false) + }" + `) + + vi.expect(Jit.generate( + t.union(t.unknown) + )).toMatchInlineSnapshot + (` + "function check(value) { + return (true) + }" + `) + + vi.expect(Jit.generate( + t.union(t.union()) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ((false)) + }" + `) + + vi.expect(Jit.generate( + t.union( + t.integer, + t.bigint, + ) + )).toMatchInlineSnapshot + + (` + "function check(value) { + return (Number.isSafeInteger(value)) || (typeof value === "bigint") + }" + `) + + vi.expect(Jit.generate( + t.union( + t.boolean, + t.symbol, + t.integer, + t.bigint, + t.number, + t.string, + ) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + (typeof value === "symbol") + || (typeof value === "boolean") + || (Number.isSafeInteger(value)) + || (typeof value === "bigint") + || (Number.isFinite(value)) + || (typeof value === "string") + ) + }" + `) + + vi.expect(Jit.generate( + t.union( + t.object({ + a: t.eq(1), + }), + t.object({ + b: t.eq(2), + }), + t.object({ + c: t.eq(3) + }) + ) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + (!!value && typeof value === "object" && !Array.isArray(value) && value.a === 1) + || (!!value && typeof value === "object" && !Array.isArray(value) && value.b === 2) + || (!!value && typeof value === "object" && !Array.isArray(value) && value.c === 3) + ) + }" + `) + + vi.expect(Jit.generate( + t.union( + t.eq(9000), + t.union( + t.object({ + a: t.eq(1), + }), + t.object({ + b: t.eq(2), + }), + t.object({ + c: t.eq(3) + }) + ) + ) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + (value === 9000) + || (( + (!!value && typeof value === "object" && !Array.isArray(value) && value.a === 1) + || (!!value && typeof value === "object" && !Array.isArray(value) && value.b === 2) + || (!!value && typeof value === "object" && !Array.isArray(value) && value.c === 3) + )) + ) + }" + `) + + }) + + vi.it('〖⛳️〗› ❲jit❳: t.intersect(...)', () => { + vi.expect(Jit.generate( + t.intersect(t.unknown) + )).toMatchInlineSnapshot + (` + "function check(value) { + return true + }" + `) + + vi.expect(Jit.generate( + t.intersect( + t.object({ + a: t.eq(1), + }), + t.object({ + b: t.eq(2), + }), + t.object({ + c: t.eq(3) + }) + ) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) && value.a === 1 + && !!value && typeof value === "object" && !Array.isArray(value) && value.b === 2 + && !!value && typeof value === "object" && !Array.isArray(value) && value.c === 3 + ) + }" + `) + + vi.expect(Jit.generate( + t.intersect( + t.eq(9000), + t.intersect( + t.object({ + a: t.eq(1), + }), + t.object({ + b: t.eq(2), + }), + t.object({ + c: t.eq(3) + }), + ) + ) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + value === 9000 + && ( + !!value && typeof value === "object" && !Array.isArray(value) && value.a === 1 + && !!value && typeof value === "object" && !Array.isArray(value) && value.b === 2 + && !!value && typeof value === "object" && !Array.isArray(value) && value.c === 3 + ) + ) + }" + `) + + }) + + vi.it('〖⛳️〗› ❲jit❳: t.tuple(...)', () => { + vi.expect(Jit.generate( + t.tuple() + )).toMatchInlineSnapshot + (` + "function check(value) { + return Array.isArray(value) && value.length === 0 + }" + `) + + vi.expect(Jit.generate( + t.tuple(t.unknown) + )).toMatchInlineSnapshot + (` + "function check(value) { + return Array.isArray(value) && value.length === 1 && true + }" + `) + + vi.expect(Jit.generate( + t.tuple( + t.tuple(), + t.tuple(), + ) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + Array.isArray(value) && value.length === 2 + && Array.isArray(value[0]) && value[0].length === 0 + && Array.isArray(value[1]) && value[1].length === 0 + ) + }" + `) + + vi.expect(Jit.generate( + t.tuple( + t.tuple( + t.eq('[0][0]'), + ), + t.tuple( + t.eq('[1][0]'), + ), + ) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + Array.isArray(value) && value.length === 2 + && Array.isArray(value[0]) && value[0].length === 1 && value[0][0] === "[0][0]" + && Array.isArray(value[1]) && value[1].length === 1 && value[1][0] === "[1][0]" + ) + }" + `) + + vi.expect(Jit.generate( + t.tuple( + t.tuple( + t.eq('[0][0]'), + t.eq('[0][1]'), + ), + t.tuple( + t.eq('[1][0]'), + t.eq('[1][1]'), + ) + ) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + Array.isArray(value) && value.length === 2 + && Array.isArray(value[0]) && value[0].length === 2 && value[0][0] === "[0][0]" && value[0][1] === "[0][1]" + && Array.isArray(value[1]) && value[1].length === 2 && value[1][0] === "[1][0]" && value[1][1] === "[1][1]" + ) + }" + `) + + vi.expect(Jit.generate( + t.tuple( + t.tuple( + t.tuple( + t.eq('[0][0][0]'), + t.eq('[0][0][1]'), + t.eq('[0][0][2]'), + ), + t.tuple( + t.eq('[0][1][0]'), + t.eq('[0][1][1]'), + t.eq('[0][1][2]'), + ), + t.tuple( + t.eq('[0][2][0]'), + t.eq('[0][2][1]'), + t.eq('[0][2][2]'), + ), + ), + t.tuple( + t.tuple( + t.eq('[1][0][0]'), + t.eq('[1][0][1]'), + t.eq('[1][0][2]'), + ), + t.tuple( + t.eq('[1][1][0]'), + t.eq('[1][1][1]'), + t.eq('[1][1][2]'), + ), + t.tuple( + t.eq('[1][2][0]'), + t.eq('[1][2][1]'), + t.eq('[1][2][2]'), + ), + ), + t.tuple( + t.tuple( + t.eq('[2][0][0]'), + t.eq('[2][0][1]'), + t.eq('[2][0][2]'), + ), + t.tuple( + t.eq('[2][1][0]'), + t.eq('[2][1][1]'), + t.eq('[2][1][2]'), + ), + t.tuple( + t.eq('[2][2][0]'), + t.eq('[2][2][1]'), + t.eq('[2][2][2]'), + ), + ) + ) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + Array.isArray(value) && value.length === 3 + && Array.isArray(value[0]) && value[0].length === 3 + && Array.isArray(value[0][0]) && value[0][0].length === 3 + && value[0][0][0] === "[0][0][0]" + && value[0][0][1] === "[0][0][1]" + && value[0][0][2] === "[0][0][2]" + && Array.isArray(value[0][1]) && value[0][1].length === 3 + && value[0][1][0] === "[0][1][0]" + && value[0][1][1] === "[0][1][1]" + && value[0][1][2] === "[0][1][2]" + && Array.isArray(value[0][2]) && value[0][2].length === 3 + && value[0][2][0] === "[0][2][0]" + && value[0][2][1] === "[0][2][1]" + && value[0][2][2] === "[0][2][2]" + && Array.isArray(value[1]) && value[1].length === 3 + && Array.isArray(value[1][0]) && value[1][0].length === 3 + && value[1][0][0] === "[1][0][0]" + && value[1][0][1] === "[1][0][1]" + && value[1][0][2] === "[1][0][2]" + && Array.isArray(value[1][1]) && value[1][1].length === 3 + && value[1][1][0] === "[1][1][0]" + && value[1][1][1] === "[1][1][1]" + && value[1][1][2] === "[1][1][2]" + && Array.isArray(value[1][2]) && value[1][2].length === 3 + && value[1][2][0] === "[1][2][0]" + && value[1][2][1] === "[1][2][1]" + && value[1][2][2] === "[1][2][2]" + && Array.isArray(value[2]) && value[2].length === 3 + && Array.isArray(value[2][0]) && value[2][0].length === 3 + && value[2][0][0] === "[2][0][0]" + && value[2][0][1] === "[2][0][1]" + && value[2][0][2] === "[2][0][2]" + && Array.isArray(value[2][1]) && value[2][1].length === 3 + && value[2][1][0] === "[2][1][0]" + && value[2][1][1] === "[2][1][1]" + && value[2][1][2] === "[2][1][2]" + && Array.isArray(value[2][2]) && value[2][2].length === 3 + && value[2][2][0] === "[2][2][0]" + && value[2][2][1] === "[2][2][1]" + && value[2][2][2] === "[2][2][2]" + ) + }" + `) + + }) + + vi.it('〖⛳️〗› ❲jit❳: object(...)', () => { + + vi.expect(Jit.generate( + t.object({}) + )).toMatchInlineSnapshot + (` + "function check(value) { + return !!value && typeof value === "object" && !Array.isArray(value) + }" + `) + + vi.expect(Jit.generate( + t.object({ + A: t.optional(t.number.min(1)), + }) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && (value.A === undefined || (Number.isFinite(value.A) && 1 <= value.A)) + ) + }" + `) + + vi.expect(Jit.generate( + t.object({ + A: t.optional(t.number), + B: t.array(t.integer) + }) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && (value.A === undefined || Number.isFinite(value.A)) + && Array.isArray(value.B) && value.B.every((value) => Number.isSafeInteger(value)) + ) + }" + `) + + vi.expect(Jit.generate( + t.object({ + B: t.array(t.integer), + A: t.object({ + C: t.optional(t.number) + }), + }) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && Array.isArray(value.B) && value.B.every((value) => Number.isSafeInteger(value)) + && !!value.A && typeof value.A === "object" && !Array.isArray(value.A) + && (value.A.C === undefined || Number.isFinite(value.A.C)) + ) + }" + `) + + vi.expect(Jit.generate( + t.object({ + A: t.union( + t.object({ + B: t.optional(t.eq(1)), + C: t.eq(2), + }), + t.object({ + D: t.optional(t.eq(3)), + E: t.eq(4), + }) + ) + }) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && ( + (!!value.A && typeof value.A === "object" && !Array.isArray(value.A) + && value.A.C === 2 + && (value.A.B === undefined || value.A.B === 1)) + || (!!value.A && typeof value.A === "object" && !Array.isArray(value.A) + && value.A.E === 4 + && (value.A.D === undefined || value.A.D === 3)) + ) + ) + }" + `) + + vi.expect(Jit.generate( + t.object({ + a: t.record(t.object({ + b: t.string, + c: t.tuple() + })) + }) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && !!value.a && typeof value.a === "object" && !Array.isArray(value.a) + && !(value.a instanceof Date) && !(value.a instanceof Uint8Array) + && Object.entries(value.a).every(([key, value]) => + typeof key === "string" && !!value && typeof value === "object" && !Array.isArray(value) + && typeof value.b === "string" + && Array.isArray(value.c) && value.c.length === 0 + ) + ) + }" + `) + + vi.expect(Jit.generate( + t.object({ + F: t.union( + t.object({ F: t.number }), + t.object({ G: t.any }) + ), + }) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && ( + (!!value.F && typeof value.F === "object" && !Array.isArray(value.F) && true) + || (!!value.F && typeof value.F === "object" && !Array.isArray(value.F) && Number.isFinite(value.F.F)) + ) + ) + }" + `) + + vi.expect(Jit.generate( + t.object({ + "#1C": t.object({ + twoC: t.intersect( + t.object({ + '\\3A': t.optional(t.symbol), + '\\3B': t.optional( + t.array( + t.union( + t.eq({ tag: 'left' }), + t.eq({ tag: 'right' }), + ) + ) + ), + }), + t.object({ + g: t.tuple( + t.object({ h: t.any }), + ), + h: t.optional( + t.object({ + i: t.optional(t.boolean), + j: t.union( + t.number.moreThan(0).max(128), + t.bigint, + ), + }) + ), + }) + ), + twoB: t.eq({ + "#3B": [ + 1, + [2], + [[3]], + ], + "#3A": { + n: 'over 9000', + o: [ + { p: false }, + ], + } + }), + twoA: t.integer.between(-10, 10), + }), + "#1A": t.integer.min(3), + "#1B": t.tuple( + t.record(t.any), + ), + }) + )).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && (Number.isSafeInteger(value["#1A"]) && 3 <= value["#1A"]) + && Array.isArray(value["#1B"]) && value["#1B"].length === 1 + && !!value["#1B"][0] && typeof value["#1B"][0] === "object" && !Array.isArray(value["#1B"][0]) + && !(value["#1B"][0] instanceof Date) && !(value["#1B"][0] instanceof Uint8Array) + && Object.entries(value["#1B"][0]).every(([key, value]) => + typeof key === "string" && true + ) + && !!value["#1C"] && typeof value["#1C"] === "object" && !Array.isArray(value["#1C"]) + && (Number.isSafeInteger(value["#1C"].twoA) && -10 <= value["#1C"].twoA && value["#1C"].twoA <= 10) + && !!value["#1C"].twoB && typeof value["#1C"].twoB === "object" && !Array.isArray(value["#1C"].twoB) + && !!value["#1C"].twoB["#3A"] && typeof value["#1C"].twoB["#3A"] === "object" && !Array.isArray(value["#1C"].twoB["#3A"]) + && value["#1C"].twoB["#3A"].n === "over 9000" + && Array.isArray(value["#1C"].twoB["#3A"].o) && value["#1C"].twoB["#3A"].o.length === 1 + && !!value["#1C"].twoB["#3A"].o[0] && typeof value["#1C"].twoB["#3A"].o[0] === "object" && !Array.isArray(value["#1C"].twoB["#3A"].o[0]) + && value["#1C"].twoB["#3A"].o[0].p === false + && Array.isArray(value["#1C"].twoB["#3B"]) && value["#1C"].twoB["#3B"].length === 3 + && value["#1C"].twoB["#3B"][0] === 1 + && Array.isArray(value["#1C"].twoB["#3B"][1]) && value["#1C"].twoB["#3B"][1].length === 1 + && value["#1C"].twoB["#3B"][1][0] === 2 + && Array.isArray(value["#1C"].twoB["#3B"][2]) && value["#1C"].twoB["#3B"][2].length === 1 + && Array.isArray(value["#1C"].twoB["#3B"][2][0]) && value["#1C"].twoB["#3B"][2][0].length === 1 + && value["#1C"].twoB["#3B"][2][0][0] === 3 + && ( + !!value["#1C"].twoC && typeof value["#1C"].twoC === "object" && !Array.isArray(value["#1C"].twoC) + && Array.isArray(value["#1C"].twoC.g) && value["#1C"].twoC.g.length === 1 + && !!value["#1C"].twoC.g[0] && typeof value["#1C"].twoC.g[0] === "object" && !Array.isArray(value["#1C"].twoC.g[0]) + && true + && ( + value["#1C"].twoC.h === undefined + || !!value["#1C"].twoC.h && typeof value["#1C"].twoC.h === "object" && !Array.isArray(value["#1C"].twoC.h) + && (value["#1C"].twoC.h?.i === undefined || typeof value["#1C"].twoC.h?.i === "boolean") + && ( + (typeof value["#1C"].twoC.h?.j === "bigint") + || ((Number.isFinite(value["#1C"].twoC.h?.j) && 0 < value["#1C"].twoC.h?.j && value["#1C"].twoC.h?.j <= 128)) + ) + ) + && !!value["#1C"].twoC && typeof value["#1C"].twoC === "object" && !Array.isArray(value["#1C"].twoC) + && (value["#1C"].twoC["\\\\3A"] === undefined || typeof value["#1C"].twoC["\\\\3A"] === "symbol") + && ( + value["#1C"].twoC["\\\\3B"] === undefined + || Array.isArray(value["#1C"].twoC["\\\\3B"]) && value["#1C"].twoC["\\\\3B"].every((value) => + ( + (!!value && typeof value === "object" && !Array.isArray(value) + && value.tag === "left") + || (!!value && typeof value === "object" && !Array.isArray(value) + && value.tag === "right") + ) + ) + ) + ) + ) + }" + `) + + }) +}) + + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-jit-compiler❳: configure', () => { + vi.it('〖⛳️〗› ❲jit❳: treatArraysAsObjects', () => { + let schema = t.object({ + F: t.union( + t.object({ F: t.number }), + t.object({ G: t.any }) + ), + }) + + configure({ + schema: { + treatArraysAsObjects: false, + } + }) && vi.expect(Jit.generate(schema)).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && ( + (!!value.F && typeof value.F === "object" && !Array.isArray(value.F) && true) + || (!!value.F && typeof value.F === "object" && !Array.isArray(value.F) && Number.isFinite(value.F.F)) + ) + ) + }" + `) + + configure({ + schema: { + treatArraysAsObjects: true + } + }) && vi.expect(Jit.generate(schema)).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" + && ( + (!!value.F && typeof value.F === "object" && true) + || (!!value.F && typeof value.F === "object" && Number.isFinite(value.F.F)) + ) + ) + }" + `) + + // Cleanup + configure({ schema: { treatArraysAsObjects: false } }) + }) + + vi.it('〖⛳️〗› ❲jit❳: exactOptional', () => { + + let schema = t.object({ + a: t.number, + b: t.optional(t.string), + c: t.optional(t.number.min(8)), + }) + + configure({ + schema: { + optionalTreatment: 'exactOptional', + } + }) && vi.expect(Jit.generate(schema)).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && Number.isFinite(value.a) + && (!Object.hasOwn(value, "c") || (Number.isFinite(value.c) && 8 <= value.c)) + && (!Object.hasOwn(value, "b") || typeof value.b === "string") + ) + }" + `) + + configure({ + schema: { + optionalTreatment: 'presentButUndefinedIsOK', + } + }) && vi.expect(Jit.generate(schema)).toMatchInlineSnapshot + (` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && Number.isFinite(value.a) + && (value.c === undefined || (Number.isFinite(value.c) && 8 <= value.c)) + && (value.b === undefined || typeof value.b === "string") + ) + }" + `) + }) +}) + diff --git a/packages/schema-jit-compiler/test/json.test.ts b/packages/schema-jit-compiler/test/json.test.ts new file mode 100644 index 00000000..d5a915e7 --- /dev/null +++ b/packages/schema-jit-compiler/test/json.test.ts @@ -0,0 +1,461 @@ +import * as vi from 'vitest' + +import { Json } from '@traversable/schema-jit-compiler' + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-jit-compiler❳: Jit.Json', () => { + + vi.it('〖⛳️〗› ❲Json.generate❳: throws given non-JSON input', () => { + /* @ts-expect-error */ + vi.assert.throws(() => Json.generate(Symbol())) + + /* @ts-expect-error */ + vi.assert.throws(() => Json.generate(1n)) + }) + + vi.it('〖⛳️〗› ❲Json.generate❳: null', () => { + vi.expect(Json.generate( + null + )).toMatchInlineSnapshot + + (`"value === null"`) + }) + + vi.it('〖⛳️〗› ❲Json.generate❳: undefined', () => { + vi.expect(Json.generate( + undefined + )).toMatchInlineSnapshot + (`"value === undefined"`) + }) + + vi.it('〖⛳️〗› ❲Json.generate❳: booleans', () => { + vi.expect(Json.generate( + false + )).toMatchInlineSnapshot + (`"value === false"`) + + vi.expect(Json.generate( + true + )).toMatchInlineSnapshot + (`"value === true"`) + }) + + vi.it('〖⛳️〗› ❲Json.generate❳: numbers', () => { + vi.expect(Json.generate( + Number.MIN_SAFE_INTEGER + )).toMatchInlineSnapshot + (`"value === -9007199254740991"`) + + vi.expect(Json.generate( + Number.MAX_SAFE_INTEGER + )).toMatchInlineSnapshot + (`"value === 9007199254740991"`) + + vi.expect(Json.generate( + +0 + )).toMatchInlineSnapshot + (`"value === +0"`) + + vi.expect(Json.generate( + -0 + )).toMatchInlineSnapshot + (`"value === -0"`) + + vi.expect(Json.generate( + 1 / 3 + )).toMatchInlineSnapshot + (`"value === 0.3333333333333333"`) + + vi.expect(Json.generate( + -1 / 3 + )).toMatchInlineSnapshot + (`"value === -0.3333333333333333"`) + + vi.expect(Json.generate( + 1e+21 + )).toMatchInlineSnapshot + (`"value === 1e+21"`) + + vi.expect(Json.generate( + -1e+21 + )).toMatchInlineSnapshot + (`"value === -1e+21"`) + + vi.expect(Json.generate( + 1e-21 + )).toMatchInlineSnapshot + (`"value === 1e-21"`) + + vi.expect(Json.generate( + -1e-21 + )).toMatchInlineSnapshot + (`"value === -1e-21"`) + }) + + vi.it('〖⛳️〗› ❲Json.generate❳: strings', () => { + vi.expect(Json.generate( + '' + )).toMatchInlineSnapshot + (`"value === """`) + + vi.expect(Json.generate( + '\\' + )).toMatchInlineSnapshot + (`"value === "\\\\""`) + }) + + vi.it('〖⛳️〗› ❲Json.generate❳: objects', () => { + vi.expect(Json.generate( + {} + )).toMatchInlineSnapshot + (`"!!value && typeof value === "object" && !Array.isArray(value)"`) + + vi.expect(Json.generate( + { + m: { o: 'O' }, + l: ['L'] + } + )).toMatchInlineSnapshot + (` + "!!value && typeof value === "object" && !Array.isArray(value) + && Array.isArray(value.l) && value.l.length === 1 + && value.l[0] === "L" + && !!value.m && typeof value.m === "object" && !Array.isArray(value.m) + && value.m.o === "O"" + `) + + }) + + vi.it('〖⛳️〗› ❲Json.generate❳: arrays', () => { + vi.expect(Json.generate( + [] + )).toMatchInlineSnapshot + (`"Array.isArray(value) && value.length === 0"`) + + vi.expect(Json.generate( + [1, 2, 3] + )).toMatchInlineSnapshot + (` + "Array.isArray(value) && value.length === 3 + && value[0] === 1 + && value[1] === 2 + && value[2] === 3" + `) + + vi.expect(Json.generate( + [[11], [22], [33]] + )).toMatchInlineSnapshot + (` + "Array.isArray(value) && value.length === 3 + && Array.isArray(value[0]) && value[0].length === 1 + && value[0][0] === 11 + && Array.isArray(value[1]) && value[1].length === 1 + && value[1][0] === 22 + && Array.isArray(value[2]) && value[2].length === 1 + && value[2][0] === 33" + `) + + vi.expect(Json.generate( + [ + { + a: 3, + b: 3, + c: [5, 6] + }, + { z: 2 }, + 1, + ] + )).toMatchInlineSnapshot + (` + "Array.isArray(value) && value.length === 3 + && value[0] === 1 + && !!value[1] && typeof value[1] === "object" && !Array.isArray(value[1]) + && value[1].z === 2 + && !!value[2] && typeof value[2] === "object" && !Array.isArray(value[2]) + && value[2].a === 3 + && value[2].b === 3 + && Array.isArray(value[2].c) && value[2].c.length === 2 + && value[2].c[0] === 5 + && value[2].c[1] === 6" + `) + + vi.expect(Json.generate( + [ + { THREE: [{ A: null, B: false }] }, + { FOUR: [{ A: 1, B: false }], C: '' }, + { TWO: [{ A: null, B: undefined }] }, + { ONE: [true] } + ] + )).toMatchInlineSnapshot + (` + "Array.isArray(value) && value.length === 4 + && !!value[0] && typeof value[0] === "object" && !Array.isArray(value[0]) + && Array.isArray(value[0].ONE) && value[0].ONE.length === 1 + && value[0].ONE[0] === true + && !!value[1] && typeof value[1] === "object" && !Array.isArray(value[1]) + && Array.isArray(value[1].TWO) && value[1].TWO.length === 1 + && !!value[1].TWO[0] && typeof value[1].TWO[0] === "object" && !Array.isArray(value[1].TWO[0]) + && value[1].TWO[0].B === undefined + && value[1].TWO[0].A === null + && !!value[2] && typeof value[2] === "object" && !Array.isArray(value[2]) + && Array.isArray(value[2].THREE) && value[2].THREE.length === 1 + && !!value[2].THREE[0] && typeof value[2].THREE[0] === "object" && !Array.isArray(value[2].THREE[0]) + && value[2].THREE[0].A === null + && value[2].THREE[0].B === false + && !!value[3] && typeof value[3] === "object" && !Array.isArray(value[3]) + && value[3].C === "" + && Array.isArray(value[3].FOUR) && value[3].FOUR.length === 1 + && !!value[3].FOUR[0] && typeof value[3].FOUR[0] === "object" && !Array.isArray(value[3].FOUR[0]) + && value[3].FOUR[0].B === false + && value[3].FOUR[0].A === 1" + `) + + let modularArithmetic = (mod: number, operator: '+' | '*') => { + let index = mod, + row = Array.of(), + col = Array.of(), + matrix = Array.of() + while (index-- !== 0) void ( + row.push(index), + col.push(index), + matrix.push(Array.from({ length: mod })) + ) + for (let i = 0; i < row.length; i++) + for (let j = 0; j < col.length; j++) + matrix[i][j] = (operator === '+' ? i + j : i * j) % mod + // + return matrix + } + + let table = modularArithmetic(5, '*') + + vi.expect(table).toMatchInlineSnapshot + (` + [ + [ + 0, + 0, + 0, + 0, + 0, + ], + [ + 0, + 1, + 2, + 3, + 4, + ], + [ + 0, + 2, + 4, + 1, + 3, + ], + [ + 0, + 3, + 1, + 4, + 2, + ], + [ + 0, + 4, + 3, + 2, + 1, + ], + ] + `) + + vi.expect(Json.generate(table)).toMatchInlineSnapshot + (` + "Array.isArray(value) && value.length === 5 + && Array.isArray(value[0]) && value[0].length === 5 + && value[0][0] === +0 + && value[0][1] === +0 + && value[0][2] === +0 + && value[0][3] === +0 + && value[0][4] === +0 + && Array.isArray(value[1]) && value[1].length === 5 + && value[1][0] === +0 + && value[1][1] === 1 + && value[1][2] === 2 + && value[1][3] === 3 + && value[1][4] === 4 + && Array.isArray(value[2]) && value[2].length === 5 + && value[2][0] === +0 + && value[2][1] === 2 + && value[2][2] === 4 + && value[2][3] === 1 + && value[2][4] === 3 + && Array.isArray(value[3]) && value[3].length === 5 + && value[3][0] === +0 + && value[3][1] === 3 + && value[3][2] === 1 + && value[3][3] === 4 + && value[3][4] === 2 + && Array.isArray(value[4]) && value[4].length === 5 + && value[4][0] === +0 + && value[4][1] === 4 + && value[4][2] === 3 + && value[4][3] === 2 + && value[4][4] === 1" + `) + + }) + + vi.it('〖⛳️〗› ❲Json.getWeight❳', () => { + vi.expect(Json.getWeight(null)).toMatchInlineSnapshot(`2`) + vi.expect(Json.getWeight(undefined)).toMatchInlineSnapshot(`1`) + vi.expect(Json.getWeight(false)).toMatchInlineSnapshot(`4`) + vi.expect(Json.getWeight(true)).toMatchInlineSnapshot(`4`) + vi.expect(Json.getWeight([true, false, ['heyy']])).toMatchInlineSnapshot(`280`) + }) + + vi.it('〖⛳️〗› ❲Json.sort❳', () => { + vi.expect(Json.sort([1, { a: 3, b: 3, c: [5, 6] }, { z: 2 }])).toMatchInlineSnapshot(` + { + "def": [ + { + "def": 1, + "tag": "@traversable/schema-core/URI::bottom", + }, + { + "def": [ + [ + "z", + { + "def": 2, + "tag": "@traversable/schema-core/URI::bottom", + }, + ], + ], + "tag": "@traversable/schema-core/URI::object", + }, + { + "def": [ + [ + "a", + { + "def": 3, + "tag": "@traversable/schema-core/URI::bottom", + }, + ], + [ + "b", + { + "def": 3, + "tag": "@traversable/schema-core/URI::bottom", + }, + ], + [ + "c", + { + "def": [ + { + "def": 5, + "tag": "@traversable/schema-core/URI::bottom", + }, + { + "def": 6, + "tag": "@traversable/schema-core/URI::bottom", + }, + ], + "tag": "@traversable/schema-core/URI::array", + }, + ], + ], + "tag": "@traversable/schema-core/URI::object", + }, + ], + "tag": "@traversable/schema-core/URI::array", + } + `) + + vi.expect(Json.sort([{ a: 2 }, { a: true }])).toMatchInlineSnapshot(` + { + "def": [ + { + "def": [ + [ + "a", + { + "def": true, + "tag": "@traversable/schema-core/URI::bottom", + }, + ], + ], + "tag": "@traversable/schema-core/URI::object", + }, + { + "def": [ + [ + "a", + { + "def": 2, + "tag": "@traversable/schema-core/URI::bottom", + }, + ], + ], + "tag": "@traversable/schema-core/URI::object", + }, + ], + "tag": "@traversable/schema-core/URI::array", + } + `) + + vi.expect(Json.sort([{ a: [[10]] }, { a: [[false]] }])).toMatchInlineSnapshot(` + { + "def": [ + { + "def": [ + [ + "a", + { + "def": [ + { + "def": [ + { + "def": false, + "tag": "@traversable/schema-core/URI::bottom", + }, + ], + "tag": "@traversable/schema-core/URI::array", + }, + ], + "tag": "@traversable/schema-core/URI::array", + }, + ], + ], + "tag": "@traversable/schema-core/URI::object", + }, + { + "def": [ + [ + "a", + { + "def": [ + { + "def": [ + { + "def": 10, + "tag": "@traversable/schema-core/URI::bottom", + }, + ], + "tag": "@traversable/schema-core/URI::array", + }, + ], + "tag": "@traversable/schema-core/URI::array", + }, + ], + ], + "tag": "@traversable/schema-core/URI::object", + }, + ], + "tag": "@traversable/schema-core/URI::array", + } + `) + }) +}) diff --git a/packages/schema-jit-compiler/test/sort.test.ts b/packages/schema-jit-compiler/test/sort.test.ts new file mode 100644 index 00000000..a500156c --- /dev/null +++ b/packages/schema-jit-compiler/test/sort.test.ts @@ -0,0 +1,63 @@ +import * as vi from 'vitest' +import { t } from '@traversable/schema-core' + +import { Jit } from '@traversable/schema-jit-compiler' + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-jit-compiler❳', () => { + vi.it('〖⛳️〗› ❲sort❳: shallow sort order is correct', () => { + vi.expect(Jit.generate( + t.object({ + [Jit.WeightByTypeName.never]: t.never, + [Jit.WeightByTypeName.any]: t.any, + [Jit.WeightByTypeName.unknown]: t.unknown, + [Jit.WeightByTypeName.void]: t.void, + [Jit.WeightByTypeName.null]: t.null, + [Jit.WeightByTypeName.undefined]: t.undefined, + [Jit.WeightByTypeName.symbol]: t.symbol, + [Jit.WeightByTypeName.boolean]: t.boolean, + [Jit.WeightByTypeName.integer]: t.integer, + [Jit.WeightByTypeName.bigint]: t.bigint, + [Jit.WeightByTypeName.number]: t.number, + [Jit.WeightByTypeName.string]: t.string, + [Jit.WeightByTypeName.eq]: t.eq({}), + [Jit.WeightByTypeName.optional]: t.optional(t.never), + [Jit.WeightByTypeName.array]: t.array(t.never), + [Jit.WeightByTypeName.record]: t.record(t.never), + [Jit.WeightByTypeName.intersect]: t.intersect(), + [Jit.WeightByTypeName.union]: t.union(), + [Jit.WeightByTypeName.tuple]: t.tuple(), + [Jit.WeightByTypeName.object]: t.object({}), + }) + )).toMatchInlineSnapshot(` + "function check(value) { + return ( + !!value && typeof value === "object" && !Array.isArray(value) + && false + && true + && true + && value["30"] === void 0 + && value["40"] === undefined + && value["50"] === null + && typeof value["60"] === "symbol" + && typeof value["70"] === "boolean" + && Number.isSafeInteger(value["80"]) + && typeof value["90"] === "bigint" + && Number.isFinite(value["100"]) + && typeof value["110"] === "string" + && (value["120"] === undefined || false) + && (true) + && (false) + && Array.isArray(value["150"]) && value["150"].length === 0 + && !!value["160"] && typeof value["160"] === "object" && !Array.isArray(value["160"]) + && Array.isArray(value["170"]) && value["170"].every((value) => false) + && !!value["180"] && typeof value["180"] === "object" && !Array.isArray(value["180"]) + && !(value["180"] instanceof Date) && !(value["180"] instanceof Uint8Array) + && Object.entries(value["180"]).every(([key, value]) => + typeof key === "string" && false + ) + && !!value["190"] && typeof value["190"] === "object" && !Array.isArray(value["190"]) + ) + }" + `) + }) +}) diff --git a/packages/schema-jit-compiler/test/version.test.ts b/packages/schema-jit-compiler/test/version.test.ts new file mode 100644 index 00000000..ef4ebea5 --- /dev/null +++ b/packages/schema-jit-compiler/test/version.test.ts @@ -0,0 +1,10 @@ +import * as vi from 'vitest' +import pkg from '../package.json' with { type: 'json' } +import { VERSION } from '@traversable/schema-jit-compiler' + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-jit-compiler❳', () => { + vi.it('〖⛳️〗› ❲schemaJitCompiler#VERSION❳', () => { + const expected = `${pkg.name}@${pkg.version}` + vi.assert.equal(VERSION, expected) + }) +}) diff --git a/packages/schema-jit-compiler/tsconfig.build.json b/packages/schema-jit-compiler/tsconfig.build.json new file mode 100644 index 00000000..18db6e09 --- /dev/null +++ b/packages/schema-jit-compiler/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "types": ["node"], + "declarationDir": "build/dts", + "outDir": "build/esm", + "stripInternal": true + }, + "references": [ + { "path": "../registry" }, + { "path": "../schema-core" }, + { "path": "../schema-seed" } + ] +} diff --git a/packages/schema-jit-compiler/tsconfig.json b/packages/schema-jit-compiler/tsconfig.json new file mode 100644 index 00000000..2c291d21 --- /dev/null +++ b/packages/schema-jit-compiler/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/packages/schema-jit-compiler/tsconfig.src.json b/packages/schema-jit-compiler/tsconfig.src.json new file mode 100644 index 00000000..6042e159 --- /dev/null +++ b/packages/schema-jit-compiler/tsconfig.src.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "types": ["node"], + "outDir": "build/src" + }, + "references": [ + { "path": "../registry" }, + { "path": "../schema-core" }, + { "path": "../schema-seed" } + ], + "include": ["src"] +} diff --git a/packages/schema-jit-compiler/tsconfig.test.json b/packages/schema-jit-compiler/tsconfig.test.json new file mode 100644 index 00000000..df338a8c --- /dev/null +++ b/packages/schema-jit-compiler/tsconfig.test.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "types": ["node"], + "noEmit": true + }, + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../registry" }, + { "path": "../schema-arbitrary" }, + { "path": "../schema-core" }, + { "path": "../schema-seed" } + ], + "include": ["test"] +} diff --git a/packages/schema-jit-compiler/vite.config.ts b/packages/schema-jit-compiler/vite.config.ts new file mode 100644 index 00000000..64dba4ad --- /dev/null +++ b/packages/schema-jit-compiler/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import sharedConfig from '../../vite.config.js' + +const localConfig = defineConfig({}) + +export default mergeConfig(sharedConfig, localConfig) \ No newline at end of file diff --git a/packages/schema-seed/package.json b/packages/schema-seed/package.json index 9c20bc07..6192917b 100644 --- a/packages/schema-seed/package.json +++ b/packages/schema-seed/package.json @@ -46,12 +46,12 @@ "peerDependencies": { "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^" + "@traversable/schema-core": "workspace:^" }, "devDependencies": { "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "fast-check": "^3.23.2" } } diff --git a/packages/schema-seed/src/__generated__/__manifest__.ts b/packages/schema-seed/src/__generated__/__manifest__.ts index 865c3a51..2f70aee8 100644 --- a/packages/schema-seed/src/__generated__/__manifest__.ts +++ b/packages/schema-seed/src/__generated__/__manifest__.ts @@ -42,12 +42,12 @@ export default { "peerDependencies": { "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^" + "@traversable/schema-core": "workspace:^" }, "devDependencies": { "@traversable/json": "workspace:^", "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "fast-check": "^3.23.2" } } as const \ No newline at end of file diff --git a/packages/schema-seed/src/arbitrary.ts b/packages/schema-seed/src/arbitrary.ts index b6a4ea25..2dc81efb 100644 --- a/packages/schema-seed/src/arbitrary.ts +++ b/packages/schema-seed/src/arbitrary.ts @@ -1,7 +1,7 @@ import type * as T from '@traversable/registry' import { fn, has, URI } from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import { isSeed, isNullary } from './seed.js' import * as Seed from './seed.js' diff --git a/packages/schema-seed/src/exports.ts b/packages/schema-seed/src/exports.ts index d73e33c8..8eae9134 100644 --- a/packages/schema-seed/src/exports.ts +++ b/packages/schema-seed/src/exports.ts @@ -5,4 +5,4 @@ export type Seed = [T] extends [never] ? import('./seed.js').Fixpoint : import('./seed.js').Seed -export * as Arbitrary from './arbitrary.js' +// export * as Arbitrary from './arbitrary.js' diff --git a/packages/schema-seed/src/fast-check.ts b/packages/schema-seed/src/fast-check.ts index fd63d257..783cfda8 100644 --- a/packages/schema-seed/src/fast-check.ts +++ b/packages/schema-seed/src/fast-check.ts @@ -1,9 +1,7 @@ export * from 'fast-check' import * as fc from 'fast-check' -import type { Force } from '@traversable/registry' import { symbol as Symbol } from '@traversable/registry' -import type { Guard } from '@traversable/schema' export interface Arbitrary extends fc.Arbitrary { readonly [Symbol.optional]?: true @@ -12,25 +10,6 @@ export interface Arbitrary extends fc.Arbitrary { export type { typeOf as typeof } type typeOf = S extends fc.Arbitrary ? T : never -/** @internal */ -const Object_keys = globalThis.Object.keys -/** @internal */ -const Array_isArray = globalThis.Array.isArray -/** @internal */ -const isString: Guard = (u): u is never => typeof u === 'string' -/** @internal */ -const arrayOf - : (p: Guard) => Guard - = (p) => (u): u is never => Array_isArray(u) && u.every(p) -/** @internal */ -const has - : (k: K, p: Guard) => Guard<{ [P in K]: T }> - = (k, p) => (u: unknown): u is never => - !!u && - typeof u === 'object' && - Object.hasOwn(u, k) && - p(u[k as never]) - const PATTERN = { identifier: /^[$_a-zA-Z][$_a-zA-Z0-9]*$/, } as const diff --git a/packages/schema-seed/src/seed.ts b/packages/schema-seed/src/seed.ts index 471cdeb8..c5134f88 100644 --- a/packages/schema-seed/src/seed.ts +++ b/packages/schema-seed/src/seed.ts @@ -3,13 +3,15 @@ * the generated schemas are more likely to be "deeper" without risk of stack overflow */ +import * as fc from 'fast-check' + import type * as T from '@traversable/registry' import { fn, parseKey, unsafeCompact, URI } from '@traversable/registry' import { Json } from '@traversable/json' -import type { SchemaOptions } from '@traversable/schema' -import { t } from '@traversable/schema' +import type { SchemaOptions } from '@traversable/schema-core' +import { t } from '@traversable/schema-core' -import * as fc from './fast-check.js' +// import * as fc from './fast-check.js' export { type Arbitraries, @@ -84,6 +86,60 @@ const isComposite = (u: unknown) => Array_isArray(u) || (u !== null && typeof u /** @internal */ const isNumeric = t.union(t.number, t.bigint) +export type UniqueArrayDefaults = fc.UniqueArrayConstraintsRecommended + +let identifier = fc.stringMatching(new RegExp('^[$_a-zA-Z][$_a-zA-Z0-9]*$', 'u')) + +let entries = (model: fc.Arbitrary, constraints?: UniqueArrayDefaults) => fc.uniqueArray( + fc.tuple( + identifier, + model), + { ...constraints, selector: ([k]) => k } +) + + +declare namespace InferSchema { + type SchemaMap = { + [URI.never]: t.never + [URI.any]: t.any + [URI.unknown]: t.unknown + [URI.void]: t.void + [URI.null]: t.null + [URI.undefined]: t.undefined + [URI.boolean]: t.boolean + [URI.symbol]: t.symbol + [URI.integer]: t.integer + [URI.bigint]: t.bigint + [URI.number]: t.number + [URI.string]: t.string + [URI.eq]: t.eq + [URI.array]: t.array + [URI.optional]: t.optional + [URI.record]: t.record + [URI.union]: t.union + [URI.intersect]: t.intersect + [URI.tuple]: t.tuple + [URI.object]: t.object + } + type LookupSchema = SchemaMap[(T extends Boundable ? T[0] : T) & keyof SchemaMap] + type CatchUnknown = unknown extends T ? SchemaMap[keyof SchemaMap] : T + type fromFixpoint = CatchUnknown< + T extends { 0: infer Head, 1: infer Tail } + ? [Head, Tail] extends [[URI.integer] | [URI.integer, any], any] ? t.integer + : [Head, Tail] extends [URI.eq, any] ? t.eq + : [Head, Tail] extends [URI.optional, Fixpoint] ? t.optional> + : [Head, Tail] extends [URI.array, Fixpoint] ? t.array> + : [Head, Tail] extends [URI.record, Fixpoint] ? t.record> + : [Head, Tail] extends [URI.union, Fixpoint[]] ? t.union<{ [I in keyof Tail]: LookupSchema }> + : [Head, Tail] extends [URI.intersect, Fixpoint[]] ? t.intersect<{ [I in keyof Tail]: LookupSchema }> + : [Head, Tail] extends [URI.tuple, Fixpoint[]] ? t.tuple<{ [I in keyof Tail]: LookupSchema }> + : [Head, Tail] extends [URI.object, infer Entries extends [k: string, v: any][]] ? t.object<{ [E in Entries[number]as E[0]]: LookupSchema }> + : LookupSchema + : unknown + > +} + + /** * If you provide a partial weight map, missing properties will fall back to `0` */ @@ -597,7 +653,7 @@ const NullarySchemaMap = { [URI.null]: t.null, [URI.undefined]: t.undefined, [URI.boolean]: t.boolean, -} as const satisfies Record +} as const satisfies Record const BoundableSchemaMap = { [URI.integer]: (bounds) => { @@ -648,7 +704,7 @@ const NullaryArbitraryMap = { [URI.null]: fc.constant(null), [URI.undefined]: fc.constant(undefined), [URI.boolean]: fc.boolean(), -} as const satisfies Record +} as const satisfies Record> const integerConstraintsFromBounds = (bounds: InclusiveBounds = {}) => { const { @@ -871,10 +927,10 @@ namespace Recursive { } } - export const toSchema: T.Functor.Algebra = (x) => { + export const toSchema: T.Functor.Algebra = (x) => { if (!isSeed(x)) return x // fn.exhaustive(x) switch (true) { - default: return fn.exhaustive(x) + default: return x // fn.exhaustive(x) case isNullary(x): return NullarySchemaMap[x] case x[0] === URI.array: return BoundableSchemaMap[x[0]](x[2], x[1]) case isBoundable(x): return BoundableSchemaMap[x[0]](x[1] as never) @@ -904,7 +960,7 @@ namespace Recursive { } } - export const toArbitrary: T.Functor.Algebra = (x) => { + export const toArbitrary: T.Functor.Algebra> = (x) => { if (!isSeed(x)) return fn.exhaustive(x) switch (true) { default: return fn.exhaustive(x) @@ -912,8 +968,8 @@ namespace Recursive { case isBoundable(x): return BoundableArbitraryMap[x[0]](x[1] as never) case x[0] === URI.eq: return fc.constant(x[1]) case x[0] === URI.array: return BoundableArbitraryMap[x[0]](x[1], x[2]) - case x[0] === URI.record: return fc.dictionary(fc.identifier(), x[1]) - case x[0] === URI.optional: return fc.optional(x[1]) + case x[0] === URI.record: return fc.dictionary(identifier, x[1]) + case x[0] === URI.optional: return fc.option(x[1], { nil: undefined }) case x[0] === URI.tuple: return fc.tuple(...x[1]) case x[0] === URI.union: return fc.oneof(...x[1]) case x[0] === URI.object: { @@ -1208,9 +1264,9 @@ const Unaries = { eq: (fix: fc.Arbitrary, _: TargetConstraints) => fix.chain(() => fc.jsonValue()).map(eqF), array: (fix: fc.Arbitrary, $: TargetConstraints) => fc.tuple(fix, arrayBounds).map(([def, bounds]) => arrayF(def, bounds)), record: (fix: fc.Arbitrary, _: TargetConstraints) => fix.map(recordF), - optional: (fix: fc.Arbitrary, _: TargetConstraints) => fc.optional(fix).map(optionalF), + optional: (fix: fc.Arbitrary, _: TargetConstraints) => fix.map(optionalF), tuple: (fix: fc.Arbitrary, $: TargetConstraints) => fc.array(fix, $.tuple).map(fn.flow((_) => _.sort(sortSeedOptionalsLast), tupleF)), - object: (fix: fc.Arbitrary, $: TargetConstraints) => fc.entries(fix, $.object).map(objectF), + object: (fix: fc.Arbitrary, $: TargetConstraints) => entries(fix, $.object).map(objectF), union: (fix: fc.Arbitrary, $: TargetConstraints) => fc.array(fix, $.union).map(unionF), intersect: (fix: fc.Arbitrary, $: TargetConstraints) => fc.array(fix, $.intersect).map(intersectF), } @@ -1352,7 +1408,7 @@ const minDepth = { record: (seeds: Seeds[], _: TargetConstraints) => fc.oneof(...seeds).map(recordF), optional: (seeds: Seeds[], $: TargetConstraints) => fc.oneof(...seeds).map(optionalF), object: (seeds: Seeds[], $: TargetConstraints) => - fc.array(fc.tuple(fc.identifier(), fc.oneof(...seeds)), { maxLength: $.object.maxLength, minLength: $.object.minLength }).map(objectF), + fc.array(fc.tuple(identifier, fc.oneof(...seeds)), { maxLength: $.object.maxLength, minLength: $.object.minLength }).map(objectF), tuple: (seeds: Seeds[], $: TargetConstraints) => fc.array(fc.oneof(...seeds), { minLength: $.tuple.minLength, maxLength: $.tuple.maxLength }).map(tupleF), union: (seeds: Seeds[], $: TargetConstraints) => @@ -1381,7 +1437,7 @@ function schemaWithMinDepth( let seed = fc.letrec(seedWithChain($)) let seeds = Object.values(seed) let branches = minDepthBranchOrder.filter(((_) => $.include.includes(_ as never) && !$.exclude.includes(_ as never))) - let arb: fc.Arbitrary = seed.tree + let arb = seed.tree while (n-- >= 0) arb = fc.nat(branches.length - 1).chain( (x): fc.Arbitrary< @@ -1395,23 +1451,24 @@ function schemaWithMinDepth( > => { switch (true) { default: return fn.exhaustive(x as never) - case x === 0: return minDepths[x](seeds, $) - case x === 1: return minDepths[x](seeds, $) - case x === 2: return minDepths[x](seeds, $) - case x === 3: return minDepths[x](seeds, $) - case x === 4: return minDepths[x](seeds, $) - case x === 5: return minDepths[x](seeds, $) - case x === 6: return minDepths[x](seeds, $) + case x === 0: return minDepths[x](seeds as never, $) + case x === 1: return minDepths[x](seeds as never, $) + case x === 2: return minDepths[x](seeds as never, $) + case x === 3: return minDepths[x](seeds as never, $) + case x === 4: return minDepths[x](seeds as never, $) + case x === 5: return minDepths[x](seeds as never, $) + case x === 6: return minDepths[x](seeds as never, $) } }); - return arb.map(toSchema) + return arb.map(toSchema as never) } const identity = fold(Recursive.identity) // ^? -const toSchema = fold(Recursive.toSchema) -// ^? +const toSchema + : (fixpoint: T) => InferSchema.fromFixpoint + = fold(Recursive.toSchema) as never const toArbitrary = fold(Recursive.toArbitrary) // ^? @@ -1443,11 +1500,11 @@ function schema< fc.Arbitrary, { tag: `${T.NS}${Exclude}` }>> function schema(constraints?: Constraints): fc.Arbitrary -function schema(constraints?: Constraints) { - return fc.letrec(seed(constraints as never)).tree.map(toSchema) as never +function schema(constraints?: Constraints): {} { + return fc.letrec(seed(constraints as never)).tree.map(toSchema) } -const extensibleArbitrary = (constraints?: Constraints) => +const extensibleArbitrary = (constraints?: Constraints) => fc.letrec(seed(constraints)).tree.map(fold(Recursive.toExtensibleSchema(constraints?.arbitraries))) /** diff --git a/packages/schema-seed/test/seed.test.ts b/packages/schema-seed/test/seed.test.ts index 4a5522fe..5205236d 100644 --- a/packages/schema-seed/test/seed.test.ts +++ b/packages/schema-seed/test/seed.test.ts @@ -2,13 +2,13 @@ import * as vi from 'vitest' import { fc, test } from '@fast-check/vitest' import { URI } from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import { Seed } from '@traversable/schema-seed' /** @internal */ const builder = fc.letrec(Seed.seed()) -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema-seed❳', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-seed❳', () => { vi.it('〖⛳️〗› ❲Seed.laxMin❳', () => { vi.assert.equal(Seed.laxMin(), void 0) vi.assert.equal(Seed.laxMin(1), 1) @@ -32,7 +32,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema-seed❳', () => { }) }) -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema-seed❳: property tests', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-seed❳: property tests', () => { vi.it('〖⛳️〗› ❲Seed.isBoundable❳', () => { vi.assert.isTrue(Seed.isBoundable([URI.integer])) vi.assert.isTrue(Seed.isBoundable([URI.bigint])) @@ -45,7 +45,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema-seed❳: property tests }) }) -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema-seed❳', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-seed❳', () => { vi.it('〖⛳️〗› ❲Seed.stringContraintsFromBounds❳', () => { vi.assert.deepEqual( Seed.stringConstraintsFromBounds({ minimum: 250, maximum: 250 }), @@ -262,7 +262,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema-seed❳', () => { }) -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema-seed❳: property tests', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-seed❳: property tests', () => { test.prop([builder.number], { // numRuns: 10_000, endOnFailure: true, @@ -390,7 +390,9 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema-seed❳: property tests ) }) -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema/seed❳: Seed.toSchema', () => { +let xs = Seed.toSchema(URI.never) + +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema/seed❳: Seed.toSchema', () => { vi.it('〖⛳️〗› ❲t.never❳', () => { vi.assert.deepEqual(Seed.toSchema(URI.never), t.never) }) @@ -444,6 +446,9 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema/seed❳: Seed.toSchema' vi.assert.equal((Seed.toSchema([URI.string, { minimum: 1, maximum: 2 }]) as t.string).minLength, 1) vi.assert.equal((Seed.toSchema([URI.string, { minimum: 1, maximum: 2 }]) as t.string).maxLength, 2) }) + + let zss = Seed.toSchema([URI.optional, URI.any]) + vi.it('〖⛳️〗› ❲t.optional(...)❳', () => { vi.assert.equal(Seed.toSchema([URI.optional, URI.any]).tag, URI.optional) vi.assert.equal((Seed.toSchema([URI.optional, URI.any]).def as t.any).tag, URI.any) @@ -461,10 +466,10 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema/seed❳: Seed.toSchema' vi.assert.deepEqual(Seed.toSchema([URI.eq, [0]]).def, [0]) }) vi.it('〖⛳️〗› ❲t.array(...)❳', () => { - vi.assert.equal((Seed.toSchema([URI.array, URI.any, { minimum: 1 }]) as t.array).minLength, 1) - vi.assert.equal((Seed.toSchema([URI.array, URI.any, { maximum: 2 }]) as t.array).maxLength, 2) - vi.assert.equal((Seed.toSchema([URI.array, URI.any, { minimum: 1, maximum: 2 }]) as t.array).minLength, 1) - vi.assert.equal((Seed.toSchema([URI.array, URI.any, { minimum: 1, maximum: 2 }]) as t.array).maxLength, 2) + vi.assert.equal((Seed.toSchema([URI.array, URI.any, { minimum: 1 }])).minLength, 1) + vi.assert.equal((Seed.toSchema([URI.array, URI.any, { maximum: 2 }])).maxLength, 2) + vi.assert.equal((Seed.toSchema([URI.array, URI.any, { minimum: 1, maximum: 2 }])).minLength, 1) + vi.assert.equal((Seed.toSchema([URI.array, URI.any, { minimum: 1, maximum: 2 }])).maxLength, 2) }) vi.it('〖⛳️〗› ❲t.record(...)❳', () => { vi.assert.equal(Seed.toSchema([URI.record, URI.any]).tag, URI.record) @@ -495,7 +500,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema/seed❳: Seed.toSchema' }) }) -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema/seed❳: Seed.fromSchema', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema/seed❳: Seed.fromSchema', () => { vi.it('〖⛳️〗› ❲t.never❳', () => { vi.assert.deepEqual(Seed.fromSchema(t.never), URI.never) }) @@ -596,7 +601,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema/seed❳: Seed.fromSchem }) }) -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema/seed❳: example-based tests', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema/seed❳: example-based tests', () => { vi.it('〖⛳️〗› ❲Seed.toJson❳', () => { vi.assert.isNull(Seed.toJson([URI.eq, URI.null]), URI.null) vi.assert.isUndefined(Seed.toJson([URI.eq, URI.any])) diff --git a/packages/schema-seed/tsconfig.build.json b/packages/schema-seed/tsconfig.build.json index e36720e1..7af09c34 100644 --- a/packages/schema-seed/tsconfig.build.json +++ b/packages/schema-seed/tsconfig.build.json @@ -10,6 +10,6 @@ "references": [ { "path": "../json" }, { "path": "../registry" }, - { "path": "../schema" } + { "path": "../schema-core" } ] } diff --git a/packages/schema-seed/tsconfig.src.json b/packages/schema-seed/tsconfig.src.json index 076cde65..a021e790 100644 --- a/packages/schema-seed/tsconfig.src.json +++ b/packages/schema-seed/tsconfig.src.json @@ -9,7 +9,7 @@ "references": [ { "path": "../json" }, { "path": "../registry" }, - { "path": "../schema" } + { "path": "../schema-core" } ], "include": ["src"] } diff --git a/packages/schema-seed/tsconfig.test.json b/packages/schema-seed/tsconfig.test.json index 5de93ab0..a0d75975 100644 --- a/packages/schema-seed/tsconfig.test.json +++ b/packages/schema-seed/tsconfig.test.json @@ -10,7 +10,7 @@ { "path": "tsconfig.src.json" }, { "path": "../json" }, { "path": "../registry" }, - { "path": "../schema" } + { "path": "../schema-core" } ], "include": ["test"] } diff --git a/packages/schema-to-json-schema/package.json b/packages/schema-to-json-schema/package.json index d386a264..40ef0290 100644 --- a/packages/schema-to-json-schema/package.json +++ b/packages/schema-to-json-schema/package.json @@ -20,7 +20,8 @@ "@traversable": { "generateExports": { "include": [ - "**/*.ts" + "**/*.ts", + "schemas/*.ts" ] }, "generateIndex": { @@ -48,11 +49,11 @@ }, "peerDependencies": { "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^" + "@traversable/schema-core": "workspace:^" }, "devDependencies": { "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "@traversable/schema-seed": "workspace:^" } } diff --git a/packages/schema-to-json-schema/src/__generated__/__manifest__.ts b/packages/schema-to-json-schema/src/__generated__/__manifest__.ts index 17196ac3..49479959 100644 --- a/packages/schema-to-json-schema/src/__generated__/__manifest__.ts +++ b/packages/schema-to-json-schema/src/__generated__/__manifest__.ts @@ -17,7 +17,7 @@ export default { }, "@traversable": { "generateExports": { - "include": ["**/*.ts"] + "include": ["**/*.ts", "schemas/*.ts"] }, "generateIndex": { "include": ["**/*.ts"] @@ -42,11 +42,11 @@ export default { }, "peerDependencies": { "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^" + "@traversable/schema-core": "workspace:^" }, "devDependencies": { "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "@traversable/schema-seed": "workspace:^" } } as const \ No newline at end of file diff --git a/packages/schema-to-json-schema/src/exports.ts b/packages/schema-to-json-schema/src/exports.ts index aebc231a..d204e630 100644 --- a/packages/schema-to-json-schema/src/exports.ts +++ b/packages/schema-to-json-schema/src/exports.ts @@ -2,5 +2,15 @@ import * as JsonSchema from './jsonSchema.js' type JsonSchema = import('./jsonSchema.js').JsonSchema export { JsonSchema } +export { + applyTupleOptionality, + getNumericBounds, +} from './jsonSchema.js' + export { toJsonSchema, fromJsonSchema } from './recursive.js' export { VERSION } from './version.js' +export type { RequiredKeys } from './properties.js' +export { hasSchema, getSchema, isRequired, property, wrapOptional } from './properties.js' +export type { MinItems } from './items.js' +export { minItems } from './items.js' +export type * from './specification.js' diff --git a/packages/schema-to-json-schema/src/install.ts b/packages/schema-to-json-schema/src/install.ts index a3b7771c..498e17d4 100644 --- a/packages/schema-to-json-schema/src/install.ts +++ b/packages/schema-to-json-schema/src/install.ts @@ -1,19 +1,21 @@ -import { t } from '@traversable/schema' +import { Object_assign } from '@traversable/registry' +import { t } from '@traversable/schema-core' + import * as JsonSchema from './jsonSchema.js' -import * as prototypes from './prototypes.js' +import * as toJsonSchema from './prototypes.js' -declare module '@traversable/schema' { +declare module '@traversable/schema-core' { interface t_LowerBound extends JsonSchema.LowerBound { } - interface t_never extends JsonSchema.never { } + interface t_void extends JsonSchema.Empty { } + interface t_undefined extends JsonSchema.Empty { } + interface t_symbol extends JsonSchema.Empty { } + interface t_bigint extends JsonSchema.Empty { } + // interface t_never extends JsonSchema.never { } interface t_unknown extends JsonSchema.unknown { } - interface t_void extends JsonSchema.void { } interface t_any extends JsonSchema.any { } interface t_null extends JsonSchema.null { } - interface t_undefined extends JsonSchema.undefined { } - interface t_symbol extends JsonSchema.symbol { } interface t_boolean extends JsonSchema.boolean { } interface t_integer extends JsonSchema.integer { } - interface t_bigint extends JsonSchema.bigint { } interface t_number extends JsonSchema.number { } interface t_string extends JsonSchema.string { } interface t_eq extends JsonSchema.eq { } @@ -33,10 +35,7 @@ void bind() /// /// INSTALL /// ///////////////// - export function bind() { - /** @internal */ - let Object_assign = globalThis.Object.assign /** no JSON schema representation */ Object_assign(t.never, { toJsonSchema: JsonSchema.empty }) Object_assign(t.void, { toJsonSchema: JsonSchema.empty }) @@ -44,20 +43,20 @@ export function bind() { Object_assign(t.symbol, { toJsonSchema: JsonSchema.empty }) Object_assign(t.bigint, { toJsonSchema: JsonSchema.empty }) /** nullary */ - Object_assign(t.unknown, { toJsonSchema: prototypes.unknown }) - Object_assign(t.any, { toJsonSchema: prototypes.any }) - Object_assign(t.null, { toJsonSchema: prototypes.null }) - Object_assign(t.boolean, { toJsonSchema: prototypes.boolean }) - Object_assign(t.integer, { toJsonSchema: prototypes.integer }) - Object_assign(t.number, { toJsonSchema: prototypes.number }) - Object_assign(t.string, { toJsonSchema: prototypes.string }) + Object_assign(t.unknown, { toJsonSchema: toJsonSchema.unknown }) + Object_assign(t.any, { toJsonSchema: toJsonSchema.any }) + Object_assign(t.null, { toJsonSchema: toJsonSchema.null }) + Object_assign(t.boolean, { toJsonSchema: toJsonSchema.boolean }) + Object_assign(t.integer, { toJsonSchema: toJsonSchema.integer }) + Object_assign(t.number, { toJsonSchema: toJsonSchema.number }) + Object_assign(t.string, { toJsonSchema: toJsonSchema.string }) /** unary */ - Object_assign(t.eq.prototype, { toJsonSchema: prototypes.eq }) - Object_assign(t.optional.prototype, { toJsonSchema: prototypes.optional }) - Object_assign(t.array.prototype, { toJsonSchema: prototypes.array }) - Object_assign(t.record.prototype, { toJsonSchema: prototypes.record }) - Object_assign(t.union.prototype, { toJsonSchema: prototypes.union }) - Object_assign(t.intersect.prototype, { toJsonSchema: prototypes.intersect }) - Object_assign(t.tuple.prototype, { toJsonSchema: prototypes.tuple }) - Object_assign(t.object.prototype, { toJsonSchema: prototypes.object }) + Object_assign(t.eq.userDefinitions, { toJsonSchema: toJsonSchema.eq }) + Object_assign(t.optional.userDefinitions, { toJsonSchema: toJsonSchema.optional }) + Object_assign(t.array.userDefinitions, { toJsonSchema: toJsonSchema.array }) + Object_assign(t.record.userDefinitions, { toJsonSchema: toJsonSchema.record }) + Object_assign(t.union.userDefinitions, { toJsonSchema: toJsonSchema.union }) + Object_assign(t.intersect.userDefinitions, { toJsonSchema: toJsonSchema.intersect }) + Object_assign(t.tuple.userDefinitions, { toJsonSchema: toJsonSchema.tuple }) + Object_assign(t.object.userDefinitions, { toJsonSchema: toJsonSchema.object }) } diff --git a/packages/schema-to-json-schema/src/items.ts b/packages/schema-to-json-schema/src/items.ts index 9e2d1408..9285614e 100644 --- a/packages/schema-to-json-schema/src/items.ts +++ b/packages/schema-to-json-schema/src/items.ts @@ -11,7 +11,8 @@ export type MinItems< T, U = { [I in keyof T]: T[I] extends optional ? I : never }, V = Extract, -> = [V] extends [never] ? T['length' & keyof T] : IndexOfFirstOptional +> = [V] extends [never] ? T['length' & keyof T] + : IndexOfFirstOptional export function minItems>(xs: T): Min export function minItems(xs: unknown[]): number { diff --git a/packages/schema-to-json-schema/src/jsonSchema.ts b/packages/schema-to-json-schema/src/jsonSchema.ts index 2d9c7334..b79d46b7 100644 --- a/packages/schema-to-json-schema/src/jsonSchema.ts +++ b/packages/schema-to-json-schema/src/jsonSchema.ts @@ -1,18 +1,18 @@ -import type { Force, PickIfDefined, Returns } from '@traversable/registry' -import { fn, has, symbol as Sym } from '@traversable/registry' +import type { Force, Mut, PickIfDefined, Returns, NonUnion } from '@traversable/registry' +import { fn, has, symbol } from '@traversable/registry' import type { MinItems } from './items.js' import { minItems } from './items.js' import type { RequiredKeys } from './properties.js' import { getSchema, isRequired, property, wrapOptional } from './properties.js' import * as Spec from './specification.js' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' export type { - Unary, Free, Nullary, NumericBounds, + Unary, } from './specification.js' export { is, @@ -34,6 +34,7 @@ export { minItems } from './items.js' export type { + Inductive, JsonSchema, LowerBound, Schema, @@ -47,20 +48,21 @@ const isNumber = (u: unknown): u is number => typeof u === 'number' type Nullable = Force -const getNumericBounds = (u: unknown): Spec.NumericBounds => ({ +export const getNumericBounds = (u: unknown): Spec.NumericBounds => ({ ...has('minimum', t.number)(u) && { minimum: u.minimum }, ...has('maximum', t.number)(u) && { maximum: u.maximum }, ...has('exclusiveMinimum', t.number)(u) && { exclusiveMinimum: u.exclusiveMinimum }, ...has('exclusiveMaximum', t.number)(u) && { exclusiveMaximum: u.exclusiveMaximum }, }) +type Inductive> = [_] extends [never] ? JsonSchema : Mut type JsonSchema = [T] extends [never] ? Spec.JsonSchema : Spec.Unary type StringBounds = Force<{ type: 'string' } & PickIfDefined> type NumberBounds = Force<{ type: 'number' } & PickIfDefined> type IntegerBounds = Force<{ type: 'integer' } & PickIfDefined> type ArrayBounds = Force<{ type: 'array', items: Returns } & PickIfDefined> -function applyTupleOptionality(xs: readonly unknown[], { min, max }: { min: number, max: number }): readonly unknown[] { +export function applyTupleOptionality(xs: readonly unknown[], { min, max }: { min: number, max: number }): readonly unknown[] { return min === max ? xs.map(getSchema) : [ ...xs.slice(0, min).map(getSchema), ...xs.slice(min).map(getSchema), @@ -79,7 +81,7 @@ interface Schema { toJsonSchema?(): any } export { Empty as never, Empty as void, - Empty as symbol, + Empty, Empty as undefined, Empty as bigint, } @@ -159,7 +161,7 @@ export function eq(value: V) { return { const: value } } export interface optional { toJsonSchema: { - [Sym.optional]: number + [symbol.optional]: number (): Nullable> } } @@ -167,14 +169,14 @@ export interface optional { export function optional(x: T): optional export function optional(x: unknown) { function toJsonSchema() { return getSchema(x) } - toJsonSchema[Sym.optional] = wrapOptional(x) + toJsonSchema[symbol.optional] = wrapOptional(x) return { toJsonSchema, } } export function optionalProto(child: T) { - (optionalProto as any)[Sym.optional] = wrapOptional(child) + (optionalProto as any)[symbol.optional] = wrapOptional(child) return { ...getSchema(child), nullable: true diff --git a/packages/schema-to-json-schema/src/properties.ts b/packages/schema-to-json-schema/src/properties.ts index bd69b5a0..aec4294f 100644 --- a/packages/schema-to-json-schema/src/properties.ts +++ b/packages/schema-to-json-schema/src/properties.ts @@ -1,31 +1,32 @@ -import { has, symbol as Sym } from '@traversable/registry' +import { has, symbol } from '@traversable/registry' +export { symbol } from '@traversable/registry' export type RequiredKeys< T, _K extends keyof T = keyof T, - _Req = _K extends _K ? T[_K]['toJsonSchema' & keyof T[_K]] extends { [Sym.optional]: number } ? never : _K : never + _Req = _K extends _K ? T[_K]['toJsonSchema' & keyof T[_K]] extends { [symbol.optional]: number } ? never : _K : never > = [_Req] extends [never] ? [] : _Req[] export const hasSchema = has('toJsonSchema', (u) => typeof u === 'function') export const getSchema = (u: T) => hasSchema(u) ? u.toJsonSchema() : u export const isRequired = (v: { [x: string]: unknown }) => (k: string) => { - if (has('toJsonSchema', Sym.optional, (x) => typeof x === 'number')(v[k]) && v[k].toJsonSchema[Sym.optional] !== 0) return false - else if (has(Sym.optional, (x) => typeof x === 'number')(v[k]) && v[k][Sym.optional] !== 0) return false + if (has('toJsonSchema', symbol.optional, (x) => typeof x === 'number')(v[k]) && v[k].toJsonSchema[symbol.optional] !== 0) return false + else if (has(symbol.optional, (x) => typeof x === 'number')(v[k]) && v[k][symbol.optional] !== 0) return false else return true } export function wrapOptional(x: unknown) { - return has(Sym.optional, (u) => typeof u === 'number')(x) - ? x[Sym.optional] + 1 + return has(symbol.optional, (u) => typeof u === 'number')(x) + ? x[symbol.optional] + 1 : 1 } export function unwrapOptional(x: unknown) { - if (has(Sym.optional, (u) => typeof u === 'number')(x)) { - const opt = x[Sym.optional] - if (opt === 1) delete (x as Partial)[Sym.optional] - else x[Sym.optional]-- + if (has(symbol.optional, (u) => typeof u === 'number')(x)) { + const opt = x[symbol.optional] + if (opt === 1) delete (x as Partial)[symbol.optional] + else x[symbol.optional]-- } return getSchema(x) } diff --git a/packages/schema-to-json-schema/src/prototypes.ts b/packages/schema-to-json-schema/src/prototypes.ts index c056671b..c23a2f6e 100644 --- a/packages/schema-to-json-schema/src/prototypes.ts +++ b/packages/schema-to-json-schema/src/prototypes.ts @@ -1,8 +1,9 @@ +import type { Returns } from '@traversable/registry' + import * as Spec from './specification.js' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import * as JsonSchema from './jsonSchema.js' -import { symbol as Sym } from '@traversable/registry' export { toJsonSchemaAny as any, @@ -35,7 +36,13 @@ function toJsonSchemaRecord(this: t.record) { return Json function toJsonSchemaUnion(this: t.union) { return JsonSchema.union(this.def) } function toJsonSchemaIntersect(this: t.intersect) { return JsonSchema.intersect(this.def) } function toJsonSchemaTuple(this: t.tuple) { return JsonSchema.tuple(this.def) } -function toJsonSchemaObject(this: t.object) { return JsonSchema.object(this.def) } + +function toJsonSchemaObject>(this: t.object): { + type: 'object' + required: { [I in keyof KS]: KS[I] & string } + properties: { [K in keyof S]: Returns } +} { return JsonSchema.object(this.def) } + function toJsonSchemaArray(this: t.array) { return JsonSchema.array(this.def, { minLength: this.minLength, maxLength: this.maxLength }) } diff --git a/packages/schema-to-json-schema/src/recursive.ts b/packages/schema-to-json-schema/src/recursive.ts index 3623d323..36afced7 100644 --- a/packages/schema-to-json-schema/src/recursive.ts +++ b/packages/schema-to-json-schema/src/recursive.ts @@ -1,6 +1,6 @@ import type * as T from '@traversable/registry' import { fn, URI, symbol } from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import { isRequired, property } from './properties.js' import type * as Spec from './specification.js' @@ -8,6 +8,70 @@ import type * as Spec from './specification.js' import * as JsonSchema from './jsonSchema.js' type JsonSchema = import('./jsonSchema.js').JsonSchema +type AnySchema = + | t.null + | t.number + | t.string + | t.boolean + | t.unknown + | t.array + | t.tuple + | t.union + | t.intersect + | t.object<{ [x: string]: AnySchema }> + +declare namespace InferSchema { + type SchemaMap = { + null: Const + boolean: Const + integer: Const + number: Const + string: Const + object: fromObjectLike + array: fromArrayLike + } + + type fromJsonSchema = never | LookupRootSchema + + type LookupRootSchema = never + | T extends { const: infer V } ? t.eq + : T extends { allOf: infer X } ? t.intersect<{ [I in keyof X]: LookupSchema }> + : T extends { anyOf: infer X } ? t.union<{ [I in keyof X]: LookupSchema }> + : Catch> + + type LookupSchema = never + | T extends { const: infer V } ? t.eq + : T extends { allOf: any } ? t.intersect + : T extends { anyOf: any } ? t.union + : Catch> + + type Catch = Spec.JsonSchema extends T ? AnySchema : T + + interface Const extends T.HKT { [-1]: T } + + interface fromObjectLike extends T.HKT { + [-1] + : [this[0]] extends [infer T] + ? T extends typeof Spec.RAW.any ? t.unknown + : T extends { additionalProperties: any } ? t.record> + : T extends { properties: infer P, required?: infer KS extends string[] } ? t.object } + & { [K in keyof P as K extends KS[number] ? never : K]: t.optional> } + >> + : AnySchema + : never + } + + interface fromArrayLike extends T.HKT { + [-1] + : [this[0]] extends [infer T] + ? T extends { additionalItems: false, items: infer S } ? t.tuple<{ -readonly [I in keyof S]: LookupSchema }> + : T extends { items: infer S } ? t.array> + : AnySchema + : never + } +} + /** @internal */ const phantom : () => T @@ -146,7 +210,7 @@ export namespace Recursive { * with {@link toJsonSchema} without any loss of information. */ export const fromJsonSchema - : (term: S) => t.LowerBound + : >(term: S) => InferSchema.fromJsonSchema = JsonSchema.fold(Recursive.fromJsonSchema) /** diff --git a/packages/schema-to-json-schema/src/schemas/any.ts b/packages/schema-to-json-schema/src/schemas/any.ts new file mode 100644 index 00000000..25336fc6 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/any.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): { type: 'object', properties: {}, nullable: true } } +export function toJsonSchema(): toJsonSchema { + function unknownToJsonSchema() { return { type: 'object', properties: {}, nullable: true } as const } + return unknownToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/array.ts b/packages/schema-to-json-schema/src/schemas/array.ts new file mode 100644 index 00000000..cbdda659 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/array.ts @@ -0,0 +1,36 @@ +import type { t } from '@traversable/schema-core' +import type * as T from '@traversable/registry' +import type { SizeBounds } from '@traversable/schema-to-json-schema' +import { hasSchema } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { + (): never | T.Force< + & { type: 'array', items: T.Returns } + & T.PickIfDefined + > +} + +export function toJsonSchema>(arraySchema: T): toJsonSchema +export function toJsonSchema(arraySchema: T): toJsonSchema +export function toJsonSchema( + { def, minLength, maxLength }: { def: unknown, minLength?: number, maxLength?: number }, +): () => { + type: 'array' + items: unknown + minLength?: number + maxLength?: number +} { + function arrayToJsonSchema() { + let items = hasSchema(def) ? def.toJsonSchema() : def + let out = { + type: 'array' as const, + items, + minLength, + maxLength, + } + if (typeof minLength !== 'number') delete out.minLength + if (typeof maxLength !== 'number') delete out.maxLength + return out + } + return arrayToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/bigint.ts b/packages/schema-to-json-schema/src/schemas/bigint.ts new file mode 100644 index 00000000..f6c7bc5b --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/bigint.ts @@ -0,0 +1,7 @@ +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function bigintToJsonSchema(): void { + return void 0 + } + return bigintToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/boolean.ts b/packages/schema-to-json-schema/src/schemas/boolean.ts new file mode 100644 index 00000000..d1b86f70 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/boolean.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): { type: 'boolean' } } +export function toJsonSchema(): toJsonSchema { + function booleanToJsonSchema() { return { type: 'boolean' as const } } + return booleanToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/eq.ts b/packages/schema-to-json-schema/src/schemas/eq.ts new file mode 100644 index 00000000..8edfd03a --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/eq.ts @@ -0,0 +1,8 @@ +import type { t } from '@traversable/schema-core' + +export interface toJsonSchema { (): { const: T } } +export function toJsonSchema(eqSchema: t.eq): toJsonSchema +export function toJsonSchema({ def }: t.eq) { + function eqToJsonSchema() { return { const: def } } + return eqToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/integer.ts b/packages/schema-to-json-schema/src/schemas/integer.ts new file mode 100644 index 00000000..d531e292 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/integer.ts @@ -0,0 +1,23 @@ +import type { Force, PickIfDefined } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { NumericBounds } from '@traversable/schema-to-json-schema' +import { getNumericBounds } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { (): Force<{ type: 'integer' } & PickIfDefined> } + +export function toJsonSchema(schema: S): toJsonSchema +export function toJsonSchema(schema: t.integer): toJsonSchema { + function integerToJsonSchema() { + const { exclusiveMaximum, exclusiveMinimum, maximum, minimum } = getNumericBounds(schema) + let bounds: NumericBounds = {} + if (typeof exclusiveMinimum === 'number') bounds.exclusiveMinimum = exclusiveMinimum + if (typeof exclusiveMaximum === 'number') bounds.exclusiveMaximum = exclusiveMaximum + if (typeof minimum === 'number') bounds.minimum = minimum + if (typeof maximum === 'number') bounds.maximum = maximum + return { + type: 'integer' as const, + ...bounds, + } + } + return integerToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/intersect.ts b/packages/schema-to-json-schema/src/schemas/intersect.ts new file mode 100644 index 00000000..d3942a94 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/intersect.ts @@ -0,0 +1,20 @@ +import type { Returns } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { getSchema } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { + (): { + allOf: { [I in keyof T]: Returns } + } +} + +export function toJsonSchema(intersectSchema: t.intersect): toJsonSchema +export function toJsonSchema(intersectSchema: t.intersect): toJsonSchema +export function toJsonSchema({ def }: t.intersect): () => {} { + function intersectToJsonSchema() { + return { + allOf: def.map(getSchema) + } + } + return intersectToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/never.ts b/packages/schema-to-json-schema/src/schemas/never.ts new file mode 100644 index 00000000..d22338df --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/never.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): never } +export function toJsonSchema(): toJsonSchema { + function neverToJsonSchema() { return void 0 as never } + return neverToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/null.ts b/packages/schema-to-json-schema/src/schemas/null.ts new file mode 100644 index 00000000..7a3b7c3a --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/null.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): { type: 'null', enum: [null] } } +export function toJsonSchema(): toJsonSchema { + function nullToJsonSchema() { return { type: 'null' as const, enum: [null] satisfies [any] } } + return nullToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/number.ts b/packages/schema-to-json-schema/src/schemas/number.ts new file mode 100644 index 00000000..7146d478 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/number.ts @@ -0,0 +1,23 @@ +import type { Force, PickIfDefined } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import type { NumericBounds } from '@traversable/schema-to-json-schema' +import { getNumericBounds } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { (): Force<{ type: 'number' } & PickIfDefined> } + +export function toJsonSchema(schema: S): toJsonSchema +export function toJsonSchema(schema: t.number): toJsonSchema { + function numberToJsonSchema() { + const { exclusiveMaximum, exclusiveMinimum, maximum, minimum } = getNumericBounds(schema) + let bounds: NumericBounds = {} + if (typeof exclusiveMinimum === 'number') bounds.exclusiveMinimum = exclusiveMinimum + if (typeof exclusiveMaximum === 'number') bounds.exclusiveMaximum = exclusiveMaximum + if (typeof minimum === 'number') bounds.minimum = minimum + if (typeof maximum === 'number') bounds.maximum = maximum + return { + type: 'number' as const, + ...bounds, + } + } + return numberToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/object.ts b/packages/schema-to-json-schema/src/schemas/object.ts new file mode 100644 index 00000000..05dc8f9e --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/object.ts @@ -0,0 +1,27 @@ +import type { Returns } from '@traversable/registry' +import { fn, Object_keys } from '@traversable/registry' +import type { RequiredKeys } from '@traversable/schema-to-json-schema' +import { isRequired, property } from '@traversable/schema-to-json-schema' +import type { t } from '@traversable/schema-core' + +export interface toJsonSchema = RequiredKeys> { + (): { + type: 'object' + required: { [I in keyof KS]: KS[I] & string } + properties: { [K in keyof T]: Returns } + } +} + +export function toJsonSchema(objectSchema: t.object): toJsonSchema +export function toJsonSchema(objectSchema: t.object): toJsonSchema +export function toJsonSchema({ def }: { def: { [x: string]: unknown } }): () => { type: 'object', required: string[], properties: {} } { + const required = Object_keys(def).filter(isRequired(def)) + function objectToJsonSchema() { + return { + type: 'object' as const, + required, + properties: fn.map(def, (v, k) => property(required)(v, k as number | string)), + } + } + return objectToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/of.ts b/packages/schema-to-json-schema/src/schemas/of.ts new file mode 100644 index 00000000..c8aaf62b --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/of.ts @@ -0,0 +1,7 @@ +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function inlineToJsonSchema(): void { + return void 0 + } + return inlineToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/optional.ts b/packages/schema-to-json-schema/src/schemas/optional.ts new file mode 100644 index 00000000..82bff553 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/optional.ts @@ -0,0 +1,19 @@ +import type { Force } from '@traversable/registry' +import type { Returns } from '@traversable/registry' +import { symbol } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { getSchema, wrapOptional } from '@traversable/schema-to-json-schema' + +type Nullable = Force + +export interface toJsonSchema { + (): Nullable> + [symbol.optional]: number +} + +export function toJsonSchema(optionalSchema: t.optional): toJsonSchema +export function toJsonSchema({ def }: t.optional) { + function optionalToJsonSchema() { return getSchema(def) } + optionalToJsonSchema[symbol.optional] = wrapOptional(def) + return optionalToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/record.ts b/packages/schema-to-json-schema/src/schemas/record.ts new file mode 100644 index 00000000..2503d568 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/record.ts @@ -0,0 +1,21 @@ +import type { t } from '@traversable/schema-core' +import type * as T from '@traversable/registry' +import { getSchema } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { + (): { + type: 'object' + additionalProperties: T.Returns + } +} + +export function toJsonSchema(recordSchema: t.record): toJsonSchema +export function toJsonSchema(recordSchema: t.record): toJsonSchema +export function toJsonSchema({ def }: { def: unknown }): () => { type: 'object', additionalProperties: unknown } { + return function recordToJsonSchema() { + return { + type: 'object' as const, + additionalProperties: getSchema(def), + } + } +} diff --git a/packages/schema-to-json-schema/src/schemas/string.ts b/packages/schema-to-json-schema/src/schemas/string.ts new file mode 100644 index 00000000..2956c069 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/string.ts @@ -0,0 +1,22 @@ +import type { Force, PickIfDefined } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { has } from '@traversable/registry' +import type { SizeBounds } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { + (): Force<{ type: 'string' } & PickIfDefined> +} + +export function toJsonSchema(schema: S): toJsonSchema +export function toJsonSchema(schema: t.string): () => { type: 'string' } & Partial { + function stringToJsonSchema() { + const minLength = has('minLength', (u: any) => typeof u === 'number')(schema) ? schema.minLength : null + const maxLength = has('maxLength', (u: any) => typeof u === 'number')(schema) ? schema.maxLength : null + let out: { type: 'string' } & Partial = { type: 'string' } + minLength !== null && void (out.minLength = minLength) + maxLength !== null && void (out.maxLength = maxLength) + + return out + } + return stringToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/symbol.ts b/packages/schema-to-json-schema/src/schemas/symbol.ts new file mode 100644 index 00000000..7046b08e --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/symbol.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function symbolToJsonSchema() { return void 0 } + return symbolToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/tuple.ts b/packages/schema-to-json-schema/src/schemas/tuple.ts new file mode 100644 index 00000000..a71ee145 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/tuple.ts @@ -0,0 +1,37 @@ +import type { Returns } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { applyTupleOptionality, minItems } from '@traversable/schema-to-json-schema' +import type { MinItems } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { + (): { + type: 'array', + items: { [I in keyof T]: Returns } + additionalItems: false + minItems: MinItems + maxItems: T['length' & keyof T] + } +} + +export function toJsonSchema(tupleSchema: t.tuple): toJsonSchema +export function toJsonSchema({ def }: t.tuple): () => { + type: 'array' + items: unknown + additionalItems: false + minItems?: {} + maxItems?: number +} { + function tupleToJsonSchema() { + let min = minItems(def) + let max = def.length + let items = applyTupleOptionality(def, { min, max }) + return { + type: 'array' as const, + additionalItems: false as const, + items, + minItems: min, + maxItems: max, + } + } + return tupleToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/undefined.ts b/packages/schema-to-json-schema/src/schemas/undefined.ts new file mode 100644 index 00000000..be46c306 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/undefined.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function undefinedToJsonSchema(): void { return void 0 } + return undefinedToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/union.ts b/packages/schema-to-json-schema/src/schemas/union.ts new file mode 100644 index 00000000..f9467612 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/union.ts @@ -0,0 +1,17 @@ +import type { Returns } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { getSchema } from '@traversable/schema-to-json-schema' + +export interface toJsonSchema { + (): { anyOf: { [I in keyof T]: Returns } } +} + +export function toJsonSchema(unionSchema: t.union): toJsonSchema +export function toJsonSchema(unionSchema: t.union): toJsonSchema +export function toJsonSchema({ def }: t.union): () => {} { + return function unionToJsonSchema() { + return { + anyOf: def.map(getSchema) + } + } +} diff --git a/packages/schema-to-json-schema/src/schemas/unknown.ts b/packages/schema-to-json-schema/src/schemas/unknown.ts new file mode 100644 index 00000000..8d5be5a0 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/unknown.ts @@ -0,0 +1,5 @@ +export interface toJsonSchema { (): { type: 'object', properties: {}, nullable: true } } +export function toJsonSchema(): toJsonSchema { + function anyToJsonSchema() { return { type: 'object', properties: {}, nullable: true } as const } + return anyToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/schemas/void.ts b/packages/schema-to-json-schema/src/schemas/void.ts new file mode 100644 index 00000000..d636b569 --- /dev/null +++ b/packages/schema-to-json-schema/src/schemas/void.ts @@ -0,0 +1,7 @@ +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function voidToJsonSchema(): void { + return void 0 + } + return voidToJsonSchema +} diff --git a/packages/schema-to-json-schema/src/specification.ts b/packages/schema-to-json-schema/src/specification.ts index a3e8058f..4c8216db 100644 --- a/packages/schema-to-json-schema/src/specification.ts +++ b/packages/schema-to-json-schema/src/specification.ts @@ -1,5 +1,5 @@ import type { HKT } from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' export const RAW = { any: { type: 'object', properties: {}, nullable: true } satisfies JsonSchema_any, diff --git a/packages/schema-to-json-schema/test/install.test.ts b/packages/schema-to-json-schema/test/install.test.ts index 5bd68909..e77ee9bd 100644 --- a/packages/schema-to-json-schema/test/install.test.ts +++ b/packages/schema-to-json-schema/test/install.test.ts @@ -1,5 +1,5 @@ import * as vi from 'vitest' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' vi.describe('〖⛳️〗‹‹‹ ❲@traversable/derive-validators❳', () => { vi.it('〖⛳️〗› ❲pre-install❳', () => vi.assert.isFalse(t.has('toJsonSchema')(t.string))) @@ -10,5 +10,3 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traversable/derive-validators❳', () => .catch((e) => vi.assert.fail(e.message)) }) }) - - diff --git a/packages/schema-to-json-schema/test/jsonSchema.test.ts b/packages/schema-to-json-schema/test/jsonSchema.test.ts index aeb6e2c4..98dedbc1 100644 --- a/packages/schema-to-json-schema/test/jsonSchema.test.ts +++ b/packages/schema-to-json-schema/test/jsonSchema.test.ts @@ -3,7 +3,7 @@ import { test } from '@fast-check/vitest' import { deepStrictEqual } from 'node:assert/strict' import { omitMethods, symbol, URI } from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import { JsonSchema, toJsonSchema, fromJsonSchema } from '@traversable/schema-to-json-schema' import { Seed } from '@traversable/schema-seed' @@ -35,7 +35,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-to-json-schema❳', () }) }) -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳: toJsonSchema', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳: toJsonSchema', () => { test.prop([seed], { // numRuns: 50_000 })('〖⛳️〗› ❲fromJsonSchema(...).toJsonSchema❳: roundtrips', (schema) => { @@ -226,7 +226,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳: toJsonSchema', () = // TODO: get `jsonSchema` working for inline schemas // vi.it('〖⛳️〗› ❲t.inline❳', () => vi.assert.deepEqual(t.inline((_) => _ instanceof Error).toJsonSchema(), void 0)) - vi.it('〖⛳️〗› ❲t.never❳', () => vi.assert.deepEqual(t.never.toJsonSchema(), void 0)) + vi.it('〖⛳️〗› ❲t.never❳', () => vi.assert.deepEqual((t.never as any).toJsonSchema(), void 0)) vi.it('〖⛳️〗› ❲t.void❳', () => vi.assert.deepEqual(t.void.toJsonSchema(), void 0)) vi.it('〖⛳️〗› ❲t.symbol❳', () => vi.assert.deepEqual(t.symbol.toJsonSchema(), void 0)) vi.it('〖⛳️〗› ❲t.undefined❳', () => vi.assert.deepEqual(t.undefined.toJsonSchema(), void 0)) @@ -414,7 +414,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳: toJsonSchema', () = }) vi.it('〖⛳️〗› ❲t.toJsonSchema❳: works with regular schemas', () => { - vi.assert.deepEqual(toJsonSchema(t.never)(), void 0) + vi.assert.deepEqual(toJsonSchema(t.never as never)(), void 0) vi.assert.deepEqual(toJsonSchema(t.bigint)(), void 0) vi.assert.deepEqual(toJsonSchema(t.symbol)(), void 0) vi.assert.deepEqual(toJsonSchema(t.undefined)(), void 0) @@ -647,7 +647,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳: toJsonSchema', () = }) }) -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳: fromJsonSchema', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳: fromJsonSchema', () => { vi.it('〖⛳️〗› ❲t.fromJsonSchema❳: t.integer', () => { let schema_01 = t.integer let schema_02 = t.integer.min(0) diff --git a/packages/schema-to-json-schema/tsconfig.build.json b/packages/schema-to-json-schema/tsconfig.build.json index cabdfce7..2972409b 100644 --- a/packages/schema-to-json-schema/tsconfig.build.json +++ b/packages/schema-to-json-schema/tsconfig.build.json @@ -9,6 +9,6 @@ }, "references": [ { "path": "../registry" }, - { "path": "../schema" } + { "path": "../schema-core" } ] } diff --git a/packages/schema-to-json-schema/tsconfig.src.json b/packages/schema-to-json-schema/tsconfig.src.json index fa90f525..9416bd78 100644 --- a/packages/schema-to-json-schema/tsconfig.src.json +++ b/packages/schema-to-json-schema/tsconfig.src.json @@ -8,7 +8,7 @@ }, "references": [ { "path": "../registry" }, - { "path": "../schema" } + { "path": "../schema-core" } ], "include": ["src"] } diff --git a/packages/schema-to-json-schema/tsconfig.test.json b/packages/schema-to-json-schema/tsconfig.test.json index 80aa9583..3ad0d988 100644 --- a/packages/schema-to-json-schema/tsconfig.test.json +++ b/packages/schema-to-json-schema/tsconfig.test.json @@ -9,7 +9,7 @@ "references": [ { "path": "tsconfig.src.json" }, { "path": "../registry" }, - { "path": "../schema" }, + { "path": "../schema-core" }, { "path": "../schema-seed" } ], "include": ["test"] diff --git a/packages/schema-to-string/package.json b/packages/schema-to-string/package.json index 2a09efa7..c13f1f9b 100644 --- a/packages/schema-to-string/package.json +++ b/packages/schema-to-string/package.json @@ -48,11 +48,11 @@ }, "peerDependencies": { "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^" + "@traversable/schema-core": "workspace:^" }, "devDependencies": { "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "@traversable/schema-seed": "workspace:^", "zod": "^3.24.2" } diff --git a/packages/schema-to-string/src/__generated__/__manifest__.ts b/packages/schema-to-string/src/__generated__/__manifest__.ts index d25edeb8..9d3ec919 100644 --- a/packages/schema-to-string/src/__generated__/__manifest__.ts +++ b/packages/schema-to-string/src/__generated__/__manifest__.ts @@ -42,11 +42,11 @@ export default { }, "peerDependencies": { "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^" + "@traversable/schema-core": "workspace:^" }, "devDependencies": { "@traversable/registry": "workspace:^", - "@traversable/schema": "workspace:^", + "@traversable/schema-core": "workspace:^", "@traversable/schema-seed": "workspace:^", "zod": "^3.24.2" } diff --git a/packages/schema-to-string/src/exports.ts b/packages/schema-to-string/src/exports.ts index 479c525e..a42d015f 100644 --- a/packages/schema-to-string/src/exports.ts +++ b/packages/schema-to-string/src/exports.ts @@ -1,2 +1,8 @@ export * as toString from './toString.js' +export { + callToString, + hasToString, + isShowable, + stringify, +} from './shared.js' export { VERSION } from './version.js' diff --git a/packages/schema-to-string/src/install.ts b/packages/schema-to-string/src/install.ts index bb260202..0e304f5b 100644 --- a/packages/schema-to-string/src/install.ts +++ b/packages/schema-to-string/src/install.ts @@ -1,8 +1,8 @@ -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import * as toString from './toString.js' -declare module '@traversable/schema' { - interface t_never extends toString.never { } +declare module '@traversable/schema-core' { + // interface t_never extends toString.never { } interface t_unknown extends toString.unknown { } interface t_void extends toString.void { } interface t_any extends toString.any { } @@ -43,12 +43,12 @@ function bind() { Object.assign(t.bigint, { toString: toString.bigint }) Object.assign(t.number, { toString: toString.number }) Object.assign(t.string, { toString: toString.string }) - Object.assign(t.eq.prototype, { toString: toString.eq }) - Object.assign(t.optional.prototype, { toString: toString.optional }) - Object.assign(t.union.prototype, { toString: toString.union }) - Object.assign(t.intersect.prototype, { toString: toString.intersect }) - Object.assign(t.tuple.prototype, { toString: toString.tuple }) - Object.assign(t.object.prototype, { toString: toString.object }) - Object.assign(t.array.prototype, { toString: toString.array }) - Object.assign(t.record.prototype, { toString: toString.record }) + Object.assign(t.eq.userDefinitions, { toString: toString.eq }) + Object.assign(t.optional.userDefinitions, { toString: toString.optional }) + Object.assign(t.union.userDefinitions, { toString: toString.union }) + Object.assign(t.intersect.userDefinitions, { toString: toString.intersect }) + Object.assign(t.tuple.userDefinitions, { toString: toString.tuple }) + Object.assign(t.object.userDefinitions, { toString: toString.object }) + Object.assign(t.array.userDefinitions, { toString: toString.array }) + Object.assign(t.record.userDefinitions, { toString: toString.record }) } diff --git a/packages/schema-to-string/src/recursive.ts b/packages/schema-to-string/src/recursive.ts index 147faaee..6fe476a6 100644 --- a/packages/schema-to-string/src/recursive.ts +++ b/packages/schema-to-string/src/recursive.ts @@ -1,10 +1,11 @@ import type * as T from '@traversable/registry' import { fn, NS, parseKey, URI } from '@traversable/registry' import { Json } from '@traversable/json' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' /** @internal */ const Object_entries = globalThis.Object.entries + /** @internal */ const OPT = '<<>>' as const /** @internal */ diff --git a/packages/schema-to-string/src/schemas/any.ts b/packages/schema-to-string/src/schemas/any.ts new file mode 100644 index 00000000..f70aa050 --- /dev/null +++ b/packages/schema-to-string/src/schemas/any.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'any' } +export function toString(): 'any' { return 'any' } diff --git a/packages/schema-to-string/src/schemas/array.ts b/packages/schema-to-string/src/schemas/array.ts new file mode 100644 index 00000000..5114ef0b --- /dev/null +++ b/packages/schema-to-string/src/schemas/array.ts @@ -0,0 +1,22 @@ +import type { t } from '@traversable/schema-core' + +export interface toString { + /* @ts-expect-error */ + (): never | `(${ReturnType})[]` +} + +export function toString(arraySchema: t.array): toString +export function toString(arraySchema: t.array): toString +export function toString({ def }: { def: unknown }) { + function arrayToString() { + let body = ( + !!def + && typeof def === 'object' + && 'toString' in def + && typeof def.toString === 'function' + ) ? def.toString() + : '${string}' + return ('(' + body + ')[]') + } + return arrayToString +} diff --git a/packages/schema-to-string/src/schemas/bigint.ts b/packages/schema-to-string/src/schemas/bigint.ts new file mode 100644 index 00000000..793c903e --- /dev/null +++ b/packages/schema-to-string/src/schemas/bigint.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'bigint' } +export function toString(): 'bigint' { return 'bigint' } diff --git a/packages/schema-to-string/src/schemas/boolean.ts b/packages/schema-to-string/src/schemas/boolean.ts new file mode 100644 index 00000000..3c408e57 --- /dev/null +++ b/packages/schema-to-string/src/schemas/boolean.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'boolean' } +export function toString(): 'boolean' { return 'boolean' } diff --git a/packages/schema-to-string/src/schemas/eq.ts b/packages/schema-to-string/src/schemas/eq.ts new file mode 100644 index 00000000..3c224bea --- /dev/null +++ b/packages/schema-to-string/src/schemas/eq.ts @@ -0,0 +1,17 @@ +import type { Key } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { stringify } from '@traversable/schema-to-string' + +export interface toString { + (): [Key] extends [never] + ? [T] extends [symbol] ? 'symbol' : 'symbol' + : [T] extends [string] ? `'${T}'` : Key +} + +export function toString(eqSchema: t.eq): toString +export function toString({ def }: t.eq): () => string { + function eqToString(): string { + return typeof def === 'symbol' ? 'symbol' : stringify(def) + } + return eqToString +} diff --git a/packages/schema-to-string/src/schemas/integer.ts b/packages/schema-to-string/src/schemas/integer.ts new file mode 100644 index 00000000..912565e6 --- /dev/null +++ b/packages/schema-to-string/src/schemas/integer.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'number' } +export function toString(): 'number' { return 'number' } diff --git a/packages/schema-to-string/src/schemas/intersect.ts b/packages/schema-to-string/src/schemas/intersect.ts new file mode 100644 index 00000000..2e179159 --- /dev/null +++ b/packages/schema-to-string/src/schemas/intersect.ts @@ -0,0 +1,18 @@ +import type { Join } from '@traversable/registry' +import { Array_isArray } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { callToString } from '@traversable/schema-to-string' + +export interface toString { + (): never | [T] extends [readonly []] ? 'unknown' + /* @ts-expect-error */ + : `(${Join<{ [I in keyof T]: Returns }, ' & '>})` +} + +export function toString(intersectSchema: t.intersect): toString +export function toString({ def }: t.intersect): () => string { + function intersectToString() { + return Array_isArray(def) ? def.length === 0 ? 'never' : `(${def.map(callToString).join(' & ')})` : 'unknown' + } + return intersectToString +} diff --git a/packages/schema-to-string/src/schemas/never.ts b/packages/schema-to-string/src/schemas/never.ts new file mode 100644 index 00000000..aaabf80d --- /dev/null +++ b/packages/schema-to-string/src/schemas/never.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'never' } +export function toString(): 'never' { return 'never' } diff --git a/packages/schema-to-string/src/schemas/null.ts b/packages/schema-to-string/src/schemas/null.ts new file mode 100644 index 00000000..35c3aef8 --- /dev/null +++ b/packages/schema-to-string/src/schemas/null.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'null' } +export function toString(): 'null' { return 'null' } diff --git a/packages/schema-to-string/src/schemas/number.ts b/packages/schema-to-string/src/schemas/number.ts new file mode 100644 index 00000000..912565e6 --- /dev/null +++ b/packages/schema-to-string/src/schemas/number.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'number' } +export function toString(): 'number' { return 'number' } diff --git a/packages/schema-to-string/src/schemas/object.ts b/packages/schema-to-string/src/schemas/object.ts new file mode 100644 index 00000000..3edc0bb8 --- /dev/null +++ b/packages/schema-to-string/src/schemas/object.ts @@ -0,0 +1,42 @@ +import type { Join, UnionToTuple } from '@traversable/registry' +import { symbol } from '@traversable/registry' +import type { t } from '@traversable/schema-core' + +/** @internal */ +type Symbol_optional = typeof Symbol_optional +const Symbol_optional: typeof symbol.optional = symbol.optional + +/** @internal */ +const hasOptionalSymbol = (u: unknown): u is { toString(): T } => + !!u && typeof u === 'function' + && Symbol_optional in u + && typeof u[Symbol_optional] === 'number' + +/** @internal */ +const hasToString = (x: unknown): x is { toString(): string } => + !!x && typeof x === 'function' && 'toString' in x && typeof x.toString === 'function' + +export interface toString> { + (): never + | [keyof T] extends [never] ? '{}' + /* @ts-expect-error */ + : `{ ${Join<{ [I in keyof _]: `'${_[I]}${T[_[I]] extends { [Symbol_optional]: any } ? `'?` : `'`}: ${ReturnType}` }, ', '>} }` +} + + +export function toString>(objectSchema: t.object): toString +export function toString({ def }: t.object) { + function objectToString() { + if (!!def && typeof def === 'object') { + const entries = Object.entries(def) + if (entries.length === 0) return '{}' + else return `{ ${entries.map(([k, x]) => `'${k}${hasOptionalSymbol(x) ? "'?" : "'" + }: ${hasToString(x) ? x.toString() : 'unknown' + }`).join(', ') + } }` + } + else return '{ [x: string]: unknown }' + } + + return objectToString +} \ No newline at end of file diff --git a/packages/schema-to-string/src/schemas/of.ts b/packages/schema-to-string/src/schemas/of.ts new file mode 100644 index 00000000..417e1048 --- /dev/null +++ b/packages/schema-to-string/src/schemas/of.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'unknown' } +export function toString(): 'unknown' { return 'unknown' } diff --git a/packages/schema-to-string/src/schemas/optional.ts b/packages/schema-to-string/src/schemas/optional.ts new file mode 100644 index 00000000..f4c96cc8 --- /dev/null +++ b/packages/schema-to-string/src/schemas/optional.ts @@ -0,0 +1,15 @@ +import type { t } from '@traversable/schema-core' +import { callToString } from '@traversable/schema-to-string' + +export interface toString { + /* @ts-expect-error */ + (): never | `(${ReturnType} | undefined)` +} + +export function toString(optionalSchema: t.optional): toString +export function toString({ def }: t.optional): () => string { + function optionalToString(): string { + return '(' + callToString(def) + ' | undefined)' + } + return optionalToString +} diff --git a/packages/schema-to-string/src/schemas/record.ts b/packages/schema-to-string/src/schemas/record.ts new file mode 100644 index 00000000..868f3544 --- /dev/null +++ b/packages/schema-to-string/src/schemas/record.ts @@ -0,0 +1,17 @@ +import type { Returns } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { callToString } from '@traversable/schema-to-string' + +export interface toString { + /* @ts-expect-error */ + (): never | `Record}>` +} + +export function toString>(recordSchema: S): toString +export function toString(recordSchema: t.record): toString +export function toString({ def }: { def: unknown }): () => string { + function recordToString() { + return `Record` + } + return recordToString +} diff --git a/packages/schema-to-string/src/schemas/string.ts b/packages/schema-to-string/src/schemas/string.ts new file mode 100644 index 00000000..86a98e16 --- /dev/null +++ b/packages/schema-to-string/src/schemas/string.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'string' } +export function toString(): 'string' { return 'string' } diff --git a/packages/schema-to-string/src/schemas/symbol.ts b/packages/schema-to-string/src/schemas/symbol.ts new file mode 100644 index 00000000..5651fe27 --- /dev/null +++ b/packages/schema-to-string/src/schemas/symbol.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'symbol' } +export function toString(): 'symbol' { return 'symbol' } diff --git a/packages/schema-to-string/src/schemas/tuple.ts b/packages/schema-to-string/src/schemas/tuple.ts new file mode 100644 index 00000000..638daca9 --- /dev/null +++ b/packages/schema-to-string/src/schemas/tuple.ts @@ -0,0 +1,27 @@ +import type { Join } from '@traversable/registry' +import { Array_isArray, has, URI } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { hasToString } from '@traversable/schema-to-string' + +export interface toString { + (): never | `[${Join<{ + [I in keyof T]: `${ + /* @ts-expect-error */ + T[I] extends { [Symbol_optional]: any } ? `_?: ${ReturnType}` : ReturnType + }` + }, ', '>}]` +} + +export function toString(tupleSchema: t.tuple): toString +export function toString(tupleSchema: t.tuple): () => string { + let isOptional = has('tag', (tag) => tag === URI.optional) + function stringToString() { + return Array_isArray(tupleSchema.def) + ? `[${tupleSchema.def.map( + (x) => isOptional(x) + ? `_?: ${hasToString(x) ? x.toString() : 'unknown'}` + : hasToString(x) ? x.toString() : 'unknown' + ).join(', ')}]` : 'unknown[]' + } + return stringToString +} diff --git a/packages/schema-to-string/src/schemas/undefined.ts b/packages/schema-to-string/src/schemas/undefined.ts new file mode 100644 index 00000000..a48b744b --- /dev/null +++ b/packages/schema-to-string/src/schemas/undefined.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'undefined' } +export function toString(): 'undefined' { return 'undefined' } diff --git a/packages/schema-to-string/src/schemas/union.ts b/packages/schema-to-string/src/schemas/union.ts new file mode 100644 index 00000000..d9c0f3d1 --- /dev/null +++ b/packages/schema-to-string/src/schemas/union.ts @@ -0,0 +1,18 @@ +import type { Join } from '@traversable/registry' +import { Array_isArray } from '@traversable/registry' +import type { t } from '@traversable/schema-core' +import { callToString } from '@traversable/schema-to-string' + +export interface toString { + (): never | [T] extends [readonly []] ? 'never' + /* @ts-expect-error */ + : `(${Join<{ [I in keyof T]: ReturnType }, ' | '>})` +} + +export function toString(unionSchema: t.union): toString +export function toString({ def }: t.union): () => string { + function unionToString() { + return Array_isArray(def) ? def.length === 0 ? 'never' : `(${def.map(callToString).join(' | ')})` : 'unknown' + } + return unionToString +} diff --git a/packages/schema-to-string/src/schemas/unknown.ts b/packages/schema-to-string/src/schemas/unknown.ts new file mode 100644 index 00000000..417e1048 --- /dev/null +++ b/packages/schema-to-string/src/schemas/unknown.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'unknown' } +export function toString(): 'unknown' { return 'unknown' } diff --git a/packages/schema-to-string/src/schemas/void.ts b/packages/schema-to-string/src/schemas/void.ts new file mode 100644 index 00000000..487d08b3 --- /dev/null +++ b/packages/schema-to-string/src/schemas/void.ts @@ -0,0 +1,2 @@ +export interface toString { (): 'void' } +export function toString(): 'void' { return 'void' } diff --git a/packages/schema-to-string/src/shared.ts b/packages/schema-to-string/src/shared.ts new file mode 100644 index 00000000..786e97a7 --- /dev/null +++ b/packages/schema-to-string/src/shared.ts @@ -0,0 +1,15 @@ +export let hasToString = (x: unknown): x is { toString(): string } => + !!x && typeof x === 'function' && 'toString' in x && typeof x.toString === 'function' + +export let isShowable = (u: unknown) => u == null + || typeof u === 'boolean' + || typeof u === 'number' + || typeof u === 'bigint' + || typeof u === 'string' + ; + +export function callToString(x: unknown): string { return hasToString(x) ? x.toString() : 'unknown' } + +export let stringify + : (u: unknown) => string + = (u) => typeof u === 'string' ? `'${u}'` : isShowable(u) ? String(u) : 'string' diff --git a/packages/schema-to-string/src/toString.ts b/packages/schema-to-string/src/toString.ts index 6518d2ea..9d8741ae 100644 --- a/packages/schema-to-string/src/toString.ts +++ b/packages/schema-to-string/src/toString.ts @@ -1,6 +1,11 @@ import type { Returns, Join, Showable, UnionToTuple } from '@traversable/registry' import { symbol } from '@traversable/registry' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' +import { + isShowable, + hasToString, + stringify, +} from './shared.js' export { neverToString as never, @@ -25,31 +30,13 @@ export { objectToString as object, } -/** @internal */ -type Symbol_optional = typeof Symbol_optional -const Symbol_optional: typeof symbol.optional = symbol.optional - /** @internal */ const isArray = globalThis.Array.isArray -/** @internal */ -const hasToString = (x: unknown): x is { toString(): string } => - !!x && typeof x === 'function' && 'toString' in x && typeof x.toString === 'function' - /** @internal */ const isOptional = (u: unknown): u is { toString(): T } => !!u && typeof u === 'function' && - Symbol_optional in u && - typeof u[Symbol_optional] === 'number' - -/** @internal */ -const isShowable = (u: unknown) => u == null - || typeof u === 'boolean' - || typeof u === 'number' - || typeof u === 'bigint' - || typeof u === 'string' - -/** @internal */ -const stringify = (u: unknown) => typeof u === 'string' ? `'${u}'` : isShowable(u) ? globalThis.String(u) : 'string' + symbol.optional in u && + typeof u[symbol.optional] === 'number' export function toString(x: unknown): string { return hasToString(x) ? x.toString() : 'unknown' } @@ -69,7 +56,7 @@ export declare namespace toString { export type tuple = never | `[${Join<{ [I in keyof T]: `${ /* @ts-expect-error */ - T[I] extends { [Symbol_optional]: any } ? `_?: ${Returns}` : Returns + symbol.optional extends keyof T[I] ? `_?: ${Returns}` : Returns }` }, ', '>}]` /* @ts-expect-error */ @@ -80,7 +67,7 @@ export declare namespace toString { export type object_> = never | [keyof T] extends [never] ? '{}' /* @ts-expect-error */ - : `{ ${Join<{ [I in keyof _]: `'${_[I]}${T[_[I]] extends { [Symbol_optional]: any } ? `'?` : `'`}: ${Returns}` }, ', '>} }` + : `{ ${Join<{ [I in keyof _]: `'${_[I]}${T[_[I]] extends { [symbol.optional]: any } ? `'?` : `'`}: ${Returns}` }, ', '>} }` } toString.never = 'never' as const @@ -144,7 +131,7 @@ function stringToString() { return toString.string } interface eqToString { toString(): Returns> } interface arrayToString { toString(): Returns> } -interface optionalToString { toString(): Returns>, [Symbol_optional]: number } +interface optionalToString { toString(): Returns>, [symbol.optional]: number } interface recordToString { toString(): Returns> } interface unionToString { toString(): Returns> } interface intersectToString { toString(): Returns> } @@ -159,3 +146,4 @@ function unionToString(this: t.union) { return toString.union(this.def) } function intersectToString(this: t.intersect) { return toString.intersect(this.def) } function tupleToString(this: t.tuple) { return toString.tuple(this.def) } function objectToString(this: t.object) { return toString.object(this.def) } + diff --git a/packages/schema-to-string/test/install.test.ts b/packages/schema-to-string/test/install.test.ts index b7c2622e..39bc6ab1 100644 --- a/packages/schema-to-string/test/install.test.ts +++ b/packages/schema-to-string/test/install.test.ts @@ -1,5 +1,5 @@ import * as vi from 'vitest' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-to-string❳', () => { vi.it('〖⛳️〗› ❲pre-install❳', () => { diff --git a/packages/schema-to-string/test/integration.test.ts b/packages/schema-to-string/test/integration.test.ts index 1356474c..7485770d 100644 --- a/packages/schema-to-string/test/integration.test.ts +++ b/packages/schema-to-string/test/integration.test.ts @@ -3,7 +3,7 @@ import { fc } from '@fast-check/vitest' import * as path from 'node:path' import * as fs from 'node:fs' -import { recurse } from '@traversable/schema' +import { recurse } from '@traversable/schema-core' import { Seed } from '@traversable/schema-seed' import '@traversable/schema-to-string/install' @@ -38,12 +38,10 @@ if (!fs.existsSync(PATH.dir)) fs.mkdirSync(PATH.dir) if (!fs.existsSync(PATH.target.schemas)) fs.writeFileSync(PATH.target.schemas, '') if (!fs.existsSync(PATH.target.toString)) fs.writeFileSync(PATH.target.toString, '') -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳: integration tests', () => { - // void bindToStrings() - +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳: integration tests', () => { const imports = [ `import * as vi from 'vitest'`, - `import { t } from '@traversable/schema'` + `import { t } from '@traversable/schema-core'` ] as const satisfies string[] const gen = fc.sample(Seed.schema(OPTIONS), NUM_RUNS) @@ -95,7 +93,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳: integration tests', fs.writeFileSync(PATH.target.schemas, schemasOut) fs.writeFileSync(PATH.target.toString, toStringsOut) - vi.it('〖⛳️〗› ❲@traverable/schema❳: it writes', () => { + vi.it('〖⛳️〗› ❲@traversable/schema-core❳: it writes', () => { vi.assert.isTrue(fs.existsSync(PATH.target.schemas)) vi.assert.isTrue(fs.existsSync(PATH.target.toString)) }) diff --git a/packages/schema-to-string/test/toString.test.ts b/packages/schema-to-string/test/toString.test.ts index 1b43ead8..aff5de42 100644 --- a/packages/schema-to-string/test/toString.test.ts +++ b/packages/schema-to-string/test/toString.test.ts @@ -1,6 +1,6 @@ import * as vi from 'vitest' -import { t, configure } from '@traversable/schema' +import { t, configure } from '@traversable/schema-core' import '@traversable/schema-to-string/install' configure({ @@ -9,7 +9,7 @@ configure({ } }) -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳: t.recurse.toString', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-core❳: t.recurse.toString', () => { vi.it('〖⛳️〗‹ ❲t.recurse.toString(t.void)❳', () => vi.expect(t.recurse.toString(t.void)).toMatchInlineSnapshot(`"t.void"`)) vi.it('〖⛳️〗‹ ❲t.recurse.toString(t.any)❳', () => vi.expect(t.recurse.toString(t.any)).toMatchInlineSnapshot(`"t.any"`)) vi.it('〖⛳️〗‹ ❲t.recurse.toString(t.unknown)❳', () => vi.expect(t.recurse.toString(t.unknown)).toMatchInlineSnapshot(`"t.unknown"`)) @@ -306,13 +306,13 @@ vi.it('〖⛳️〗› ❲t.enum(...)❳', () => { vi.expect(ex_02).toMatchInlineSnapshot(`"undefined | null | symbol | true | 1n | 1 | '\\'"`) vi.assertType<`undefined | null | false | symbol | 0 | 0n | ''`>(ex_01) vi.assertType< - | `undefined | null | true | symbol | 1 | 1n | '\\'` | `'\\' | undefined | null | true | symbol | 1 | 1n` + | `1 | undefined | null | true | symbol | 1n | '\\'` | `undefined | null | true | symbol | 1 | '\\' | 1n` + | `undefined | null | true | symbol | 1 | 1n | '\\'` >(ex_02) }) - vi.it('〖⛳️〗› ❲t.tuple(...).toString❳', () => { vi.expect(t.tuple().toString()).toMatchInlineSnapshot(`"[]"`) vi.assertType<'[]'>(t.tuple().toString()) @@ -451,6 +451,7 @@ vi.it('〖⛳️〗› ❲t.object(...).toString❳', () => ( | `{ 'a': { 'e': { 'g': 'a.e.g', 'f': 'a.e.f' }, 'b': { 'd': 'a.b.d', 'c': 'a.b.c' } }, 'h': { 'i': { 'j': 'h.i.j', 'k': 'h.i.k' }, 'l': { 'm': 'h.l.m', 'n': 'h.l.n' } } }` | `{ 'a': { 'e': { 'g': 'a.e.g', 'f': 'a.e.f' }, 'b': { 'd': 'a.b.d', 'c': 'a.b.c' } }, 'h': { 'l': { 'm': 'h.l.m', 'n': 'h.l.n' }, 'i': { 'j': 'h.i.j', 'k': 'h.i.k' } } }` | `{ 'h': { 'i': { 'j': 'h.i.j', 'k': 'h.i.k' }, 'l': { 'n': 'h.l.n', 'm': 'h.l.m' } }, 'a': { 'b': { 'd': 'a.b.d', 'c': 'a.b.c' }, 'e': { 'g': 'a.e.g', 'f': 'a.e.f' } } }` + | `{ 'h': { 'l': { 'm': 'h.l.m', 'n': 'h.l.n' }, 'i': { 'j': 'h.i.j', 'k': 'h.i.k' } }, 'a': { 'b': { 'c': 'a.b.c', 'd': 'a.b.d' }, 'e': { 'f': 'a.e.f', 'g': 'a.e.g' } } }` | `{ 'h': { 'l': { 'm': 'h.l.m', 'n': 'h.l.n' }, 'i': { 'j': 'h.i.j', 'k': 'h.i.k' } }, 'a': { 'b': { 'c': 'a.b.c', 'd': 'a.b.d' }, 'e': { 'g': 'a.e.g', 'f': 'a.e.f' } } }` | `{ 'h': { 'l': { 'm': 'h.l.m', 'n': 'h.l.n' }, 'i': { 'j': 'h.i.j', 'k': 'h.i.k' } }, 'a': { 'e': { 'g': 'a.e.g', 'f': 'a.e.f' }, 'b': { 'c': 'a.b.c', 'd': 'a.b.d' } } }` | `{ 'h': { 'l': { 'n': 'h.l.n', 'm': 'h.l.m' }, 'i': { 'j': 'h.i.j', 'k': 'h.i.k' } }, 'a': { 'e': { 'f': 'a.e.f', 'g': 'a.e.g' }, 'b': { 'd': 'a.b.d', 'c': 'a.b.c' } } }` @@ -505,6 +506,7 @@ vi.it('〖⛳️〗› ❲t.object(...).toString❳', () => ( ).toMatchInlineSnapshot(`"{ 'a': { 'b': { 'c'?: ('a.b.c' | undefined), 'd': 'a.b.d' }, 'e'?: ({ 'f': 'a.e.f', 'g'?: ('a.e.g' | undefined) } | undefined) }, 'h'?: ({ 'i'?: ({ 'j': 'h.i.j', 'k'?: ('h.i.k' | undefined) } | undefined), 'l': { 'm'?: ('h.l.m' | undefined), 'n': 'h.l.n' } } | undefined) }"`), vi.assertType< + | `{ 'a': { 'b': { 'c': ('a.b.c' | undefined), 'd': 'a.b.d' }, 'e': ({ 'f': 'a.e.f', 'g': ('a.e.g' | undefined) } | undefined) }, 'h': ({ 'i': ({ 'j': 'h.i.j', 'k': ('h.i.k' | undefined) } | undefined), 'l': { 'm': ('h.l.m' | undefined), 'n': 'h.l.n' } } | undefined) }` | `{ 'a': { 'b': { 'c'?: ('a.b.c' | undefined), 'd': 'a.b.d' }, 'e'?: ({ 'f': 'a.e.f', 'g'?: ('a.e.g' | undefined) } | undefined) }, 'h'?: ({ 'i'?: ({ 'j': 'h.i.j', 'k'?: ('h.i.k' | undefined) } | undefined), 'l': { 'm'?: ('h.l.m' | undefined), 'n': 'h.l.n' } } | undefined) }` | `{ 'a': { 'b': { 'c'?: ('a.b.c' | undefined), 'd': 'a.b.d' }, 'e'?: ({ 'f': 'a.e.f', 'g'?: ('a.e.g' | undefined) } | undefined) }, 'h'?: ({ 'i'?: ({ 'k'?: ('h.i.k' | undefined), 'j': 'h.i.j' } | undefined), 'l': { 'm'?: ('h.l.m' | undefined), 'n': 'h.l.n' } } | undefined) }` | `{ 'a': { 'b': { 'c'?: ('a.b.c' | undefined), 'd': 'a.b.d' }, 'e'?: ({ 'f': 'a.e.f', 'g'?: ('a.e.g' | undefined) } | undefined) }, 'h'?: ({ 'l': { 'm'?: ('h.l.m' | undefined), 'n': 'h.l.n' }, 'i'?: ({ 'k'?: ('h.i.k' | undefined), 'j': 'h.i.j' } | undefined) } | undefined) }` @@ -515,6 +517,7 @@ vi.it('〖⛳️〗› ❲t.object(...).toString❳', () => ( | `{ 'a': { 'e'?: ({ 'f': 'a.e.f', 'g'?: ('a.e.g' | undefined) } | undefined), 'b': { 'c'?: ('a.b.c' | undefined), 'd': 'a.b.d' } }, 'h'?: ({ 'l': { 'n': 'h.l.n', 'm'?: ('h.l.m' | undefined) }, 'i'?: ({ 'k'?: ('h.i.k' | undefined), 'j': 'h.i.j' } | undefined) } | undefined) }` | `{ 'a': { 'e'?: ({ 'g'?: ('a.e.g' | undefined), 'f': 'a.e.f' } | undefined), 'b': { 'd': 'a.b.d', 'c'?: ('a.b.c' | undefined) } }, 'h'?: ({ 'i'?: ({ 'j': 'h.i.j', 'k'?: ('h.i.k' | undefined) } | undefined), 'l': { 'm'?: ('h.l.m' | undefined), 'n': 'h.l.n' } } | undefined) }` | `{ 'h'?: ({ 'i'?: ({ 'j': 'h.i.j', 'k'?: ('h.i.k' | undefined) } | undefined), 'l': { 'n': 'h.l.n', 'm'?: ('h.l.m' | undefined) } } | undefined), 'a': { 'b': { 'd': 'a.b.d', 'c'?: ('a.b.c' | undefined) }, 'e'?: ({ 'g'?: ('a.e.g' | undefined), 'f': 'a.e.f' } | undefined) } }` + | `{ 'h'?: ({ 'l': { 'm'?: ('h.l.m' | undefined), 'n': 'h.l.n' }, 'i'?: ({ 'j': 'h.i.j', 'k'?: ('h.i.k' | undefined) } | undefined) } | undefined), 'a': { 'b': { 'c'?: ('a.b.c' | undefined), 'd': 'a.b.d' }, 'e'?: ({ 'f': 'a.e.f', 'g'?: ('a.e.g' | undefined) } | undefined) } }` | `{ 'h'?: ({ 'l': { 'm'?: ('h.l.m' | undefined), 'n': 'h.l.n' }, 'i'?: ({ 'j': 'h.i.j', 'k'?: ('h.i.k' | undefined) } | undefined) } | undefined), 'a': { 'b': { 'c'?: ('a.b.c' | undefined), 'd': 'a.b.d' }, 'e'?: ({ 'g'?: ('a.e.g' | undefined), 'f': 'a.e.f' } | undefined) } }` | `{ 'h'?: ({ 'l': { 'm'?: ('h.l.m' | undefined), 'n': 'h.l.n' }, 'i'?: ({ 'j': 'h.i.j', 'k'?: ('h.i.k' | undefined) } | undefined) } | undefined), 'a': { 'e'?: ({ 'g'?: ('a.e.g' | undefined), 'f': 'a.e.f' } | undefined), 'b': { 'c'?: ('a.b.c' | undefined), 'd': 'a.b.d' } } }` | `{ 'h'?: ({ 'l': { 'n': 'h.l.n', 'm'?: ('h.l.m' | undefined) }, 'i'?: ({ 'j': 'h.i.j', 'k'?: ('h.i.k' | undefined) } | undefined) } | undefined), 'a': { 'e'?: ({ 'f': 'a.e.f', 'g'?: ('a.e.g' | undefined) } | undefined), 'b': { 'd': 'a.b.d', 'c'?: ('a.b.c' | undefined) } } }` @@ -1042,6 +1045,7 @@ vi.it('〖⛳️〗› ❲t.object.toString❳: stress tests', () => { ).toMatchInlineSnapshot(`"{ 'one': { 'one': 1, 'two': { 'one': 1, 'two': 2, 'three': { 'one': 1, 'two': 2, 'three': 3, 'four': { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'eight': { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'eight': 8, 'nine': { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10, 'eleven': { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10, 'eleven': 11, 'twelve': { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10, 'eleven': 11, 'twelve': 12, 'thirteen': { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10, 'eleven': 11, 'twelve': 12, 'thirteen': 13, 'fourteen': { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10, 'eleven': 11, 'twelve': 12, 'thirteen': 13, 'fourteen': 14, 'fifteen': {} }, 'one_hundred': 100, 'two_hundred': 200, 'three_hundred': 300, 'four_hundred': 400, 'five_hundred': 500, 'six_hundred': 600, 'seven_hundred': 700, 'eight_hundred': 800, 'nine_hundred': 900, 'one_thousand': 1000, 'eleven_hundred': 1100, 'twelve_hundred': 1200, 'thirteen_hundred': 1300, 'fourteen_hundred': 1400 }, 'one_hundred': 100, 'two_hundred': 200, 'three_hundred': 300, 'four_hundred': 400, 'five_hundred': 500, 'six_hundred': 600, 'seven_hundred': 700, 'eight_hundred': 800, 'nine_hundred': 900, 'one_thousand': 1000, 'eleven_hundred': 1100, 'twelve_hundred': 1200, 'thirteen_hundred': 1300 }, 'one_hundred': 100, 'two_hundred': 200, 'three_hundred': 300, 'four_hundred': 400, 'five_hundred': 500, 'six_hundred': 600, 'seven_hundred': 700, 'eight_hundred': 800, 'nine_hundred': 900, 'one_thousand': 1000, 'eleven_hundred': 1100, 'twelve_hundred': 1200 }, 'one_hundred': 100, 'two_hundred': 200, 'three_hundred': 300, 'four_hundred': 400, 'five_hundred': 500, 'six_hundred': 600, 'seven_hundred': 700, 'eight_hundred': 800, 'nine_hundred': 900, 'one_thousand': 1000, 'eleven_hundred': 1100 }, 'one_hundred': 100, 'two_hundred': 200, 'three_hundred': 300, 'four_hundred': 400, 'five_hundred': 500, 'six_hundred': 600, 'seven_hundred': 700, 'eight_hundred': 800, 'nine_hundred': 900, 'one_thousand': 1000 }, 'one_hundred': 100, 'two_hundred': 200, 'three_hundred': 300, 'four_hundred': 400, 'five_hundred': 500, 'six_hundred': 600, 'seven_hundred': 700, 'eight_hundred': 800, 'nine_hundred': 900 }, 'one_hundred': 100, 'two_hundred': 200, 'three_hundred': 300, 'four_hundred': 400, 'five_hundred': 500, 'six_hundred': 600, 'seven_hundred': 700, 'eight_hundred': 800 }, 'one_hundred': 100, 'two_hundred': 200, 'three_hundred': 300, 'four_hundred': 400, 'five_hundred': 500, 'six_hundred': 600, 'seven_hundred': 700 }, 'one_hundred': 100, 'two_hundred': 200, 'three_hundred': 300, 'four_hundred': 400, 'five_hundred': 500, 'six_hundred': 600 }, 'one_hundred': 100, 'two_hundred': 200, 'three_hundred': 300, 'four_hundred': 400, 'five_hundred': 500 }, 'one_hundred': 100, 'two_hundred': 200, 'three_hundred': 300, 'four_hundred': 400 }, 'one_hundred': 100, 'two_hundred': 200, 'three_hundred': 300 }, 'one_hundred': 100, 'two_hundred': 200 }, 'one_hundred': 100 }"`) vi.assertType< + | `{ '1': ['3'], '2': ['3'], '0.2': { '22': ['3', '+', '30'] }, '3': ['3'], '0.3': { '33': ['3', '+', '30'], '0.33': { '333': ['3', '+', '30', '300'] } }, '4': ['4'], '0.4': { '44': ['4', '+', '40'], '0.44': { '444': ['4', '+', '40', '400'], '0.444': { '4444': ['4', '+', '40', '+', '400', '+', '4000'] } } }, '5': ['5'], '0.5': { '55': ['5', '+', '50'], '0.55': { '555': ['5', '+', '50', '500'], '0.555': { '5555': ['5', '+', '50', '+', '500', '+', '5000'], '0.5555': { '55555': ['5', '+', '50', '+', '500', '+', '5000', '+', '50000'] } } } }, '6': ['6'], '0.6': { '66': ['6', '+', '60'], '0.66': { '666': ['6', '+', '60', '600'], '0.666': { '6666': ['6', '+', '60', '+', '600', '+', '6000'], '0.6666': { '66666': ['6', '+', '60', '+', '600', '+', '6000', '+', '60000'], '0.66666': { '666666': ['6', '+', '60', '+', '600', '+', '6000', '+', '60000', '+', '600000'] } } } } }, '7': ['7'], '0.7': { '77': ['7', '+', '70'], '0.77': { '777': ['7', '+', '70', '700'], '0.777': { '7777': ['7', '+', '70', '+', '700', '+', '7000'], '0.7777': { '77777': ['7', '+', '70', '+', '700', '+', '7000', '+', '70000'], '0.77777': { '777777': ['7', '+', '70', '+', '700', '+', '7000', '+', '70000', '+', '700000'], '0.777777': { '7777777': ['7', '+', '70', '+', '700', '+', '7000', '+', '70000', '+', '700000', '+', '7000000'] } } } } } }, '8': ['8'], '0.8': { '88': ['8', '+', '80'], '0.88': { '888': ['8', '+', '80', '800'], '0.888': { '8888': ['8', '+', '80', '+', '800', '+', '8000'], '0.8888': { '88888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000'], '0.88888': { '888888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000', '+', '800000'], '0.888888': { '8888888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000', '+', '800000', '+', '8000000'], '0.8888888': { '88888888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000', '+', '800000', '+', '8000000', '+', '80000000'], '0.88888888': { '888888888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000', '+', '800000', '+', '8000000', '+', '80000000', '+', '800000000'] } } } } } } } }, '9': ['9'], '0.9': { '99': ['9', '+', '90'], '0.99': { '999': ['9', '+', '90', '900'], '0.999': { '9999': ['9', '+', '90', '+', '900', '+', '9000'], '0.9999': { '99999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000'], '0.99999': { '999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000'], '0.999999': { '9999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000', '+', '9000000'], '0.9999999': { '99999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000', '+', '9000000', '+', '90000000'], '0.99999999': { '999999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000', '+', '9000000', '+', '90000000', '+', '900000000'], '0.999999999': { '9999999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000', '+', '9000000', '+', '90000000', '+', '900000000', '+', '9000000000'] } } } } } } } } } }` | `{ '1': ['3'], '2': ['3'], '3': ['3'], '4': ['4'], '5': ['5'], '6': ['6'], '7': ['7'], '8': ['8'], '9': ['9'], '0.2': { '22': ['3', '+', '30'] }, '0.3': { '33': ['3', '+', '30'], '0.33': { '333': ['3', '+', '30', '300'] } }, '0.4': { '44': ['4', '+', '40'], '0.44': { '444': ['4', '+', '40', '400'], '0.444': { '4444': ['4', '+', '40', '+', '400', '+', '4000'] } } }, '0.5': { '55': ['5', '+', '50'], '0.55': { '555': ['5', '+', '50', '500'], '0.555': { '5555': ['5', '+', '50', '+', '500', '+', '5000'], '0.5555': { '55555': ['5', '+', '50', '+', '500', '+', '5000', '+', '50000'] } } } }, '0.6': { '66': ['6', '+', '60'], '0.66': { '666': ['6', '+', '60', '600'], '0.666': { '6666': ['6', '+', '60', '+', '600', '+', '6000'], '0.6666': { '66666': ['6', '+', '60', '+', '600', '+', '6000', '+', '60000'], '0.66666': { '666666': ['6', '+', '60', '+', '600', '+', '6000', '+', '60000', '+', '600000'] } } } } }, '0.7': { '77': ['7', '+', '70'], '0.77': { '777': ['7', '+', '70', '700'], '0.777': { '7777': ['7', '+', '70', '+', '700', '+', '7000'], '0.7777': { '77777': ['7', '+', '70', '+', '700', '+', '7000', '+', '70000'], '0.77777': { '777777': ['7', '+', '70', '+', '700', '+', '7000', '+', '70000', '+', '700000'], '0.777777': { '7777777': ['7', '+', '70', '+', '700', '+', '7000', '+', '70000', '+', '700000', '+', '7000000'] } } } } } }, '0.8': { '88': ['8', '+', '80'], '0.88': { '888': ['8', '+', '80', '800'], '0.888': { '8888': ['8', '+', '80', '+', '800', '+', '8000'], '0.8888': { '88888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000'], '0.88888': { '888888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000', '+', '800000'], '0.888888': { '8888888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000', '+', '800000', '+', '8000000'], '0.8888888': { '88888888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000', '+', '800000', '+', '8000000', '+', '80000000'], '0.88888888': { '888888888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000', '+', '800000', '+', '8000000', '+', '80000000', '+', '800000000'] } } } } } } } }, '0.9': { '99': ['9', '+', '90'], '0.99': { '999': ['9', '+', '90', '900'], '0.999': { '9999': ['9', '+', '90', '+', '900', '+', '9000'], '0.9999': { '99999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000'], '0.99999': { '999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000'], '0.999999': { '9999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000', '+', '9000000'], '0.9999999': { '99999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000', '+', '9000000', '+', '90000000'], '0.99999999': { '999999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000', '+', '9000000', '+', '90000000', '+', '900000000'], '0.999999999': { '9999999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000', '+', '9000000', '+', '90000000', '+', '900000000', '+', '9000000000'] } } } } } } } } } }` | `{ '5': ['5'], '9': ['9'], '8': ['8'], '1': ['3'], '2': ['3'], '3': ['3'], '4': ['4'], '6': ['6'], '7': ['7'], '0.2': { '22': ['3', '+', '30'] }, '0.3': { '33': ['3', '+', '30'], '0.33': { '333': ['3', '+', '30', '300'] } }, '0.4': { '44': ['4', '+', '40'], '0.44': { '444': ['4', '+', '40', '400'], '0.444': { '4444': ['4', '+', '40', '+', '400', '+', '4000'] } } }, '0.5': { '55': ['5', '+', '50'], '0.55': { '555': ['5', '+', '50', '500'], '0.555': { '5555': ['5', '+', '50', '+', '500', '+', '5000'], '0.5555': { '55555': ['5', '+', '50', '+', '500', '+', '5000', '+', '50000'] } } } }, '0.6': { '66': ['6', '+', '60'], '0.66': { '666': ['6', '+', '60', '600'], '0.666': { '6666': ['6', '+', '60', '+', '600', '+', '6000'], '0.6666': { '66666': ['6', '+', '60', '+', '600', '+', '6000', '+', '60000'], '0.66666': { '666666': ['6', '+', '60', '+', '600', '+', '6000', '+', '60000', '+', '600000'] } } } } }, '0.7': { '77': ['7', '+', '70'], '0.77': { '777': ['7', '+', '70', '700'], '0.777': { '7777': ['7', '+', '70', '+', '700', '+', '7000'], '0.7777': { '77777': ['7', '+', '70', '+', '700', '+', '7000', '+', '70000'], '0.77777': { '777777': ['7', '+', '70', '+', '700', '+', '7000', '+', '70000', '+', '700000'], '0.777777': { '7777777': ['7', '+', '70', '+', '700', '+', '7000', '+', '70000', '+', '700000', '+', '7000000'] } } } } } }, '0.8': { '88': ['8', '+', '80'], '0.88': { '888': ['8', '+', '80', '800'], '0.888': { '8888': ['8', '+', '80', '+', '800', '+', '8000'], '0.8888': { '88888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000'], '0.88888': { '888888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000', '+', '800000'], '0.888888': { '8888888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000', '+', '800000', '+', '8000000'], '0.8888888': { '88888888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000', '+', '800000', '+', '8000000', '+', '80000000'], '0.88888888': { '888888888': ['8', '+', '80', '+', '800', '+', '8000', '+', '80000', '+', '800000', '+', '8000000', '+', '80000000', '+', '800000000'] } } } } } } } }, '0.9': { '99': ['9', '+', '90'], '0.99': { '999': ['9', '+', '90', '900'], '0.999': { '9999': ['9', '+', '90', '+', '900', '+', '9000'], '0.9999': { '99999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000'], '0.99999': { '999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000'], '0.999999': { '9999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000', '+', '9000000'], '0.9999999': { '99999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000', '+', '9000000', '+', '90000000'], '0.99999999': { '999999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000', '+', '9000000', '+', '90000000', '+', '900000000'], '0.999999999': { '9999999999': ['9', '+', '90', '+', '900', '+', '9000', '+', '90000', '+', '900000', '+', '9000000', '+', '90000000', '+', '900000000', '+', '9000000000'] } } } } } } } } } }` >( diff --git a/packages/schema-to-string/test/zod-side-by-side.test.ts b/packages/schema-to-string/test/zod-side-by-side.test.ts index 8759d1be..03c21023 100644 --- a/packages/schema-to-string/test/zod-side-by-side.test.ts +++ b/packages/schema-to-string/test/zod-side-by-side.test.ts @@ -1,5 +1,5 @@ import * as vi from 'vitest' -import { t } from '@traversable/schema' +import { t } from '@traversable/schema-core' import { z } from 'zod' /** @@ -1191,7 +1191,7 @@ const ZodSchema = z.object({ -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema-to-string❳', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema-to-string❳', () => { vi.it('〖⛳️〗‹ ❲side-by-side❳: zod', () => { /** diff --git a/packages/schema-to-string/tsconfig.build.json b/packages/schema-to-string/tsconfig.build.json index cabdfce7..2972409b 100644 --- a/packages/schema-to-string/tsconfig.build.json +++ b/packages/schema-to-string/tsconfig.build.json @@ -9,6 +9,6 @@ }, "references": [ { "path": "../registry" }, - { "path": "../schema" } + { "path": "../schema-core" } ] } diff --git a/packages/schema-to-string/tsconfig.src.json b/packages/schema-to-string/tsconfig.src.json index fa90f525..9416bd78 100644 --- a/packages/schema-to-string/tsconfig.src.json +++ b/packages/schema-to-string/tsconfig.src.json @@ -8,7 +8,7 @@ }, "references": [ { "path": "../registry" }, - { "path": "../schema" } + { "path": "../schema-core" } ], "include": ["src"] } diff --git a/packages/schema-to-string/tsconfig.test.json b/packages/schema-to-string/tsconfig.test.json index 7ac34fc6..3ad0d988 100644 --- a/packages/schema-to-string/tsconfig.test.json +++ b/packages/schema-to-string/tsconfig.test.json @@ -9,8 +9,8 @@ "references": [ { "path": "tsconfig.src.json" }, { "path": "../registry" }, - { "path": "../schema" }, - { "path": "../schema-seed" }, + { "path": "../schema-core" }, + { "path": "../schema-seed" } ], "include": ["test"] } diff --git a/packages/schema/README.md b/packages/schema/README.md index 61df6ae0..3e533d99 100644 --- a/packages/schema/README.md +++ b/packages/schema/README.md @@ -3,10 +3,7 @@

- A lightweight, modular schema library with opt-in power tools. - Extensible in userland via - side-effect imports - + module augmentation. + TODO: write me

@@ -35,346 +32,7 @@ TypeScript Playground   •   npm -
+
-
- -
- -`@traversable/schema` exploits a TypeScript feature called -[inferred type predicates](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#inferred-type-predicates) -to do what libaries like `zod` do, without the additional runtime overhead or abstraction. - -> **Note:** -> -> These docs are a W.I.P. -> -> We recommend jumping straight to the [demo](https://stackblitz.com/edit/traversable?file=src%2Fsandbox.tsx) -> or [playground](https://tsplay.dev/w2y29W). - -## Requirements - -The only hard requirement is [TypeScript 5.5](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/). -Since the core primitive that `@traversable/schema` is built on top of is -[inferred type predicates](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#inferred-type-predicates), -we do not have plans to backport to previous versions. - -## Quick start - -```typescript -import { t } from '@traversable/schema' - -declare let ex_01: unknown - -if (t.bigint(ex_01)) { - ex_01 - // ^? let ex_01: bigint -} - -const schema_01 = t.object({ - abc: t.optional(t.string), - def: t.tuple( - t.eq(1), - t.optional(t.eq(2)), // `t.eq` can be used to match any literal JSON value - t.optional(t.eq(3)), - ) -}) - -if (schema_01(ex_01)) { - ex_01 - // ^? let ex_01: { abc?: string, def: [ᵃ: 1, ᵇ?: 2, ᶜ?: 3] } - // ^ tuples are labeled to support optionality -} -``` - - -## Features - -`@traversable/schema` is modular by schema (like valibot), but takes it a step further by making its feature set opt-in by default. - -The ability to add features like this is a knock-on effect of traversable's extensible core. - -### First-class support for inferred type predicates - -> **Note:** This is the only feature on this list that is built into the core library. - -The motivation for creating another schema library was to add native support for inferred type predicates, -which no other schema library currently does (although please file an issue if that has changed!). - -This is possible because the traversable schemas are themselves just type predicates with a few additional properties -that allow them to also be used for reflection. - -- **Instructions:** To use this feature, define a predicate inline and `@traversable/schema` will figure out the rest. - -#### Example - -You can play with this example in the TypeScript Playground. - -```typescript -import { t } from '@traversable/schema' - -export let Classes = t.object({ - promise: (v) => v instanceof Promise, - set: (v) => v instanceof Set, - map: (v) => v instanceof Map, - weakMap: (v) => v instanceof WeakMap, - date: (v) => v instanceof Date, - regex: (v) => v instanceof RegExp, - error: (v) => v instanceof Error, - typeError: (v) => v instanceof TypeError, - syntaxError: (v) => v instanceof SyntaxError, - buffer: (v) => v instanceof ArrayBuffer, - readableStream: (v) => v instanceof ReadableStream, -}) - -type Classes = t.typeof -// ^? type Classes = { -// promise: Promise -// set: Set -// map: Map -// weakMap: WeakMap -// date: Date -// regex: RegExp -// error: Error -// typeError: TypeError -// syntaxError: SyntaxError -// buffer: ArrayBuffer -// readableStream: ReadableStream -// } - -let Values = t.object({ - function: (v) => typeof v === 'function', - successStatus: (v) => v === 200 || v === 201 || v === 202 || v === 204, - clientErrorStatus: (v) => v === 400 || v === 401 || v === 403 || v === 404, - serverErrorStatus: (v) => v === 500 || v === 502 || v === 503, - teapot: (v) => v === 418, - true: (v) => v === true, - false: (v) => v === false, - mixed: (v) => Array.isArray(v) || v === true, - startsWith: (v): v is `bill${string}` => typeof v === 'string' && v.startsWith('bill'), - endsWith: (v): v is `${string}murray` => typeof v === 'string' && v.endsWith('murral'), -}) - -type Values = t.typeof -// ^? type Values = { -// function: Function -// successStatus: 200 | 201 | 202 | 204 -// clientErrorStatus: 400 | 401 | 403 | 404 -// serverErrorStatus: 500 | 502 | 503 -// teapot: 418 -// true: true -// false: false -// mixed: true | any[] -// startsWith: `bill${string}` -// endsWith: `${string}murray` -// } - -let Shorthand = t.object({ - nonnullable: Boolean, - unknown: () => true, - never: () => false, -}) - -type Shorthand = t.typeof -// ^? type Shorthand = { -// nonnullable: {} -// unknown: unknown -// never?: never -// } -``` - -### `.validate` - -`.validate` is similar to `z.safeParse`, except more than an order of magnitude faster*. - -- **Instructions:** To install the `.validate` method to all schemas, simply import `@traversable/derive-validators/install`. -- [ ] TODO: add benchmarks + write-up - -#### Example - -Play with this example in the [TypeScript playground](https://tsplay.dev/NaBEPm). - -```typescript -import { t } from '@traversable/schema' -import '@traversable/derive-validators/install' -// ↑↑ importing `@traversable/derive-validators/install` adds `.validate` to all schemas - -let schema_01 = t.object({ - product: t.object({ - x: t.integer, - y: t.integer - }), - sum: t.union( - t.tuple(t.eq(0), t.integer), - t.tuple(t.eq(1), t.integer), - ), -}) - -let result = schema_01.validate({ product: { x: null }, sum: [2, 3.141592]}) -// ↑↑ .validate is available - -console.log(result) -// => -// [ -// { "kind": "TYPE_MISMATCH", "path": [ "product", "x" ], "expected": "number", "got": null }, -// { "kind": "REQUIRED", "path": [ "product" ], "msg": "Missing key 'y'" }, -// { "kind": "TYPE_MISMATCH", "path": [ "sum", 0 ], "expected": 0, "got": 2 }, -// { "kind": "TYPE_MISMATCH", "path": [ "sum", 1 ], "expected": "number", "got": 3.141592 }, -// { "kind": "TYPE_MISMATCH", "path": [ "sum", 0 ], "expected": 1, "got": 2 }, -// { "kind": "TYPE_MISMATCH", "path": [ "sum", 1 ], "expected": "number", "got": 3.141592 }, -// ] -``` - -### `.toString` - -The `.toString` method prints a stringified version of the type that the schema represents. - -Works on both the term- and type-level. - -- **Instructions:** To install the `.toString` method on all schemas, simply import `@traversable/schema-to-string/install`. - -- Caveat: type-level functionality is provided as a heuristic only; since object keys are unordered in the TS type system, the order that the -keys are printed at runtime might differ from the order they appear on the type-level. - -#### Example - -Play with this example in the [TypeScript playground](https://tsplay.dev/W49jew) - -```typescript -import { t } from '@traversable/schema' -import '@traversable/schema-to-string/install' -// ↑↑ importing `@traversable/schema-to-string/install` adds the upgraded `.toString` method on all schemas - -const schema_02 = t.intersect( - t.object({ - bool: t.optional(t.boolean), - nested: t.object({ - int: t.integer, - union: t.union(t.tuple(t.string), t.null), - }), - key: t.union(t.string, t.symbol, t.number), - }), - t.object({ - record: t.record(t.string), - maybeArray: t.optional(t.array(t.string)), - enum: t.enum('x', 'y', 1, 2, null), - }), -) - -let ex_02 = schema_02.toString() -// ^? let ex_02: "({ -// 'bool'?: (boolean | undefined), -// 'nested': { 'int': number, 'union': ([string] | null) }, -// 'key': (string | symbol | number) } -// & { -// 'record': Record, -// 'maybeArray'?: ((string)[] | undefined), -// 'enum': 'x' | 'y' | 1 | 2 | null -// })" -``` - -### `.toJsonSchema` - -- **Instructions:** To install the `.toJsonSchema` method on all schemas, simply import `@traversable/schema-to-json-schema/install`. - -#### Example - -Play with this example in the [TypeScript playground](https://tsplay.dev/NB98Vw). - -```typescript -import * as vi from 'vitest' - -import { t } from '@traversable/schema' -import '@traversable/schema-to-json-schema/install' -// ↑↑ importing `@traversable/schema-to-json-schema/install` adds `.toJsonSchema` on all schemas - -const schema_02 = t.intersect( - t.object({ - stringWithMaxExample: t.optional(t.string.max(255)), - nestedObjectExample: t.object({ - integerExample: t.integer, - tupleExample: t.tuple( - t.eq(1), - t.optional(t.eq(2)), - t.optional(t.eq(3)), - ), - }), - stringOrNumberExample: t.union(t.string, t.number), - }), - t.object({ - recordExample: t.record(t.string), - arrayExample: t.optional(t.array(t.string)), - enumExample: t.enum('x', 'y', 1, 2, null), - }), -) - -vi.assertType<{ - allOf: [ - { - type: "object" - required: ("nestedObjectExample" | "stringOrNumberExample")[] - properties: { - stringWithMaxExample: { type: "string", minLength: 3 } - stringOrNumberExample: { anyOf: [{ type: "string" }, { type: "number" }] } - nestedObjectExample: { - type: "object" - required: ("integerExample" | "tupleExample")[] - properties: { - integerExample: { type: "integer" } - tupleExample: { - type: "array" - minItems: 1 - maxItems: 3 - items: [{ const: 1 }, { const: 2 }, { const: 3 }] - additionalItems: false - } - } - } - } - }, - { - type: "object" - required: ("recordExample" | "enumExample")[] - properties: { - recordExample: { type: "object", additionalProperties: { type: "string" } } - arrayExample: { type: "array", items: { type: "string" } } - enumExample: { enum: ["x", "y", 1, 2, null] } - } - } - ] -}>(schema_02.toJsonSchema()) -// ↑↑ importing `@traversable/schema-to-json-schema` installs `.toJsonSchema` -``` - -### Codec (`.pipe`, `.extend`, `.parse`, `.decode` & `.encode`) - -- **Instructions:** to install the `.codec` method on all schemas, all you need to do is import `@traversable/derive-codec`. - - To create a covariant codec (similar to zod's `.transform`), use `.codec.pipe` - - To create a contravariant codec (similar to zod's `.preprocess`), use `.codec.extend` (WIP) - -#### Example - -Play with this example in the [TypeScript playground](https://tsplay.dev/mbbv3m). - -```typescript -import { t } from '@traversable/schema' -import '@traversable/derive-codec/install' -// ↑↑ importing `@traversable/derive-codec/install` adds `.codec` on all schemas - -let User = t - .object({ name: t.optional(t.string), createdAt: t.string }) - .codec // <-- notice we're pulling off the `.codec` property - .pipe((user) => ({ ...user, createdAt: new Date(user.createdAt) })) - .unpipe((user) => ({ ...user, createdAt: user.createdAt.toISOString() })) - -let fromAPI = User.parse({ name: 'Bill Murray', createdAt: new Date().toISOString() }) -// ^? let fromAPI: Error | { name?: string, createdAt: Date} - -if (fromAPI instanceof Error) throw fromAPI -fromAPI -// ^? { name?: string, createdAt: Date } - -let toAPI = User.encode(fromAPI) -// ^? let toAPI: { name?: string, createdAt: string } -``` - +
+
diff --git a/packages/schema/package.json b/packages/schema/package.json index f2414b22..533e9257 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -15,15 +15,14 @@ "email": "ahrjarrett@gmail.com" }, "@traversable": { - "generateExports": { + "generateExports": { "include": [ - "**/*.ts" + "**/*.ts", + "schemas/*.ts" ] }, - "generateIndex": { - "include": [ - "**/*.ts" - ] + "generateIndex": { + "include": ["**/*.ts"] } }, "publishConfig": { @@ -32,7 +31,10 @@ "registry": "https://registry.npmjs.org" }, "scripts": { + "bench": "echo NOTHING TO BENCH", "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:schemas": "pnpm dlx tsx ./src/build.ts", + "build:schemas:watch": "pnpm dlx tsx --watch ./src/build.ts", "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", "build:esm": "tsc -b tsconfig.build.json", "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", @@ -42,14 +44,24 @@ "clean:deps": "rm -rf node_modules", "test": "vitest" }, - "devDependencies": { - "@traversable/schema-zod-adapter": "workspace:^", - "@types/lodash.isequal": "^4.5.8", - "fast-check": "^3.0.0", - "lodash.isequal": "^4.5.0", - "zod": "^3.24.2" - }, "peerDependencies": { - "@traversable/registry": "workspace:^" + "@traversable/derive-codec": "workspace:^", + "@traversable/derive-equals": "workspace:^", + "@traversable/derive-validators": "workspace:^", + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^", + "@traversable/schema-generator": "workspace:^", + "@traversable/schema-to-json-schema": "workspace:^", + "@traversable/schema-to-string": "workspace:^" + }, + "devDependencies": { + "@traversable/derive-codec": "workspace:^", + "@traversable/derive-equals": "workspace:^", + "@traversable/derive-validators": "workspace:^", + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^", + "@traversable/schema-generator": "workspace:^", + "@traversable/schema-to-json-schema": "workspace:^", + "@traversable/schema-to-string": "workspace:^" } } diff --git a/packages/schema/src/__generated__/__manifest__.ts b/packages/schema/src/__generated__/__manifest__.ts index 5ff492d9..c1d3b16e 100644 --- a/packages/schema/src/__generated__/__manifest__.ts +++ b/packages/schema/src/__generated__/__manifest__.ts @@ -16,7 +16,7 @@ export default { }, "@traversable": { "generateExports": { - "include": ["**/*.ts"] + "include": ["**/*.ts", "schemas/*.ts"] }, "generateIndex": { "include": ["**/*.ts"] @@ -28,7 +28,10 @@ export default { "registry": "https://registry.npmjs.org" }, "scripts": { + "bench": "echo NOTHING TO BENCH", "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate", + "build:schemas": "pnpm dlx tsx ./src/build.ts", + "build:schemas:watch": "pnpm dlx tsx --watch ./src/build.ts", "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", "build:esm": "tsc -b tsconfig.build.json", "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", @@ -38,14 +41,24 @@ export default { "clean:deps": "rm -rf node_modules", "test": "vitest" }, - "devDependencies": { - "@traversable/schema-zod-adapter": "workspace:^", - "@types/lodash.isequal": "^4.5.8", - "fast-check": "^3.0.0", - "lodash.isequal": "^4.5.0", - "zod": "^3.24.2" - }, "peerDependencies": { - "@traversable/registry": "workspace:^" + "@traversable/derive-codec": "workspace:^", + "@traversable/derive-equals": "workspace:^", + "@traversable/derive-validators": "workspace:^", + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^", + "@traversable/schema-generator": "workspace:^", + "@traversable/schema-to-json-schema": "workspace:^", + "@traversable/schema-to-string": "workspace:^" + }, + "devDependencies": { + "@traversable/derive-codec": "workspace:^", + "@traversable/derive-equals": "workspace:^", + "@traversable/derive-validators": "workspace:^", + "@traversable/registry": "workspace:^", + "@traversable/schema-core": "workspace:^", + "@traversable/schema-generator": "workspace:^", + "@traversable/schema-to-json-schema": "workspace:^", + "@traversable/schema-to-string": "workspace:^" } } as const \ No newline at end of file diff --git a/packages/schema/src/__schemas__/any.ts b/packages/schema/src/__schemas__/any.ts new file mode 100644 index 00000000..88f53e49 --- /dev/null +++ b/packages/schema/src/__schemas__/any.ts @@ -0,0 +1,80 @@ +/** + * any_ schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { Equal, Unknown } from '@traversable/registry' +import { Object_assign, Object_is, URI } from '@traversable/registry' +import type { t } from '../_exports.js' +import type { ValidationFn } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: unknown, right: unknown): boolean { + return Object_is(left, right) +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): { type: 'object', properties: {}, nullable: true } } +export function toJsonSchema(): toJsonSchema { + function unknownToJsonSchema() { return { type: 'object', properties: {}, nullable: true } as const } + return unknownToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'any' } +export function toString(): 'any' { return 'any' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(_?: any_): validate { + validateAny.tag = URI.any + function validateAny() { return true as const } + return validateAny +} +/// validate /// +////////////////////// + +export { any_ as any } + +export let userDefinitions: Record = { + equals, + toJsonSchema, + toString, +} + +export let userExtensions: Record = { + validate, +} + +interface any_ extends any_.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +function AnySchema(src: unknown): src is any { return true } +AnySchema.tag = URI.any +AnySchema.def = void 0 as any + +const any_ = Object_assign( + AnySchema, + userDefinitions, +) as any_ + +Object_assign(any_, userExtensions) + +declare namespace any_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.any + _type: any + get def(): this['_type'] + } +} diff --git a/packages/schema/src/__schemas__/array.ts b/packages/schema/src/__schemas__/array.ts new file mode 100644 index 00000000..745032f0 --- /dev/null +++ b/packages/schema/src/__schemas__/array.ts @@ -0,0 +1,250 @@ +/** + * array schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type * as T from '@traversable/registry' +import type { + Bounds, + Equal, + Integer, + Unknown +} from '@traversable/registry' +import { + _isPredicate, + array as arrayOf, + Array_isArray, + bindUserExtensions, + carryover, + has, + Math_max, + Math_min, + Number_isSafeInteger, + Object_assign, + Object_is, + URI, + within +} from '@traversable/registry' +import type { Guarded, Schema, SchemaLike } from '../_namespace.js' +import type { of } from './of.js' +import type { t } from '../_exports.js' +import type { SizeBounds } from '@traversable/schema-to-json-schema' +import { hasSchema } from '@traversable/schema-to-json-schema' +import type { ValidationError, ValidationFn, Validator } from '@traversable/derive-validators' +import { Errors, NullaryErrors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = never | Equal + +export function equals(arraySchema: array): equals +export function equals(arraySchema: array): equals +export function equals({ def }: array<{ equals: Equal }>): Equal { + let equals = has('equals', (x): x is Equal => typeof x === 'function')(def) ? def.equals : Object_is + function arrayEquals(l: unknown[], r: unknown[]): boolean { + if (Object_is(l, r)) return true + if (Array_isArray(l)) { + if (!Array_isArray(r)) return false + let len = l.length + if (len !== r.length) return false + for (let ix = len; ix-- !== 0;) + if (!equals(l[ix], r[ix])) return false + return true + } else return false + } + return arrayEquals +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { + (): never | T.Force< + & { type: 'array', items: T.Returns } + & T.PickIfDefined + > +} + +export function toJsonSchema>(arraySchema: T): toJsonSchema +export function toJsonSchema(arraySchema: T): toJsonSchema +export function toJsonSchema( + { def, minLength, maxLength }: { def: unknown, minLength?: number, maxLength?: number }, +): () => { + type: 'array' + items: unknown + minLength?: number + maxLength?: number +} { + function arrayToJsonSchema() { + let items = hasSchema(def) ? def.toJsonSchema() : def + let out = { + type: 'array' as const, + items, + minLength, + maxLength, + } + if (typeof minLength !== 'number') delete out.minLength + if (typeof maxLength !== 'number') delete out.maxLength + return out + } + return arrayToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { + /* @ts-expect-error */ + (): never | `(${ReturnType})[]` +} + +export function toString(arraySchema: array): toString +export function toString(arraySchema: array): toString +export function toString({ def }: { def: unknown }) { + function arrayToString() { + let body = ( + !!def + && typeof def === 'object' + && 'toString' in def + && typeof def.toString === 'function' + ) ? def.toString() + : '${string}' + return ('(' + body + ')[]') + } + return arrayToString +} +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = never | ValidationFn +export function validate(arraySchema: array): validate +export function validate(arraySchema: array): validate +export function validate( + { def: { validate = () => true }, minLength, maxLength }: array +) { + validateArray.tag = URI.array + function validateArray(u: unknown, path = Array.of()) { + if (!Array.isArray(u)) return [NullaryErrors.array(u, path)] + let errors = Array.of() + if (typeof minLength === 'number' && u.length < minLength) errors.push(Errors.arrayMinLength(u, path, minLength)) + if (typeof maxLength === 'number' && u.length > maxLength) errors.push(Errors.arrayMaxLength(u, path, maxLength)) + for (let i = 0, len = u.length; i < len; i++) { + let y = u[i] + let results = validate(y, [...path, i]) + if (results === true) continue + else errors.push(...results) + } + return errors.length === 0 || errors + } + return validateArray +} +/// validate /// +////////////////////// + +/** @internal */ +function boundedArray(schema: S, bounds: Bounds, carry?: Partial>): ((u: unknown) => boolean) & Bounds & array +function boundedArray(schema: S, bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & array +function boundedArray(schema: S, bounds: Bounds, carry?: {}): ((u: unknown) => boolean) & Bounds & array { + return Object_assign(function BoundedArraySchema(u: unknown) { + return Array_isArray(u) && within(bounds)(u.length) + }, carry, array(schema)) +} + +export interface array extends array.core { + toString: toString + equals: equals + toJsonSchema: toJsonSchema + validate: validate +} + +export function array(schema: S, readonly: 'readonly'): readonlyArray +export function array(schema: S): array +export function array(schema: S): array>> +export function array(schema: S): array { + return array.def(schema) +} + +export namespace array { + export let userDefinitions: Record = { + } as array + export function def(x: S, prev?: array): array + export function def(x: S, prev?: unknown): array + export function def(x: S, prev?: array): array + export function def(x: unknown, prev?: unknown): {} { + let userExtensions: Record = { + toJsonSchema, + validate, + toString, + equals, + } + const predicate = _isPredicate(x) ? arrayOf(x) : Array_isArray + function ArraySchema(src: unknown) { return predicate(src) } + ArraySchema.tag = URI.array + ArraySchema.def = x + ArraySchema.min = function arrayMin(minLength: Min) { + return Object_assign( + boundedArray(x, { gte: minLength }, carryover(this, 'minLength' as never)), + { minLength }, + ) + } + ArraySchema.max = function arrayMax(maxLength: Max) { + return Object_assign( + boundedArray(x, { lte: maxLength }, carryover(this, 'maxLength' as never)), + { maxLength }, + ) + } + ArraySchema.between = function arrayBetween( + min: Min, + max: Max, + minLength = Math_min(min, max), + maxLength = Math_max(min, max) + ) { + return Object_assign( + boundedArray(x, { gte: minLength, lte: maxLength }), + { minLength, maxLength }, + ) + } + if (has('minLength', Number_isSafeInteger)(prev)) ArraySchema.minLength = prev.minLength + if (has('maxLength', Number_isSafeInteger)(prev)) ArraySchema.maxLength = prev.maxLength + Object_assign(ArraySchema, userDefinitions) + return Object_assign(ArraySchema, bindUserExtensions(ArraySchema, userExtensions)) + } +} + +export declare namespace array { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.array + get def(): S + _type: S['_type' & keyof S][] + minLength?: number + maxLength?: number + min>(minLength: Min): array.Min + max>(maxLength: Max): array.Max + between, Max extends Integer>(minLength: Min, maxLength: Max): array.between<[min: Min, max: Max], S> + } + type Min + = [Self] extends [{ maxLength: number }] + ? array.between<[min: Min, max: Self['maxLength']], Self['def' & keyof Self]> + : array.min + ; + type Max + = [Self] extends [{ minLength: number }] + ? array.between<[min: Self['minLength'], max: Max], Self['def' & keyof Self]> + : array.max + ; + interface min extends array { minLength: Min } + interface max extends array { maxLength: Max } + interface between extends array { minLength: Bounds[0], maxLength: Bounds[1] } + type type = never | T +} + +export const readonlyArray: { + (schema: S): readonlyArray + (schema: S): readonlyArray> +} = array +export interface readonlyArray { + (u: unknown): u is this['_type'] + tag: URI.array + def: S + _type: ReadonlyArray +} diff --git a/packages/schema/src/__schemas__/bigint.ts b/packages/schema/src/__schemas__/bigint.ts new file mode 100644 index 00000000..39609a10 --- /dev/null +++ b/packages/schema/src/__schemas__/bigint.ts @@ -0,0 +1,151 @@ +/** + * bigint_ schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { Bounds, Equal, Unknown } from '@traversable/registry' +import { + bindUserExtensions, + carryover, + Object_assign, + Object_is, + URI, + withinBig as within +} from '@traversable/registry' +import type { ValidationError, ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' +import type { t } from '../_exports.js' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: bigint, right: bigint): boolean { + return Object_is(left, right) +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function bigintToJsonSchema(): void { + return void 0 + } + return bigintToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'bigint' } +export function toString(): 'bigint' { return 'bigint' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(bigIntSchema: S): validate { + validateBigInt.tag = URI.bigint + function validateBigInt(u: unknown, path = Array.of()): true | ValidationError[] { + return bigIntSchema(u) || [NullaryErrors.bigint(u, path)] + } + return validateBigInt +} +/// validate /// +////////////////////// + +export { bigint_ as bigint } + +/** @internal */ +function boundedBigInt(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & bigint_ +function boundedBigInt(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & bigint_ +function boundedBigInt(bounds: Bounds, carry?: {}): {} { + return Object_assign(function BoundedBigIntSchema(u: unknown) { + return bigint_(u) && within(bounds)(u) + }, carry, bigint_) +} + +interface bigint_ extends bigint_.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let userDefinitions: Record = { + equals, + toJsonSchema, + toString, +} + +export let userExtensions: Record = { + validate, +} + +function BigIntSchema(src: unknown) { return typeof src === 'bigint' } +BigIntSchema.tag = URI.bigint +BigIntSchema.def = 0n + +const bigint_ = Object_assign( + BigIntSchema, + userDefinitions, +) as bigint_ + +bigint_.min = function bigIntMin(minimum) { + return Object_assign( + boundedBigInt({ gte: minimum }, carryover(this, 'minimum')), + { minimum }, + ) +} +bigint_.max = function bigIntMax(maximum) { + return Object_assign( + boundedBigInt({ lte: maximum }, carryover(this, 'maximum')), + { maximum }, + ) +} +bigint_.between = function bigIntBetween( + min, + max, + minimum = (max < min ? max : min), + maximum = (max < min ? min : max), +) { + return Object_assign( + boundedBigInt({ gte: minimum, lte: maximum }), + { minimum, maximum } + ) +} + +Object_assign( + bigint_, + bindUserExtensions(bigint_, userExtensions), +) + +declare namespace bigint_ { + interface core extends bigint_.methods { + (u: this['_type'] | Unknown): u is this['_type'] + _type: bigint + tag: URI.bigint + get def(): this['_type'] + minimum?: bigint + maximum?: bigint + } + type Min + = [Self] extends [{ maximum: bigint }] + ? bigint_.between<[min: X, max: Self['maximum']]> + : bigint_.min + + type Max + = [Self] extends [{ minimum: bigint }] + ? bigint_.between<[min: Self['minimum'], max: X]> + : bigint_.max + + interface methods { + min(minimum: Min): bigint_.Min + max(maximum: Max): bigint_.Max + between( + minimum: Min, + maximum: Max + ): bigint_.between<[min: Min, max: Max]> + } + interface min extends bigint_ { minimum: Min } + interface max extends bigint_ { maximum: Max } + interface between extends bigint_ { minimum: Bounds[0], maximum: Bounds[1] } +} diff --git a/packages/schema/src/__schemas__/boolean.ts b/packages/schema/src/__schemas__/boolean.ts new file mode 100644 index 00000000..b34a306a --- /dev/null +++ b/packages/schema/src/__schemas__/boolean.ts @@ -0,0 +1,83 @@ +/** + * boolean_ schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { Equal, Unknown } from '@traversable/registry' +import { Object_assign, Object_is, URI } from '@traversable/registry' +import type { t } from '../_exports.js' +import { NullaryErrors, type ValidationFn } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: boolean, right: boolean): boolean { + return Object_is(left, right) +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): { type: 'boolean' } } +export function toJsonSchema(): toJsonSchema { + function booleanToJsonSchema() { return { type: 'boolean' as const } } + return booleanToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'boolean' } +export function toString(): 'boolean' { return 'boolean' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(booleanSchema: boolean_): validate { + validateBoolean.tag = URI.boolean + function validateBoolean(u: unknown, path = Array.of()) { + return booleanSchema(true as const) || [NullaryErrors.null(u, path)] + } + return validateBoolean +} +/// validate /// +////////////////////// + +export { boolean_ as boolean } + +interface boolean_ extends boolean_.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let userDefinitions: Record = { + equals, + toJsonSchema, + toString, +} + +export let userExtensions: Record = { + validate, +} + +function BooleanSchema(src: unknown): src is boolean { return typeof src === 'boolean' } + +BooleanSchema.tag = URI.boolean +BooleanSchema.def = false + +const boolean_ = Object_assign( + BooleanSchema, + userDefinitions, +) as boolean_ + +Object_assign(boolean_, userExtensions) + +declare namespace boolean_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.boolean + _type: boolean + get def(): this['_type'] + } +} diff --git a/packages/schema/src/__schemas__/eq.ts b/packages/schema/src/__schemas__/eq.ts new file mode 100644 index 00000000..2508f290 --- /dev/null +++ b/packages/schema/src/__schemas__/eq.ts @@ -0,0 +1,122 @@ +/** + * eq schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { + Key, + Mut, + Mutable, + SchemaOptions as Options, + Unknown +} from '@traversable/registry' +import { + _isPredicate, + applyOptions, + bindUserExtensions, + Equal, + getConfig, + laxEquals, + Object_assign, + URI +} from '@traversable/registry' +import type { t } from '../_exports.js' +import { stringify } from '@traversable/schema-to-string' +import type { Validate } from '@traversable/derive-validators' +import { Errors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = never | Equal +export function equals(eqSchema: eq): equals +export function equals(): Equal { + return function eqEquals(left: any, right: any) { + return laxEquals(left, right) + } +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): { const: T } } +export function toJsonSchema(eqSchema: eq): toJsonSchema +export function toJsonSchema({ def }: eq) { + function eqToJsonSchema() { return { const: def } } + return eqToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { + (): [Key] extends [never] + ? [T] extends [symbol] ? 'symbol' : 'symbol' + : [T] extends [string] ? `'${T}'` : Key +} + +export function toString(eqSchema: eq): toString +export function toString({ def }: eq): () => string { + function eqToString(): string { + return typeof def === 'symbol' ? 'symbol' : stringify(def) + } + return eqToString +} +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = Validate +export function validate(eqSchema: eq): validate +export function validate({ def }: eq): validate { + validateEq.tag = URI.eq + function validateEq(u: unknown, path = Array.of()) { + let options = getConfig().schema + let equals = options?.eq?.equalsFn || Equal.lax + if (equals(def, u)) return true + else return [Errors.eq(u, path, def)] + } + return validateEq +} +/// validate /// +////////////////////// + +export function eq>(value: V, options?: Options): eq> +export function eq(value: V, options?: Options): eq +export function eq(value: V, options?: Options): eq { + return eq.def(value, options) +} + +export interface eq extends eq.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export namespace eq { + export let userDefinitions: Record = { + } + export function def(value: T, options?: Options): eq + export function def(x: T, $?: Options): {} { + let userExtensions: Record = { + toString, + equals, + toJsonSchema, + validate, + } + const options = applyOptions($) + const predicate = _isPredicate(x) ? x : (y: unknown) => options.eq.equalsFn(x, y) + function EqSchema(src: unknown) { return predicate(src) } + EqSchema.tag = URI.eq + EqSchema.def = x + Object_assign(EqSchema, eq.userDefinitions) + return Object_assign(EqSchema, bindUserExtensions(EqSchema, userExtensions)) + } +} + +export declare namespace eq { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.eq + _type: V + get def(): V + } +} diff --git a/packages/schema/src/__schemas__/integer.ts b/packages/schema/src/__schemas__/integer.ts new file mode 100644 index 00000000..dfa8344b --- /dev/null +++ b/packages/schema/src/__schemas__/integer.ts @@ -0,0 +1,172 @@ +/** + * integer schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { + Bounds, + Equal, + Force, + Integer, + PickIfDefined, + Unknown +} from '@traversable/registry' +import { + bindUserExtensions, + carryover, + Math_max, + Math_min, + Number_isSafeInteger, + Object_assign, + SameValueNumber, + URI, + within +} from '@traversable/registry' +import type { t } from '../_exports.js' +import type { NumericBounds } from '@traversable/schema-to-json-schema' +import { getNumericBounds } from '@traversable/schema-to-json-schema' +import type { ValidationError, ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: number, right: number): boolean { + return SameValueNumber(left, right) +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): Force<{ type: 'integer' } & PickIfDefined> } + +export function toJsonSchema(schema: S): toJsonSchema +export function toJsonSchema(schema: integer): toJsonSchema { + function integerToJsonSchema() { + const { exclusiveMaximum, exclusiveMinimum, maximum, minimum } = getNumericBounds(schema) + let bounds: NumericBounds = {} + if (typeof exclusiveMinimum === 'number') bounds.exclusiveMinimum = exclusiveMinimum + if (typeof exclusiveMaximum === 'number') bounds.exclusiveMaximum = exclusiveMaximum + if (typeof minimum === 'number') bounds.minimum = minimum + if (typeof maximum === 'number') bounds.maximum = maximum + return { + type: 'integer' as const, + ...bounds, + } + } + return integerToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'number' } +export function toString(): 'number' { return 'number' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(integerSchema: S): validate { + validateInteger.tag = URI.integer + function validateInteger(u: unknown, path = Array.of()): true | ValidationError[] { + return integerSchema(u) || [NullaryErrors.integer(u, path)] + } + return validateInteger +} +/// validate /// +////////////////////// + +export { integer } + +/** @internal */ +function boundedInteger(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & integer +function boundedInteger(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & integer +function boundedInteger(bounds: Bounds, carry?: {}): {} { + return Object_assign(function BoundedIntegerSchema(u: unknown) { + return integer(u) && within(bounds)(u) + }, carry, integer) +} + +interface integer extends integer.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let userDefinitions: Record = { + equals, + toString, +} + +export let userExtensions: Record = { + toJsonSchema, + validate, +} + +function IntegerSchema(src: unknown) { return Number_isSafeInteger(src) } +IntegerSchema.tag = URI.integer +IntegerSchema.def = 0 + +const integer = Object_assign( + IntegerSchema, + userDefinitions, +) as integer + +integer.min = function integerMin(minimum) { + return Object_assign( + boundedInteger({ gte: minimum }, carryover(this, 'minimum')), + { minimum }, + ) +} +integer.max = function integerMax(maximum) { + return Object_assign( + boundedInteger({ lte: maximum }, carryover(this, 'maximum')), + { maximum }, + ) +} +integer.between = function integerBetween( + min, + max, + minimum = Math_min(min, max), + maximum = Math_max(min, max), +) { + return Object_assign( + boundedInteger({ gte: minimum, lte: maximum }), + { minimum, maximum }, + ) +} + +Object_assign( + integer, + bindUserExtensions(integer, userExtensions), +) + +declare namespace integer { + interface core extends integer.methods { + (u: this['_type'] | Unknown): u is this['_type'] + _type: number + tag: URI.integer + get def(): this['_type'] + minimum?: number + maximum?: number + } + interface methods { + min>(minimum: Min): integer.Min + max>(maximum: Max): integer.Max + between, const Max extends Integer>( + minimum: Min, + maximum: Max + ): integer.between<[min: Min, max: Max]> + } + type Min + = [Self] extends [{ maximum: number }] + ? integer.between<[min: X, max: Self['maximum']]> + : integer.min + type Max + = [Self] extends [{ minimum: number }] + ? integer.between<[min: Self['minimum'], max: X]> + : integer.max + interface min extends integer { minimum: Min } + interface max extends integer { maximum: Max } + interface between extends integer { minimum: Bounds[0], maximum: Bounds[1] } +} diff --git a/packages/schema/src/__schemas__/intersect.ts b/packages/schema/src/__schemas__/intersect.ts new file mode 100644 index 00000000..8170fbbf --- /dev/null +++ b/packages/schema/src/__schemas__/intersect.ts @@ -0,0 +1,147 @@ +/** + * intersect schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { + Equal, + Join, + Returns, + Unknown +} from '@traversable/registry' +import { + _isPredicate, + Array_isArray, + bindUserExtensions, + intersect as intersect$, + isUnknown as isAny, + Object_assign, + Object_is, + URI +} from '@traversable/registry' +import type { + Entry, + IntersectType, + Schema, + SchemaLike +} from '../_namespace.js' +import type { t } from '../_exports.js' +import { getSchema } from '@traversable/schema-to-json-schema' +import { callToString } from '@traversable/schema-to-string' +import type { Validate, ValidationError, Validator } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(intersectSchema: intersect<[...S]>): equals +export function equals(intersectSchema: intersect<[...S]>): equals +export function equals({ def }: intersect<{ equals: Equal }[]>): Equal { + function intersectEquals(l: unknown, r: unknown): boolean { + if (Object_is(l, r)) return true + for (let ix = def.length; ix-- !== 0;) + if (!def[ix].equals(l, r)) return false + return true + } + return intersectEquals +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { + (): { + allOf: { [I in keyof T]: Returns } + } +} + +export function toJsonSchema(intersectSchema: intersect): toJsonSchema +export function toJsonSchema(intersectSchema: intersect): toJsonSchema +export function toJsonSchema({ def }: intersect): () => {} { + function intersectToJsonSchema() { + return { + allOf: def.map(getSchema) + } + } + return intersectToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { + (): never | [T] extends [readonly []] ? 'unknown' + /* @ts-expect-error */ + : `(${Join<{ [I in keyof T]: Returns }, ' & '>})` +} + +export function toString(intersectSchema: intersect): toString +export function toString({ def }: intersect): () => string { + function intersectToString() { + return Array_isArray(def) ? def.length === 0 ? 'never' : `(${def.map(callToString).join(' & ')})` : 'unknown' + } + return intersectToString +} +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = Validate + +export function validate(intersectSchema: intersect): validate +export function validate(intersectSchema: intersect): validate +export function validate({ def }: intersect) { + validateIntersect.tag = URI.intersect + function validateIntersect(u: unknown, path = Array.of()): true | ValidationError[] { + let errors = Array.of() + for (let i = 0; i < def.length; i++) { + let results = def[i].validate(u, path) + if (results !== true) + for (let j = 0; j < results.length; j++) errors.push(results[j]) + } + return errors.length === 0 || errors + } + return validateIntersect +} +/// validate /// +////////////////////// + +export function intersect(...schemas: S): intersect +export function intersect }>(...schemas: S): intersect +export function intersect(...schemas: readonly unknown[]) { + return intersect.def(schemas) +} + +export interface intersect extends intersect.core { + toString: toString + equals: equals + toJsonSchema: toJsonSchema + validate: validate +} + +export namespace intersect { + export let userDefinitions: Record = { + } as intersect + export function def(xs: readonly [...T]): intersect + export function def(xs: readonly unknown[]): {} { + let userExtensions: Record = { + toJsonSchema, + validate, + toString, + equals, + } + const predicate = xs.every(_isPredicate) ? intersect$(xs) : isAny + function IntersectSchema(src: unknown) { return predicate(src) } + IntersectSchema.tag = URI.intersect + IntersectSchema.def = xs + Object_assign(IntersectSchema, intersect.userDefinitions) + return Object_assign(IntersectSchema, bindUserExtensions(IntersectSchema, userExtensions)) + } +} + +export declare namespace intersect { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.intersect + get def(): S + _type: IntersectType + } + type type> = never | T +} diff --git a/packages/schema/src/__schemas__/never.ts b/packages/schema/src/__schemas__/never.ts new file mode 100644 index 00000000..57f7ab19 --- /dev/null +++ b/packages/schema/src/__schemas__/never.ts @@ -0,0 +1,80 @@ +/** + * never_ schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { Equal, Unknown } from '@traversable/registry' +import { Object_assign, URI } from '@traversable/registry' +import type { t } from '../_exports.js' +import { NullaryErrors, type ValidationFn } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: never, right: never): boolean { + return false +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): never } +export function toJsonSchema(): toJsonSchema { + function neverToJsonSchema() { return void 0 as never } + return neverToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'never' } +export function toString(): 'never' { return 'never' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(_?: never_): validate { + validateNever.tag = URI.never + function validateNever(u: unknown, path = Array.of()) { return [NullaryErrors.never(u, path)] } + return validateNever +} +/// validate /// +////////////////////// + +export { never_ as never } + +export let userDefinitions: Record = { + equals, + toJsonSchema, + toString, +} + +export let userExtensions: Record = { + validate, +} + +interface never_ extends never_.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +function NeverSchema(src: unknown): src is never { return false } +NeverSchema.tag = URI.never; +NeverSchema.def = void 0 as never + +const never_ = Object_assign( + NeverSchema, + userDefinitions, +) as never_ + +Object_assign(never_, userExtensions) + +export declare namespace never_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.never + _type: never + get def(): this['_type'] + } +} diff --git a/packages/schema/src/__schemas__/null.ts b/packages/schema/src/__schemas__/null.ts new file mode 100644 index 00000000..e0e468d1 --- /dev/null +++ b/packages/schema/src/__schemas__/null.ts @@ -0,0 +1,86 @@ +/** + * null_ schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { Equal, Unknown } from '@traversable/registry' +import { Object_assign, Object_is, URI } from '@traversable/registry' +import type { t } from '../_exports.js' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: null, right: null): boolean { + return Object_is(left, right) +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): { type: 'null', enum: [null] } } +export function toJsonSchema(): toJsonSchema { + function nullToJsonSchema() { return { type: 'null' as const, enum: [null] satisfies [any] } } + return nullToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'null' } +export function toString(): 'null' { return 'null' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(nullSchema: null_): validate { + validateNull.tag = URI.null + function validateNull(u: unknown, path = Array.of()) { + return nullSchema(u) || [NullaryErrors.null(u, path)] + } + return validateNull +} +/// validate /// +////////////////////// + +export { null_ as null, null_ } + +export let userDefinitions: Record = { + equals, + toJsonSchema, + toString, +} + +export let userExtensions: Record = { + validate, +} + +interface null_ extends null_.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +function NullSchema(src: unknown): src is null { return src === null } +NullSchema.def = null +NullSchema.tag = URI.null + +const null_ = Object_assign( + NullSchema, + userDefinitions, +) as null_ + +Object_assign( + null_, + userExtensions, +) + +declare namespace null_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.null + _type: null + get def(): this['_type'] + } +} diff --git a/packages/schema/src/__schemas__/number.ts b/packages/schema/src/__schemas__/number.ts new file mode 100644 index 00000000..55db83f8 --- /dev/null +++ b/packages/schema/src/__schemas__/number.ts @@ -0,0 +1,210 @@ +/** + * number_ schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { + Bounds, + Equal, + Force, + PickIfDefined, + Unknown +} from '@traversable/registry' +import { + bindUserExtensions, + carryover, + Math_max, + Math_min, + Object_assign, + SameValueNumber, + URI, + within +} from '@traversable/registry' +import type { t } from '../_exports.js' +import type { NumericBounds } from '@traversable/schema-to-json-schema' +import { getNumericBounds } from '@traversable/schema-to-json-schema' +import type { ValidationError, ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: number, right: number): boolean { + return SameValueNumber(left, right) +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): Force<{ type: 'number' } & PickIfDefined> } + +export function toJsonSchema(schema: S): toJsonSchema +export function toJsonSchema(schema: number_): toJsonSchema { + function numberToJsonSchema() { + const { exclusiveMaximum, exclusiveMinimum, maximum, minimum } = getNumericBounds(schema) + let bounds: NumericBounds = {} + if (typeof exclusiveMinimum === 'number') bounds.exclusiveMinimum = exclusiveMinimum + if (typeof exclusiveMaximum === 'number') bounds.exclusiveMaximum = exclusiveMaximum + if (typeof minimum === 'number') bounds.minimum = minimum + if (typeof maximum === 'number') bounds.maximum = maximum + return { + type: 'number' as const, + ...bounds, + } + } + return numberToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'number' } +export function toString(): 'number' { return 'number' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(numberSchema: S): validate { + validateNumber.tag = URI.number + function validateNumber(u: unknown, path: (keyof any)[] = []): true | ValidationError[] { + return numberSchema(u) || [NullaryErrors.number(u, path)] + } + return validateNumber +} +/// validate /// +////////////////////// + +export { number_ as number } + +interface number_ extends number_.core { + toString: toString + equals: equals + toJsonSchema: toJsonSchema + validate: validate +} + +export let userDefinitions: Record = { + toString, + equals, +} + +export let userExtensions: Record = { + toJsonSchema, + validate, +} + +function NumberSchema(src: unknown) { return typeof src === 'number' } +NumberSchema.tag = URI.number +NumberSchema.def = 0 + +const number_ = Object_assign( + NumberSchema, + userDefinitions, +) as number_ + +number_.min = function numberMin(minimum) { + return Object_assign( + boundedNumber({ gte: minimum }, carryover(this, 'minimum')), + { minimum }, + ) +} +number_.max = function numberMax(maximum) { + return Object_assign( + boundedNumber({ lte: maximum }, carryover(this, 'maximum')), + { maximum }, + ) +} +number_.moreThan = function numberMoreThan(exclusiveMinimum) { + return Object_assign( + boundedNumber({ gt: exclusiveMinimum }, carryover(this, 'exclusiveMinimum')), + { exclusiveMinimum }, + ) +} +number_.lessThan = function numberLessThan(exclusiveMaximum) { + return Object_assign( + boundedNumber({ lt: exclusiveMaximum }, carryover(this, 'exclusiveMaximum')), + { exclusiveMaximum }, + ) +} +number_.between = function numberBetween( + min, + max, + minimum = Math_min(min, max), + maximum = Math_max(min, max), +) { + return Object_assign( + boundedNumber({ gte: minimum, lte: maximum }), + { minimum, maximum }, + ) +} + +Object_assign( + number_, + bindUserExtensions(number_, userExtensions), +) + +function boundedNumber(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & number_ +function boundedNumber(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & number_ +function boundedNumber(bounds: Bounds, carry?: {}): {} { + return Object_assign(function BoundedNumberSchema(u: unknown) { + return typeof u === 'number' && within(bounds)(u) + }, carry, number_) +} + +declare namespace number_ { + interface core extends number_.methods { + (u: this['_type'] | Unknown): u is this['_type'] + _type: number + tag: URI.number + get def(): this['_type'] + minimum?: number + maximum?: number + exclusiveMinimum?: number + exclusiveMaximum?: number + } + interface methods { + min(minimum: Min): number_.Min + max(maximum: Max): number_.Max + moreThan(moreThan: Min): ExclusiveMin + lessThan(lessThan: Max): ExclusiveMax + between( + minimum: Min, + maximum: Max + ): number_.between<[min: Min, max: Max]> + } + type Min + = [Self] extends [{ exclusiveMaximum: number }] + ? number_.minStrictMax<[min: X, max: Self['exclusiveMaximum']]> + : [Self] extends [{ maximum: number }] + ? number_.between<[min: X, max: Self['maximum']]> + : number_.min + + type Max + = [Self] extends [{ exclusiveMinimum: number }] + ? number_.maxStrictMin<[Self['exclusiveMinimum'], X]> + : [Self] extends [{ minimum: number }] + ? number_.between<[min: Self['minimum'], max: X]> + : number_.max + + type ExclusiveMin + = [Self] extends [{ exclusiveMaximum: number }] + ? number_.strictlyBetween<[X, Self['exclusiveMaximum']]> + : [Self] extends [{ maximum: number }] + ? number_.maxStrictMin<[min: X, Self['maximum']]> + : number_.moreThan + + type ExclusiveMax + = [Self] extends [{ exclusiveMinimum: number }] + ? number_.strictlyBetween<[Self['exclusiveMinimum'], X]> + : [Self] extends [{ minimum: number }] + ? number_.minStrictMax<[Self['minimum'], min: X]> + : number_.lessThan + + interface min extends number_ { minimum: Min } + interface max extends number_ { maximum: Max } + interface moreThan extends number_ { exclusiveMinimum: Min } + interface lessThan extends number_ { exclusiveMaximum: Max } + interface between extends number_ { minimum: Bounds[0], maximum: Bounds[1] } + interface minStrictMax extends number_ { minimum: Bounds[0], exclusiveMaximum: Bounds[1] } + interface maxStrictMin extends number_ { maximum: Bounds[1], exclusiveMinimum: Bounds[0] } + interface strictlyBetween extends number_ { exclusiveMinimum: Bounds[0], exclusiveMaximum: Bounds[1] } +} diff --git a/packages/schema/src/__schemas__/object.ts b/packages/schema/src/__schemas__/object.ts new file mode 100644 index 00000000..ef38f97c --- /dev/null +++ b/packages/schema/src/__schemas__/object.ts @@ -0,0 +1,331 @@ +/** + * object_ schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type * as T from '@traversable/registry' +import type { + Force, + Join, + Returns, + SchemaOptions as Options, + UnionToTuple, + Unknown +} from '@traversable/registry' +import { + _isPredicate, + applyOptions, + Array_isArray, + bindUserExtensions, + fn, + has, + isAnyObject, + object as object$, + Object_assign, + Object_hasOwn, + Object_is, + Object_keys, + record as record$, + symbol, + typeName, + URI +} from '@traversable/registry' +import type { + Entry, + Optional, + Required, + Schema, + SchemaLike +} from '../_namespace.js' +import type { t } from '../_exports.js' +import { getConfig } from '../_exports.js' +import type { RequiredKeys } from '@traversable/schema-to-json-schema' +import { isRequired, property } from '@traversable/schema-to-json-schema' +import type { ValidationError, ValidationFn, Validator } from '@traversable/derive-validators' +import { Errors, NullaryErrors, UnaryErrors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = never | T.Equal +export function equals(objectSchema: object_): equals> +export function equals(objectSchema: object_): equals> +export function equals({ def }: object_): equals> { + function objectEquals(l: { [x: string]: unknown }, r: { [x: string]: unknown }) { + if (Object_is(l, r)) return true + if (!l || typeof l !== 'object' || Array_isArray(l)) return false + if (!r || typeof r !== 'object' || Array_isArray(r)) return false + for (const k in def) { + const lHas = Object_hasOwn(l, k) + const rHas = Object_hasOwn(r, k) + if (lHas) { + if (!rHas) return false + if (!def[k].equals(l[k], r[k])) return false + } + if (rHas) { + if (!lHas) return false + if (!def[k].equals(l[k], r[k])) return false + } + if (!def[k].equals(l[k], r[k])) return false + } + return true + } + return objectEquals +} + +// export type equals = never | T.Equal +// export function equals(objectSchema: object_): equals> +// export function equals(objectSchema: object_): equals> +// export function equals({ def }: object_<{ [x: string]: { equals: T.Equal } }>): T.Equal<{ [x: string]: unknown }> { +// function objectEquals(l: { [x: string]: unknown }, r: { [x: string]: unknown }) { +// if (Object_is(l, r)) return true +// if (!l || typeof l !== 'object' || Array_isArray(l)) return false +// if (!r || typeof r !== 'object' || Array_isArray(r)) return false +// for (const k in def) { +// const lHas = Object_hasOwn(l, k) +// const rHas = Object_hasOwn(r, k) +// if (lHas) { +// if (!rHas) return false +// if (!def[k].equals(l[k], r[k])) return false +// } +// if (rHas) { +// if (!lHas) return false +// if (!def[k].equals(l[k], r[k])) return false +// } +// if (!def[k].equals(l[k], r[k])) return false +// } +// return true +// } +// return objectEquals +// } +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema = RequiredKeys> { + (): { + type: 'object' + required: { [I in keyof KS]: KS[I] & string } + properties: { [K in keyof T]: Returns } + } +} + +export function toJsonSchema(objectSchema: object_): toJsonSchema +export function toJsonSchema(objectSchema: object_): toJsonSchema +export function toJsonSchema({ def }: { def: { [x: string]: unknown } }): () => { type: 'object', required: string[], properties: {} } { + const required = Object_keys(def).filter(isRequired(def)) + function objectToJsonSchema() { + return { + type: 'object' as const, + required, + properties: fn.map(def, (v, k) => property(required)(v, k as number | string)), + } + } + return objectToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +/** @internal */ +type Symbol_optional = typeof Symbol_optional +const Symbol_optional: typeof symbol.optional = symbol.optional + +/** @internal */ +const hasOptionalSymbol = (u: unknown): u is { toString(): T } => + !!u && typeof u === 'function' + && Symbol_optional in u + && typeof u[Symbol_optional] === 'number' + +/** @internal */ +const hasToString = (x: unknown): x is { toString(): string } => + !!x && typeof x === 'function' && 'toString' in x && typeof x.toString === 'function' + +export interface toString> { + (): never + | [keyof T] extends [never] ? '{}' + /* @ts-expect-error */ + : `{ ${Join<{ [I in keyof _]: `'${_[I]}${T[_[I]] extends { [Symbol_optional]: any } ? `'?` : `'`}: ${ReturnType}` }, ', '>} }` +} + + +export function toString>(objectSchema: object_): toString +export function toString({ def }: object_) { + function objectToString() { + if (!!def && typeof def === 'object') { + const entries = Object.entries(def) + if (entries.length === 0) return '{}' + else return `{ ${entries.map(([k, x]) => `'${k}${hasOptionalSymbol(x) ? "'?" : "'" + }: ${hasToString(x) ? x.toString() : 'unknown' + }`).join(', ') + } }` + } + else return '{ [x: string]: unknown }' + } + + return objectToString +} +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +/** @internal */ +let isObject = (u: unknown): u is { [x: string]: unknown } => + !!u && typeof u === 'object' && !Array_isArray(u) + +/** @internal */ +let isKeyOf = (k: keyof any, u: T): k is keyof T => + !!u && (typeof u === 'function' || typeof u === 'object') && k in u + +/** @internal */ +let isOptional = has('tag', (tag) => tag === URI.optional) + + +export type validate = never | ValidationFn + +export function validate(objectSchema: object_): validate +export function validate(objectSchema: object_): validate +export function validate(objectSchema: object_): validate<{ [x: string]: unknown }> { + validateObject.tag = URI.object + function validateObject(u: unknown, path_ = Array.of()) { + // if (objectSchema(u)) return true + if (!isObject(u)) return [Errors.object(u, path_)] + let errors = Array.of() + let { schema: { optionalTreatment } } = getConfig() + let keys = Object_keys(objectSchema.def) + if (optionalTreatment === 'exactOptional') { + for (let i = 0, len = keys.length; i < len; i++) { + let k = keys[i] + let path = [...path_, k] + if (Object_hasOwn(u, k) && u[k] === undefined) { + if (isOptional(objectSchema.def[k].validate)) { + let tag = typeName(objectSchema.def[k].validate) + if (isKeyOf(tag, NullaryErrors)) { + let args = [u[k], path, tag] as never as [unknown, (keyof any)[]] + errors.push(NullaryErrors[tag](...args)) + } + else if (isKeyOf(tag, UnaryErrors)) { + errors.push(UnaryErrors[tag as keyof typeof UnaryErrors].invalid(u[k], path)) + } + } + let results = objectSchema.def[k].validate(u[k], path) + if (results === true) continue + let tag = typeName(objectSchema.def[k].validate) + if (isKeyOf(tag, NullaryErrors)) { + errors.push(NullaryErrors[tag](u[k], path, tag)) + } + else if (isKeyOf(tag, UnaryErrors)) { + errors.push(UnaryErrors[tag].invalid(u[k], path)) + } + errors.push(...results) + } + else if (Object_hasOwn(u, k)) { + let results = objectSchema.def[k].validate(u[k], path) + if (results === true) continue + errors.push(...results) + continue + } else { + errors.push(UnaryErrors.object.missing(u, path)) + continue + } + } + } + else { + for (let i = 0, len = keys.length; i < len; i++) { + let k = keys[i] + let path = [...path_, k] + if (!Object_hasOwn(u, k)) { + if (!isOptional(objectSchema.def[k].validate)) { + errors.push(UnaryErrors.object.missing(u, path)) + continue + } + else { + if (!Object_hasOwn(u, k)) continue + if (isOptional(objectSchema.def[k].validate) && Object_hasOwn(u, k)) { + if (u[k] === undefined) continue + let results = objectSchema.def[k].validate(u[k], path) + if (results === true) continue + for (let j = 0; j < results.length; j++) { + let result = results[j] + errors.push(result) + continue + } + } + } + } + let results = objectSchema.def[k].validate(u[k], path) + if (results === true) continue + for (let l = 0; l < results.length; l++) { + let result = results[l] + errors.push(result) + } + } + } + return errors.length === 0 || errors + } + + return validateObject +} +/// validate /// +////////////////////// + +export { object_ as object } + +function object_< + S extends { [x: string]: Schema }, + T extends { [K in keyof S]: Entry } +>(schemas: S, options?: Options): object_ +function object_< + S extends { [x: string]: SchemaLike }, + T extends { [K in keyof S]: Entry } +>(schemas: S, options?: Options): object_ +function object_(schemas: S, options?: Options) { + return object_.def(schemas, options) +} + +interface object_ extends object_.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +namespace object_ { + export let userDefinitions: Record = { + } as object_ + export function def(xs: T, $?: Options, opt?: string[]): object_ + export function def(xs: T, $?: Options, opt?: string[]): object_ + export function def(xs: { [x: string]: unknown }, $?: Options, opt_?: string[]): {} { + let userExtensions: Record = { + toString, + equals, + toJsonSchema, + validate, + } + const keys = Object_keys(xs) + const opt = Array_isArray(opt_) ? opt_ : keys.filter((k) => has(symbol.optional)(xs[k])) + const req = keys.filter((k) => !has(symbol.optional)(xs[k])) + const predicate = !record$(_isPredicate)(xs) ? isAnyObject : object$(xs, applyOptions($)) + function ObjectSchema(src: unknown) { return predicate(src) } + ObjectSchema.tag = URI.object + ObjectSchema.def = xs + ObjectSchema.opt = opt + ObjectSchema.req = req + Object_assign(ObjectSchema, userDefinitions) + return Object_assign(ObjectSchema, bindUserExtensions(ObjectSchema, userExtensions)) + } +} + +declare namespace object_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + _type: object_.type + tag: URI.object + get def(): S + opt: Optional // TODO: use object_.Opt? + req: Required // TODO: use object_.Req? + } + type Opt = symbol.optional extends keyof S[K] ? never : K + type Req = symbol.optional extends keyof S[K] ? K : never + type type = Force< + & { [K in keyof S as Opt]-?: S[K]['_type' & keyof S[K]] } + & { [K in keyof S as Req]+?: S[K]['_type' & keyof S[K]] } + > +} diff --git a/packages/schema/src/__schemas__/of.ts b/packages/schema/src/__schemas__/of.ts new file mode 100644 index 00000000..bda3e284 --- /dev/null +++ b/packages/schema/src/__schemas__/of.ts @@ -0,0 +1,94 @@ +/** + * of schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { Unknown } from '@traversable/registry' +import { Equal, Object_assign, URI } from '@traversable/registry' +import type { + Entry, + Guard, + Guarded, + SchemaLike +} from '../_namespace.js' +import type { t } from '../_exports.js' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: T, right: T): boolean { + return Equal.lax(left, right) +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function inlineToJsonSchema(): void { + return void 0 + } + return inlineToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'unknown' } +export function toString(): 'unknown' { return 'unknown' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(inlineSchema: of): validate { + validateInline.tag = URI.inline + function validateInline(u: unknown, path = Array.of()) { + return inlineSchema(u) || [NullaryErrors.inline(u, path)] + } + return validateInline +} +/// validate /// +////////////////////// + +export interface of extends of.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export function of(typeguard: S): Entry +export function of(typeguard: S): of +export function of(typeguard: (Guard) & { tag?: URI.inline, def?: Guard }) { + typeguard.def = typeguard + return Object_assign(typeguard, of.prototype) +} + +export namespace of { + export let userDefinitions: Record = { + equals, + toJsonSchema, + toString, + } + export let userExtensions: Record = { + validate, + } + export function def(guard: T): of + export function def(guard: T) { + function InlineSchema(src: unknown) { return guard(src) } + InlineSchema.tag = URI.inline + InlineSchema.def = guard + return InlineSchema + } +} + +export declare namespace of { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + _type: Guarded + tag: URI.inline + get def(): S + } + type type> = never | T +} diff --git a/packages/schema/src/__schemas__/optional.ts b/packages/schema/src/__schemas__/optional.ts new file mode 100644 index 00000000..1e57374a --- /dev/null +++ b/packages/schema/src/__schemas__/optional.ts @@ -0,0 +1,135 @@ +/** + * optional schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { + Equal, + Force, + Returns, + Unknown +} from '@traversable/registry' +import { + _isPredicate, + bindUserExtensions, + has, + isUnknown as isAny, + Object_assign, + Object_is, + optional as optional$, + symbol, + URI +} from '@traversable/registry' +import type { Entry, Schema, SchemaLike } from '../_namespace.js' +import type { t } from '../_exports.js' +import { getSchema, wrapOptional } from '@traversable/schema-to-json-schema' +import { callToString } from '@traversable/schema-to-string' +import type { Validate, ValidationFn, Validator } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = never | Equal +export function equals(optionalSchema: optional): equals +export function equals(optionalSchema: optional): equals +export function equals({ def }: optional<{ equals: Equal }>): Equal { + return function optionalEquals(l: unknown, r: unknown): boolean { + if (Object_is(l, r)) return true + return def.equals(l, r) + } +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +type Nullable = Force + +export interface toJsonSchema { + (): Nullable> + [symbol.optional]: number +} + +export function toJsonSchema(optionalSchema: optional): toJsonSchema +export function toJsonSchema({ def }: optional) { + function optionalToJsonSchema() { return getSchema(def) } + optionalToJsonSchema[symbol.optional] = wrapOptional(def) + return optionalToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { + /* @ts-expect-error */ + (): never | `(${ReturnType} | undefined)` +} + +export function toString(optionalSchema: optional): toString +export function toString({ def }: optional): () => string { + function optionalToString(): string { + return '(' + callToString(def) + ' | undefined)' + } + return optionalToString +} +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = Validate + +export function validate(optionalSchema: optional): validate +export function validate(optionalSchema: optional): validate +export function validate({ def }: optional): ValidationFn { + validateOptional.tag = URI.optional + validateOptional.optional = 1 + function validateOptional(u: unknown, path = Array.of()) { + if (u === void 0) return true + return def.validate(u, path) + } + return validateOptional +} +/// validate /// +////////////////////// + +export function optional(schema: S): optional +export function optional(schema: S): optional> +export function optional(schema: S): optional { return optional.def(schema) } + +export interface optional extends optional.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export namespace optional { + export let userDefinitions: Record = { + } + export function def(x: T): optional + export function def(x: T) { + let userExtensions: Record = { + toString, + equals, + toJsonSchema, + validate, + } + const predicate = _isPredicate(x) ? optional$(x) : isAny + function OptionalSchema(src: unknown) { return predicate(src) } + OptionalSchema.tag = URI.optional + OptionalSchema.def = x + OptionalSchema[symbol.optional] = 1 + Object_assign(OptionalSchema, { ...optional.userDefinitions, get def() { return x } }) + return Object_assign(OptionalSchema, bindUserExtensions(OptionalSchema, userExtensions)) + } + export const is + : (u: unknown) => u is optional + = has('tag', (u) => u === URI.optional) +} + +export declare namespace optional { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.optional + _type: undefined | S['_type' & keyof S] + def: S + [symbol.optional]: number + } + export type type = never | T +} diff --git a/packages/schema/src/__schemas__/record.ts b/packages/schema/src/__schemas__/record.ts new file mode 100644 index 00000000..7e8996cf --- /dev/null +++ b/packages/schema/src/__schemas__/record.ts @@ -0,0 +1,160 @@ +/** + * record schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type * as T from '@traversable/registry' +import type { Equal, Returns, Unknown } from '@traversable/registry' +import { + _isPredicate, + Array_isArray, + bindUserExtensions, + isAnyObject, + Object_assign, + Object_hasOwn, + Object_is, + Object_keys, + record as record$, + URI +} from '@traversable/registry' +import type { Entry, Schema, SchemaLike } from '../_namespace.js' +import type { t } from '../_exports.js' +import { getSchema } from '@traversable/schema-to-json-schema' +import { callToString } from '@traversable/schema-to-string' +import type { ValidationError, ValidationFn, Validator } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = never | Equal +export function equals(recordSchema: record): equals +export function equals(recordSchema: record): equals +export function equals({ def }: record<{ equals: Equal }>): Equal> { + function recordEquals(l: Record, r: Record): boolean { + if (Object_is(l, r)) return true + if (!l || typeof l !== 'object' || Array_isArray(l)) return false + if (!r || typeof r !== 'object' || Array_isArray(r)) return false + const lhs = Object_keys(l) + const rhs = Object_keys(r) + let len = lhs.length + let k: string + if (len !== rhs.length) return false + for (let ix = len; ix-- !== 0;) { + k = lhs[ix] + if (!Object_hasOwn(r, k)) return false + if (!(def.equals(l[k], r[k]))) return false + } + len = rhs.length + for (let ix = len; ix-- !== 0;) { + k = rhs[ix] + if (!Object_hasOwn(l, k)) return false + if (!(def.equals(l[k], r[k]))) return false + } + return true + } + return recordEquals +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { + (): { + type: 'object' + additionalProperties: T.Returns + } +} + +export function toJsonSchema(recordSchema: record): toJsonSchema +export function toJsonSchema(recordSchema: record): toJsonSchema +export function toJsonSchema({ def }: { def: unknown }): () => { type: 'object', additionalProperties: unknown } { + return function recordToJsonSchema() { + return { + type: 'object' as const, + additionalProperties: getSchema(def), + } + } +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { + /* @ts-expect-error */ + (): never | `Record}>` +} + +export function toString>(recordSchema: S): toString +export function toString(recordSchema: record): toString +export function toString({ def }: { def: unknown }): () => string { + function recordToString() { + return `Record` + } + return recordToString +} +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = never | ValidationFn +export function validate(recordSchema: record): validate +export function validate(recordSchema: record): validate +export function validate({ def: { validate = () => true } }: record) { + validateRecord.tag = URI.record + function validateRecord(u: unknown, path = Array.of()) { + if (!u || typeof u !== 'object' || Array_isArray(u)) return [NullaryErrors.record(u, path)] + let errors = Array.of() + let keys = Object_keys(u) + for (let k of keys) { + let y = u[k] + let results = validate(y, [...path, k]) + if (results === true) continue + else errors.push(...results) + } + return errors.length === 0 || errors + } + return validateRecord +} +/// validate /// +////////////////////// + +export function record(schema: S): record +export function record(schema: S): record> +export function record(schema: Schema) { + return record.def(schema) +} + +export interface record extends record.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export namespace record { + export let userDefinitions: Record = { + } + export function def(x: T): record + export function def(x: unknown): {} { + let userExtensions: Record = { + toString, + equals, + toJsonSchema, + validate, + } + const predicate = _isPredicate(x) ? record$(x) : isAnyObject + function RecordSchema(src: unknown) { return predicate(src) } + RecordSchema.tag = URI.record + RecordSchema.def = x + Object_assign(RecordSchema, record.userDefinitions) + return Object_assign(RecordSchema, bindUserExtensions(RecordSchema, userExtensions)) + } +} + +export declare namespace record { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.record + get def(): S + _type: Record + } + export type type> = never | T +} diff --git a/packages/schema/src/__schemas__/string.ts b/packages/schema/src/__schemas__/string.ts new file mode 100644 index 00000000..28b18d12 --- /dev/null +++ b/packages/schema/src/__schemas__/string.ts @@ -0,0 +1,173 @@ +/** + * string_ schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { + Bounds, + Equal, + Force, + Integer, + PickIfDefined, + Unknown +} from '@traversable/registry' +import { + bindUserExtensions, + carryover, + has, + Math_max, + Math_min, + Object_assign, + URI, + within +} from '@traversable/registry' +import type { t } from '../_exports.js' +import type { SizeBounds } from '@traversable/schema-to-json-schema' +import type { ValidationError, ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: string, right: string): boolean { + return left === right +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { + (): Force<{ type: 'string' } & PickIfDefined> +} + +export function toJsonSchema(schema: S): toJsonSchema +export function toJsonSchema(schema: string_): () => { type: 'string' } & Partial { + function stringToJsonSchema() { + const minLength = has('minLength', (u: any) => typeof u === 'number')(schema) ? schema.minLength : null + const maxLength = has('maxLength', (u: any) => typeof u === 'number')(schema) ? schema.maxLength : null + let out: { type: 'string' } & Partial = { type: 'string' } + minLength !== null && void (out.minLength = minLength) + maxLength !== null && void (out.maxLength = maxLength) + + return out + } + return stringToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'string' } +export function toString(): 'string' { return 'string' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(stringSchema: S): validate { + validateString.tag = URI.string + function validateString(u: unknown, path = Array.of()): true | ValidationError[] { + return stringSchema(u) || [NullaryErrors.number(u, path)] + } + return validateString +} +/// validate /// +////////////////////// + +export { string_ as string } + +/** @internal */ +function boundedString(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & string_ +function boundedString(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & string_ +function boundedString(bounds: Bounds, carry?: {}): {} { + return Object_assign(function BoundedStringSchema(u: unknown) { + return string_(u) && within(bounds)(u.length) + }, carry, string_) +} + +interface string_ extends string_.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let userDefinitions: Record = { + toString, + equals, +} + +export let userExtensions: Record = { + toJsonSchema, + validate, +} + +function StringSchema(src: unknown) { return typeof src === 'string' } +StringSchema.tag = URI.string +StringSchema.def = '' + +const string_ = Object_assign( + StringSchema, + userDefinitions, +) as string_ + +string_.min = function stringMinLength(minLength) { + return Object_assign( + boundedString({ gte: minLength }, carryover(this, 'minLength')), + { minLength }, + ) +} +string_.max = function stringMaxLength(maxLength) { + return Object_assign( + boundedString({ lte: maxLength }, carryover(this, 'maxLength')), + { maxLength }, + ) +} +string_.between = function stringBetween( + min, + max, + minLength = Math_min(min, max), + maxLength = Math_max(min, max)) { + return Object_assign( + boundedString({ gte: minLength, lte: maxLength }), + { minLength, maxLength }, + ) +} + +Object_assign( + string_, + bindUserExtensions(string_, userExtensions), +) + +declare namespace string_ { + interface core extends string_.methods { + (u: this['_type'] | Unknown): u is this['_type'] + _type: string + tag: URI.string + get def(): this['_type'] + } + interface methods { + minLength?: number + maxLength?: number + min>(minLength: Min): string_.Min + max>(maxLength: Max): string_.Max + between, const Max extends Integer>( + minLength: Min, + maxLength: Max + ): string_.between<[min: Min, max: Max]> + } + type Min + = [Self] extends [{ maxLength: number }] + ? string_.between<[min: Min, max: Self['maxLength']]> + : string_.min + + type Max + = [Self] extends [{ minLength: number }] + ? string_.between<[min: Self['minLength'], max: Max]> + : string_.max + + interface min extends string_ { minLength: Min } + interface max extends string_ { maxLength: Max } + interface between extends string_ { + minLength: Bounds[0] + maxLength: Bounds[1] + } +} diff --git a/packages/schema/src/__schemas__/symbol.ts b/packages/schema/src/__schemas__/symbol.ts new file mode 100644 index 00000000..605e3dc4 --- /dev/null +++ b/packages/schema/src/__schemas__/symbol.ts @@ -0,0 +1,83 @@ +/** + * symbol_ schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { Equal, Unknown } from '@traversable/registry' +import { Object_assign, Object_is, URI } from '@traversable/registry' +import type { t } from '../_exports.js' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: symbol, right: symbol): boolean { + return Object_is(left, right) +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function symbolToJsonSchema() { return void 0 } + return symbolToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'symbol' } +export function toString(): 'symbol' { return 'symbol' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(symbolSchema: symbol_): validate { + validateSymbol.tag = URI.symbol + function validateSymbol(u: unknown, path = Array.of()) { + return symbolSchema(true as const) || [NullaryErrors.symbol(u, path)] + } + return validateSymbol +} +/// validate /// +////////////////////// + +export { symbol_ as symbol } + +export let userDefinitions: Record = { + equals, + toJsonSchema, + toString, +} + +export let userExtensions: Record = { + validate, +} + +interface symbol_ extends symbol_.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +function SymbolSchema(src: unknown): src is symbol { return typeof src === 'symbol' } +SymbolSchema.tag = URI.symbol +SymbolSchema.def = Symbol() + +const symbol_ = Object_assign( + SymbolSchema, + userDefinitions, +) as symbol_ + +Object_assign(symbol_, userExtensions) + +declare namespace symbol_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.symbol + _type: symbol + get def(): this['_type'] + } +} diff --git a/packages/schema/src/__schemas__/tuple.ts b/packages/schema/src/__schemas__/tuple.ts new file mode 100644 index 00000000..4b95fcff --- /dev/null +++ b/packages/schema/src/__schemas__/tuple.ts @@ -0,0 +1,225 @@ +/** + * tuple schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { + Equal, + Join, + Returns, + SchemaOptions as Options, + TypeError, + Unknown +} from '@traversable/registry' +import { + _isPredicate, + Array_isArray, + bindUserExtensions, + getConfig, + has, + Object_assign, + Object_hasOwn, + Object_is, + parseArgs, + symbol, + tuple as tuple$, + URI +} from '@traversable/registry' +import type { + Entry, + FirstOptionalItem, + invalid, + Schema, + SchemaLike, + TupleType, + ValidateTuple +} from '../_namespace.js' +import type { optional } from './optional.js' +import type { t } from '../_exports.js' +import type { MinItems } from '@traversable/schema-to-json-schema' +import { applyTupleOptionality, minItems } from '@traversable/schema-to-json-schema' +import { hasToString } from '@traversable/schema-to-string' +import type { Validate, ValidationError, Validator } from '@traversable/derive-validators' +import { Errors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal + +export function equals(tupleSchema: tuple): equals +export function equals(tupleSchema: tuple): equals +export function equals(tupleSchema: tuple) { + function tupleEquals(l: typeof tupleSchema['_type'], r: typeof tupleSchema['_type']): boolean { + if (Object_is(l, r)) return true + if (Array_isArray(l)) { + if (!Array_isArray(r)) return false + for (let ix = tupleSchema.def.length; ix-- !== 0;) { + if (!Object_hasOwn(l, ix) && !Object_hasOwn(r, ix)) continue + if (Object_hasOwn(l, ix) && !Object_hasOwn(r, ix)) return false + if (!Object_hasOwn(l, ix) && Object_hasOwn(r, ix)) return false + if (Object_hasOwn(l, ix) && Object_hasOwn(r, ix)) { + if (!tupleSchema.def[ix].equals(l[ix], r[ix])) return false + } + } + return true + } + return false + } + return tupleEquals +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { + (): { + type: 'array', + items: { [I in keyof T]: Returns } + additionalItems: false + minItems: MinItems + maxItems: T['length' & keyof T] + } +} + +export function toJsonSchema(tupleSchema: tuple): toJsonSchema +export function toJsonSchema({ def }: tuple): () => { + type: 'array' + items: unknown + additionalItems: false + minItems?: {} + maxItems?: number +} { + function tupleToJsonSchema() { + let min = minItems(def) + let max = def.length + let items = applyTupleOptionality(def, { min, max }) + return { + type: 'array' as const, + additionalItems: false as const, + items, + minItems: min, + maxItems: max, + } + } + return tupleToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { + (): never | `[${Join<{ + [I in keyof T]: `${ + /* @ts-expect-error */ + T[I] extends { [Symbol_optional]: any } ? `_?: ${ReturnType}` : ReturnType + }` + }, ', '>}]` +} + +export function toString(tupleSchema: tuple): toString +export function toString(tupleSchema: tuple): () => string { + let isOptional = has('tag', (tag) => tag === URI.optional) + function stringToString() { + return Array_isArray(tupleSchema.def) + ? `[${tupleSchema.def.map( + (x) => isOptional(x) + ? `_?: ${hasToString(x) ? x.toString() : 'unknown'}` + : hasToString(x) ? x.toString() : 'unknown' + ).join(', ')}]` : 'unknown[]' + } + return stringToString +} +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = Validate +export function validate(tupleSchema: tuple<[...S]>): validate +export function validate(tupleSchema: tuple<[...S]>): validate +export function validate(tupleSchema: tuple<[...S]>): Validate { + validateTuple.tag = URI.tuple + let isOptional = has('tag', (tag) => tag === URI.optional) + function validateTuple(u: unknown, path = Array.of()) { + let errors = Array.of() + if (!Array_isArray(u)) return [Errors.array(u, path)] + for (let i = 0; i < tupleSchema.def.length; i++) { + if (!(i in u) && !(isOptional(tupleSchema.def[i].validate))) { + errors.push(Errors.missingIndex(u, [...path, i])) + continue + } + let results = tupleSchema.def[i].validate(u[i], [...path, i]) + if (results !== true) { + for (let j = 0; j < results.length; j++) errors.push(results[j]) + results.push(Errors.arrayElement(u[i], [...path, i])) + } + } + if (u.length > tupleSchema.def.length) { + for (let k = tupleSchema.def.length; k < u.length; k++) { + let excess = u[k] + errors.push(Errors.excessItems(excess, [...path, k])) + } + } + return errors.length === 0 || errors + } + return validateTuple +} +/// validate /// +////////////////////// + +export { tuple } + +function tuple }>(...schemas: tuple.validate): tuple, T>> +function tuple(...schemas: tuple.validate): tuple, S>> +function tuple }>(...args: [...schemas: tuple.validate, options: Options]): tuple, T>> +function tuple(...args: [...schemas: tuple.validate, options: Options]): tuple, S>> +function tuple }>(...schemas: tuple.validate): tuple, T>> +function tuple(...schemas: tuple.validate): tuple, S>> +function tuple(...args: [...SchemaLike[]] | [...SchemaLike[], Options]) { + return tuple.def(...parseArgs(getConfig().schema, args)) +} + +interface tuple extends tuple.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +namespace tuple { + export let userDefinitions: Record = { + } as tuple + export function def(xs: readonly [...T], $?: Options, opt_?: number): tuple + export function def(xs: readonly unknown[], $: Options = getConfig().schema, opt_?: number): {} { + let userExtensions: Record = { + toString, + equals, + toJsonSchema, + validate, + } + const opt = opt_ || xs.findIndex(has(symbol.optional)) + const options = { + ...$, minLength: $.optionalTreatment === 'treatUndefinedAndOptionalAsTheSame' ? -1 : xs.findIndex(has(symbol.optional)) + } satisfies tuple.InternalOptions + const predicate = !xs.every(_isPredicate) ? Array_isArray : tuple$(xs, options) + function TupleSchema(src: unknown) { return predicate(src) } + TupleSchema.tag = URI.tuple + TupleSchema.def = xs + TupleSchema.opt = opt + Object_assign(TupleSchema, tuple.userDefinitions) + return Object_assign(TupleSchema, bindUserExtensions(TupleSchema, userExtensions)) + } +} + +declare namespace tuple { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.tuple + _type: TupleType + opt: FirstOptionalItem + def: S + } + type type> = never | T + type InternalOptions = { minLength?: number } + type validate = ValidateTuple> + + type from + = TypeError extends V[number] ? { [I in keyof V]: V[I] extends TypeError ? invalid> : V[I] } : T +} diff --git a/packages/schema/src/__schemas__/undefined.ts b/packages/schema/src/__schemas__/undefined.ts new file mode 100644 index 00000000..9eaf8412 --- /dev/null +++ b/packages/schema/src/__schemas__/undefined.ts @@ -0,0 +1,83 @@ +/** + * undefined_ schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { Equal, Unknown } from '@traversable/registry' +import { Object_assign, Object_is, URI } from '@traversable/registry' +import type { t } from '../_exports.js' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: undefined, right: undefined): boolean { + return Object_is(left, right) +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function undefinedToJsonSchema(): void { return void 0 } + return undefinedToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'undefined' } +export function toString(): 'undefined' { return 'undefined' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(undefinedSchema: undefined_): validate { + validateUndefined.tag = URI.undefined + function validateUndefined(u: unknown, path = Array.of()) { + return undefinedSchema(u) || [NullaryErrors.undefined(u, path)] + } + return validateUndefined +} +/// validate /// +////////////////////// + +export { undefined_ as undefined } + +export let userDefinitions: Record = { + equals, + toJsonSchema, + toString, +} + +export let userExtensions: Record = { + validate, +} + +interface undefined_ extends undefined_.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +function UndefinedSchema(src: unknown): src is undefined { return src === void 0 } +UndefinedSchema.tag = URI.undefined +UndefinedSchema.def = void 0 as undefined + +const undefined_ = Object_assign( + UndefinedSchema, + userDefinitions, +) as undefined_ + +Object_assign(undefined_, userExtensions) + +declare namespace undefined_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.undefined + _type: undefined + get def(): this['_type'] + } +} diff --git a/packages/schema/src/__schemas__/union.ts b/packages/schema/src/__schemas__/union.ts new file mode 100644 index 00000000..a69abce7 --- /dev/null +++ b/packages/schema/src/__schemas__/union.ts @@ -0,0 +1,144 @@ +/** + * union schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { + Equal, + Join, + Returns, + Unknown +} from '@traversable/registry' +import { + _isPredicate, + Array_isArray, + bindUserExtensions, + isUnknown as isAny, + Object_assign, + Object_is, + union as union$, + URI +} from '@traversable/registry' +import type { Entry, Schema, SchemaLike } from '../_namespace.js' +import type { t } from '../_exports.js' +import { getSchema } from '@traversable/schema-to-json-schema' +import { callToString } from '@traversable/schema-to-string' +import type { Validate, ValidationError, Validator } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(unionSchema: union<[...S]>): equals +export function equals(unionSchema: union<[...S]>): equals +export function equals({ def }: union<{ equals: Equal }[]>): Equal { + function unionEquals(l: unknown, r: unknown): boolean { + if (Object_is(l, r)) return true + for (let ix = def.length; ix-- !== 0;) + if (def[ix].equals(l, r)) return true + return false + } + return unionEquals +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { + (): { anyOf: { [I in keyof T]: Returns } } +} + +export function toJsonSchema(unionSchema: union): toJsonSchema +export function toJsonSchema(unionSchema: union): toJsonSchema +export function toJsonSchema({ def }: union): () => {} { + return function unionToJsonSchema() { + return { + anyOf: def.map(getSchema) + } + } +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { + (): never | [T] extends [readonly []] ? 'never' + /* @ts-expect-error */ + : `(${Join<{ [I in keyof T]: ReturnType }, ' | '>})` +} + +export function toString(unionSchema: union): toString +export function toString({ def }: union): () => string { + function unionToString() { + return Array_isArray(def) ? def.length === 0 ? 'never' : `(${def.map(callToString).join(' | ')})` : 'unknown' + } + return unionToString +} +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = Validate + +export function validate(unionSchema: union): validate +export function validate(unionSchema: union): validate +export function validate({ def }: union) { + validateUnion.tag = URI.union + function validateUnion(u: unknown, path = Array.of()): true | ValidationError[] { + // if (this.def.every((x) => t.optional.is(x.validate))) validateUnion.optional = 1; + let errors = Array.of() + for (let i = 0; i < def.length; i++) { + let results = def[i].validate(u, path) + if (results === true) { + // validateUnion.optional = 0 + return true + } + for (let j = 0; j < results.length; j++) errors.push(results[j]) + } + // validateUnion.optional = 0 + return errors.length === 0 || errors + } + return validateUnion +} +/// validate /// +////////////////////// + +export function union(...schemas: S): union +export function union }>(...schemas: S): union +export function union(...schemas: unknown[]) { + return union.def(schemas) +} + +export interface union extends union.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export namespace union { + export let userDefinitions: Record = { + } as Partial> + export function def(xs: T): union + export function def(xs: unknown[]) { + let userExtensions: Record = { + toString, + equals, + toJsonSchema, + validate, + } + const predicate = xs.every(_isPredicate) ? union$(xs) : isAny + function UnionSchema(src: unknown): src is unknown { return predicate(src) } + UnionSchema.tag = URI.union + UnionSchema.def = xs + Object_assign(UnionSchema, union.userDefinitions) + return Object_assign(UnionSchema, bindUserExtensions(UnionSchema, userExtensions)) + } +} + +export declare namespace union { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.union + _type: union.type + get def(): S + } + type type = never | T +} diff --git a/packages/schema/src/__schemas__/unknown.ts b/packages/schema/src/__schemas__/unknown.ts new file mode 100644 index 00000000..852327b3 --- /dev/null +++ b/packages/schema/src/__schemas__/unknown.ts @@ -0,0 +1,80 @@ +/** + * unknown_ schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { Equal, Unknown } from '@traversable/registry' +import { Object_assign, Object_is, URI } from '@traversable/registry' +import type { t } from '../_exports.js' +import type { ValidationFn } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: any, right: any): boolean { + return Object_is(left, right) +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): { type: 'object', properties: {}, nullable: true } } +export function toJsonSchema(): toJsonSchema { + function anyToJsonSchema() { return { type: 'object', properties: {}, nullable: true } as const } + return anyToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'unknown' } +export function toString(): 'unknown' { return 'unknown' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(_?: unknown_): validate { + validateUnknown.tag = URI.unknown + function validateUnknown() { return true as const } + return validateUnknown +} +/// validate /// +////////////////////// + +export { unknown_ as unknown } + +export let userDefinitions: Record = { + equals, + toJsonSchema, + toString, +} + +export let userExtensions: Record = { + validate, +} + +interface unknown_ extends unknown_.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +function UnknownSchema(src: unknown): src is unknown { return true } +UnknownSchema.tag = URI.unknown +UnknownSchema.def = void 0 as unknown + +const unknown_ = Object_assign( + UnknownSchema, + userDefinitions, +) as unknown_ + +Object_assign(unknown_, userExtensions) + +declare namespace unknown_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.unknown + _type: unknown + get def(): this['_type'] + } +} diff --git a/packages/schema/src/__schemas__/void.ts b/packages/schema/src/__schemas__/void.ts new file mode 100644 index 00000000..4043f69b --- /dev/null +++ b/packages/schema/src/__schemas__/void.ts @@ -0,0 +1,85 @@ +/** + * void_ schema + * made with ᯓᡣ𐭩 by @traversable/schema@0.0.35 + */ +import type { Equal, Unknown } from '@traversable/registry' +import { Object_assign, Object_is, URI } from '@traversable/registry' +import type { t } from '../_exports.js' +import type { ValidationFn } from '@traversable/derive-validators' +import { NullaryErrors } from '@traversable/derive-validators' + //////////////////// +/// equals /// +export type equals = Equal +export function equals(left: void, right: void): boolean { + return Object_is(left, right) +} +/// equals /// +//////////////////// + ////////////////////////// +/// toJsonSchema /// +export interface toJsonSchema { (): void } +export function toJsonSchema(): toJsonSchema { + function voidToJsonSchema(): void { + return void 0 + } + return voidToJsonSchema +} +/// toJsonSchema /// +////////////////////////// + ////////////////////// +/// toString /// +export interface toString { (): 'void' } +export function toString(): 'void' { return 'void' } +/// toString /// +////////////////////// + ////////////////////// +/// validate /// +export type validate = ValidationFn +export function validate(voidSchema: void_): validate { + validateVoid.tag = URI.void + function validateVoid(u: unknown, path = Array.of()) { + return voidSchema(u) || [NullaryErrors.void(u, path)] + } + return validateVoid +} +/// validate /// +////////////////////// + +export { void_ as void, void_ } + +export let userDefinitions: Record = { + equals, + toJsonSchema, + toString, +} + +export let userExtensions: Record = { + validate, +} + +interface void_ extends void_.core { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +function VoidSchema(src: unknown): src is void { return src === void 0 } +VoidSchema.tag = URI.void +VoidSchema.def = void 0 as void + +const void_ = Object_assign( + VoidSchema, + userDefinitions, +) as void_ + +Object_assign(void_, userExtensions) + +declare namespace void_ { + interface core { + (u: this['_type'] | Unknown): u is this['_type'] + tag: URI.void + _type: void + get def(): this['_type'] + } +} diff --git a/packages/schema/src/_exports.ts b/packages/schema/src/_exports.ts new file mode 100644 index 00000000..51b9c459 --- /dev/null +++ b/packages/schema/src/_exports.ts @@ -0,0 +1,2 @@ +export type * as t from './_namespace.js' +export { getConfig } from '@traversable/schema-core' diff --git a/packages/schema/src/_namespace.ts b/packages/schema/src/_namespace.ts new file mode 100644 index 00000000..87a0d25c --- /dev/null +++ b/packages/schema/src/_namespace.ts @@ -0,0 +1,36 @@ +export type { + Entry, + FirstOptionalItem, + IntersectType, + Guard, + Guarded, + invalid, + Optional, + Required, + Schema, + SchemaLike, + TupleType, + ValidateTuple, +} from '@traversable/schema-core/namespace' + +export { any } from './__schemas__/any.js' +export { array } from './__schemas__/array.js' +export { bigint } from './__schemas__/bigint.js' +export { boolean } from './__schemas__/boolean.js' +export { eq } from './__schemas__/eq.js' +export { integer } from './__schemas__/integer.js' +export { intersect } from './__schemas__/intersect.js' +export { never } from './__schemas__/never.js' +export { null } from './__schemas__/null.js' +export { number } from './__schemas__/number.js' +export { object } from './__schemas__/object.js' +export { of } from './__schemas__/of.js' +export { optional } from './__schemas__/optional.js' +export { record } from './__schemas__/record.js' +export { string } from './__schemas__/string.js' +export { symbol } from './__schemas__/symbol.js' +export { tuple } from './__schemas__/tuple.js' +export { undefined } from './__schemas__/undefined.js' +export { union } from './__schemas__/union.js' +export { unknown } from './__schemas__/unknown.js' +export { void } from './__schemas__/void.js' diff --git a/packages/schema/src/build.ts b/packages/schema/src/build.ts new file mode 100755 index 00000000..ae98ab7e --- /dev/null +++ b/packages/schema/src/build.ts @@ -0,0 +1,462 @@ +#!/usr/bin/env pnpm dlx tsx +import type { IfUnaryReturns } from '@traversable/registry' +import * as path from 'node:path' +import * as fs from 'node:fs' +import { fn } from '@traversable/registry' +import { t } from '@traversable/schema-core' +import { generateSchemas } from '@traversable/schema-generator' + +import { VERSION } from './version.js' + +let CWD = process.cwd() +let PATH = { + libsDir: path.join(CWD, 'node_modules', '@traversable'), + tempDir: path.join(CWD, 'src', 'temp'), + extensionsDir: path.join(CWD, 'src', 'extensions'), + namespaceFile: path.join(CWD, 'src', '_namespace.ts'), + targetDir: path.join(CWD, 'src', '__schemas__'), +} + +let EXTENSION_FILES_IGNORE_LIST = [ + 'equals.ts', + 'toJsonSchema.ts', + 'toString.ts', + 'validate.ts', +] + +/** + * TODO: Derive this list from the {@link EXTENSION_FILES_IGNORE_LIST ignore list} + */ +let REMOVE_IMPORTS_LIST = [ + /.*equals.js'\n/, + /.*toJsonSchema.js'\n/, + /.*toString.js'\n/, + /.*validate.js'\n/, +] + +type Library = typeof Library[keyof typeof Library] +let Library = { + Core: 'schema-core', + Equals: 'derive-equals', + ToJsonSchema: 'schema-to-json-schema', + ToString: 'schema-to-string', + Validators: 'derive-validators', +} as const + +let LIB_NAME_TO_TARGET_FILENAME = { + [Library.Core]: 'core', + [Library.Equals]: 'equals', + [Library.ToJsonSchema]: 'toJsonSchema', + [Library.Validators]: 'validate', + [Library.ToString]: 'toString', +} as const satisfies Record + +let removeIgnoredImports = (content: string) => { + for (let ignore of REMOVE_IMPORTS_LIST) + content = content.replace(ignore, '') + return content +} + +let localSchemaNames = { + any: 'any_', + bigint: 'bigint_', + boolean: 'boolean_', + never: 'never_', + null: 'null_', + number: 'number_', + object: 'object_', + string: 'string_', + symbol: 'symbol_', + undefined: 'undefined_', + unknown: 'unknown_', + void: 'void_', + array: 'array', + eq: 'eq', + integer: 'integer', + intersect: 'intersect', + of: 'of', + optional: 'optional', + record: 'record', + tuple: 'tuple', + union: 'union', +} as Record + +let TargetReplace = { + internal: { + /** + * @example + * // from: + * import type { Guarded, Schema, SchemaLike } from '../../_namespace.js' + * // to: + * import type { Guarded, Schema, SchemaLike } from '../namespace.js' + */ + from: /'(\.\.\/)namespace.js'/g, + to: '\'../_namespace.js\'', + }, + namespace: { + from: /'@traversable\/schema-core'/g, + to: '\'../_exports.js\'', + }, + coverageDirective: { + from: /\s*\/\* v8 ignore .+ \*\//g, + to: '', + }, + selfReference: (schemaName: string) => { + let localSchemaName = localSchemaNames[schemaName] + return { + from: `t.${schemaName}`, + to: localSchemaName, + } + }, +} + +type Rewrite = (x: string) => string +let rewriteCoreInternalImport: Rewrite = (_) => _.replaceAll(TargetReplace.internal.from, TargetReplace.internal.to) +let rewriteCoreNamespaceImport: Rewrite = (_) => _.replaceAll(TargetReplace.namespace.from, TargetReplace.namespace.to) +let removeCoverageDirectives: Rewrite = (_) => _.replaceAll(TargetReplace.coverageDirective.from, TargetReplace.coverageDirective.to) +let rewriteSelfReferences: (schemaName: string) => Rewrite = (schemaName) => { + let { from, to } = TargetReplace.selfReference(schemaName) + return (_) => _.replaceAll(from, to) +} + +let isKeyOf = (k: keyof any, t: T): k is keyof T => + !!t && (typeof t === 'object' || typeof t === 'function') && k in t + +type GetTargetFileName = (libName: string, schemaName: string) => `${string}.ts` +type PostProcessor = (sourceFileContent: string, schemaName: string) => string + +type LibOptions = t.typeof +let LibOptions = t.object({ + relativePath: t.string, + getTargetFileName: (x): x is GetTargetFileName => typeof x === 'function', + // TODO: actually exclude files + excludeFiles: t.array(t.string), + includeFiles: t.optional(t.array(t.string)), +}) + +type BuildOptions = t.typeof +let BuildOptions = t.object({ + dryRun: t.optional(t.boolean), + skipCleanup: t.optional(t.boolean), + postProcessor: (x): x is PostProcessor => typeof x === 'function', + excludeSchemas: t.optional(t.union(t.array(t.string), t.null)), + getSourceDir: t.optional((x): x is (() => string) => typeof x === 'function'), + getNamespaceFile: t.optional((x): x is (() => string) => typeof x === 'function'), + getTempDir: t.optional((x): x is (() => string) => typeof x === 'function'), + getTargetDir: t.optional((x): x is (() => string) => typeof x === 'function'), + getExtensionFilesDir: t.optional((x): x is (() => string) => typeof x === 'function'), +}) + +type LibsOptions = never | { libs: Record> } +type LibsConfig = never | { libs: Record } +type ParseOptions = never | { [K in keyof T as K extends `get${infer P}` ? Uncapitalize

: K]-?: IfUnaryReturns } +type BuildConfig = ParseOptions + +type Options = + & BuildOptions + & LibsOptions + +type Config = + & BuildConfig + & LibsConfig + +let defaultGetTargetFileName = ( + (libName, _schemaName) => isKeyOf(libName, LIB_NAME_TO_TARGET_FILENAME) + ? `${LIB_NAME_TO_TARGET_FILENAME[libName]}.ts` as const + : `${libName}.ts` +) satisfies LibOptions['getTargetFileName'] + +let defaultPostProcessor = (content: string, schemaName: string) => fn.pipe( + content, + rewriteCoreInternalImport, + rewriteCoreNamespaceImport, + removeCoverageDirectives, + removeIgnoredImports, + rewriteSelfReferences(schemaName), +) + +let defaultLibOptions = { + relativePath: 'src/schemas', + excludeFiles: [], + getTargetFileName: defaultGetTargetFileName, +} satisfies LibOptions + +let defaultLibs = { + [Library.Core]: defaultLibOptions, + [Library.Equals]: defaultLibOptions, + [Library.ToJsonSchema]: defaultLibOptions, + [Library.ToString]: defaultLibOptions, + [Library.Validators]: defaultLibOptions, +} satisfies Record + +let defaultOptions = { + dryRun: false, + skipCleanup: false, + postProcessor: defaultPostProcessor, + excludeSchemas: null, + getExtensionFilesDir: () => PATH.extensionsDir, + getNamespaceFile: () => PATH.namespaceFile, + getSourceDir: () => PATH.libsDir, + getTempDir: () => PATH.tempDir, + getTargetDir: () => PATH.targetDir, + libs: defaultLibs, +} satisfies Required & LibsOptions + +function parseLibOptions({ + excludeFiles = defaultLibOptions.excludeFiles, + relativePath = defaultLibOptions.relativePath, + getTargetFileName = defaultLibOptions.getTargetFileName, + includeFiles, +}: Partial): LibOptions { + return { + excludeFiles, + relativePath, + getTargetFileName, + ...includeFiles && { includeFiles } + } +} + +function parseOptions(options: Options): Config +function parseOptions({ + dryRun = defaultOptions.dryRun, + excludeSchemas = null, + getExtensionFilesDir = defaultOptions.getExtensionFilesDir, + getNamespaceFile = defaultOptions.getNamespaceFile, + getSourceDir = defaultOptions.getSourceDir, + getTargetDir = defaultOptions.getTargetDir, + getTempDir = defaultOptions.getTempDir, + libs, + postProcessor = defaultOptions.postProcessor, + skipCleanup = defaultOptions.skipCleanup, +}: Options = defaultOptions): Config { + return { + dryRun, + excludeSchemas, + extensionFilesDir: getExtensionFilesDir(), + libs: fn.map(libs, parseLibOptions), + namespaceFile: getNamespaceFile(), + postProcessor, + skipCleanup, + sourceDir: getSourceDir(), + targetDir: getTargetDir(), + tempDir: getTempDir(), + } +} + +let tap + : (effect: (s: S) => T) => (x: S) => S + = (effect) => (x) => (effect(x), x) + +let ensureDir + : (dirpath: string, $: Config) => void + = (dirpath, $) => !$.dryRun + ? void (!fs.existsSync(dirpath) && fs.mkdirSync(dirpath)) + : void ( + console.group('[[DRY_RUN]]: `ensureDir`'), + console.debug('mkDir:', dirpath), + console.groupEnd() + ) + +function copyExtensionFiles($: Config) { + if (!fs.existsSync($.extensionFilesDir)) { + throw Error('Could not find extensions dir: ' + $.extensionFilesDir) + } + let filenames = fs + .readdirSync($.extensionFilesDir) + .filter((filename) => !EXTENSION_FILES_IGNORE_LIST.includes(filename)) + + filenames.forEach((filename) => { + let tempDirName = filename.slice(0, -'.ts'.length) + let tempDirPath = path.join($.tempDir, tempDirName) + let tempPath = path.join(tempDirPath, 'extension.ts') + let sourcePath = path.join($.extensionFilesDir, filename) + let content = fs.readFileSync(sourcePath).toString('utf8') + ensureDir(tempDirPath, $) + if ($.dryRun) { + console.group('\n\n[[DRY_RUN]]:: `copyExtensionFiles`') + console.debug('\ntempPath:\n', tempPath) + console.debug('\ncontent:\n', content) + console.groupEnd() + } else { + fs.writeFileSync(tempPath, content) + } + }) +} + +function buildSchemas($: Config): void { + let cache = new Set() + + return void fs.readdirSync( + path.join($.sourceDir), { withFileTypes: true }) + .filter(({ name }) => Object.keys($.libs).includes(name)) + .map( + (sourceDir) => { + let LIB_NAME = sourceDir.name + let LIB = $.libs[LIB_NAME] + return fn.pipe( + path.join( + sourceDir.parentPath, + LIB_NAME, + $.libs[LIB_NAME].relativePath, + ), + (schemasDir) => fs.readdirSync(schemasDir, { withFileTypes: true }), + fn.map( + (schemaFile) => { + let sourceFilePath = path.join(schemaFile.parentPath, schemaFile.name) + let sourceFileContent = fs.readFileSync(sourceFilePath).toString('utf8') + let targetFileName = LIB.getTargetFileName(LIB_NAME, schemaFile.name) + let schemaName = schemaFile.name.endsWith('.ts') + ? schemaFile.name.slice(0, -'.ts'.length) + : schemaFile.name + + let targetFilePath = path.join( + $.tempDir, + schemaName, + targetFileName + ) + + let tempDirPath = path.join( + $.tempDir, + schemaFile.name.slice(0, -'.ts'.length), + ) + + if (!cache.has(tempDirPath) && !$.dryRun) { + cache.add(tempDirPath) + ensureDir(tempDirPath, $) + } + + if (!$.dryRun) { + fs.writeFileSync( + targetFilePath, + sourceFileContent, + ) + } else { + console.group('\n\n[[DRY_RUN]]:: `buildSchemas`') + console.debug('\ntargetFilePath:\n', targetFilePath) + console.debug('\nsourceFileContent:\n', sourceFileContent) + console.groupEnd() + } + } + ), + ) + } + ) +} + +function getSourcePaths($: Config): Record> { + if (!fs.existsSync($.tempDir)) { + throw Error('[getSourcePaths] Expected temp directory to exist: ' + $.tempDir) + } + + return fs.readdirSync($.tempDir, { withFileTypes: true }) + .reduce( + (acc, { name, parentPath }) => ({ + ...acc, + [name]: fs + .readdirSync(path.join(parentPath, name), { withFileTypes: true }) + .reduce( + (acc, { name, parentPath }) => ({ + ...acc, + [name.slice(0, -'.ts'.length)]: path.join(parentPath, name) + }), + {} + ) + }), + {} + ) +} + +function createTargetPaths($: Config, sourcePaths: Record>) { + return fn.map(sourcePaths, (_, schemaName) => path.join($.targetDir, `${schemaName}.ts`)) +} + +export function writeSchemas($: Config, sources: Record>, targets: Record): void { + let schemas = generateSchemas(sources, targets, VERSION) + let schemaNames = Array.of() + for (let [target, generatedContent] of schemas) { + let pathSegments = target.split('/') + let fileName = pathSegments[pathSegments.length - 1] + let schemaName = fileName.endsWith('.ts') ? fileName.slice(0, -'.ts'.length) : fileName + void (schemaNames.push(schemaName)) + let content = $.postProcessor(generatedContent, schemaName) + if ($.dryRun) { + console.group('\n\n[[DRY_RUN]]:: `writeSchemas`') + console.debug('\ntarget:\n', target) + console.debug('\ncontent after post-processing:\n', content) + console.groupEnd() + } else { + fs.writeFileSync(target, content) + } + } +} + +function getNamespaceFileContent(previousContent: string, $: Config, sources: Record>) { + console.log('calling getNamespaceFileContent') + let targetDirNames = $.targetDir.split('/') + let targetDirName = targetDirNames[targetDirNames.length - 1] + let lines = Object.keys(sources).map((schemaName) => `export { ${schemaName} } from './${targetDirName}/${schemaName}.js'`) + return previousContent + '\r\n' + lines.join('\n') + '\r\n' +} + +export function writeNamespaceFile($: Config, sources: Record>) { + let namespaceFileContent = fs.readFileSync($.namespaceFile).toString('utf8') + let content = getNamespaceFileContent(namespaceFileContent, $, sources) + if (namespaceFileContent.includes('__schemas__')) { + if ($.dryRun) { + console.group('\n\n[[DRY_RUN]]:: `writeNamespaceFile`') + console.debug('\ntarget file already has term-level exports:\n', content) + console.groupEnd() + } else { + return void 0 + } + } + else if ($.dryRun) { + console.group('\n\n[[DRY_RUN]]:: `writeNamespaceFile`') + console.debug('\nnamespace file path:\n', $.namespaceFile) + console.debug('\nnamespace file content:\n', content) + console.groupEnd() + } else { + fs.writeFileSync($.namespaceFile, content) + } +} + +export function cleanupTempDir($: Config) { + if ($.dryRun) { + console.group('\n\n[[DRY_RUN]]:: `cleanupTempDir`') + console.debug('\ntemp dir path:\n', $.tempDir) + console.groupEnd() + } + else { + void fs.rmSync($.tempDir, { force: true, recursive: true }) + } +} + +function build(options: Options) { + let $ = parseOptions(options) + void ensureDir($.tempDir, $) + void copyExtensionFiles($) + buildSchemas($) + + let sources = getSourcePaths($) + let targets = createTargetPaths($, sources) + + if ($.dryRun) { + console.group('\n\n[[DRY_RUN]]:: `build`') + console.debug('\nsources:\n', sources) + console.debug('\ntargets:\n', targets) + console.groupEnd() + } + + void ensureDir($.targetDir, $) + void writeSchemas($, sources, targets) + void writeNamespaceFile($, sources) + if ($.skipCleanup) { + console.group('\n\n[[SKIP_CLEANUP]]: `build`\n') + console.debug('\n`build` received \'skipCleanup\': true. ' + $.tempDir + ' was not removed.') + console.groupEnd() + return void 0 + } + else void cleanupTempDir($) +} + +build(defaultOptions) diff --git a/packages/schema/src/clone.ts b/packages/schema/src/clone.ts deleted file mode 100644 index c954f122..00000000 --- a/packages/schema/src/clone.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Param } from '@traversable/registry' -import type { LowerBound } from './schema.js' - -export function clone(schema: S): S -export function clone(schema: S) { - function cloned(u: Param) { return schema(u) } - for (const k in schema) (cloned as typeof schema)[k] = schema[k] - return cloned -} - -// export function extend(): (schema: S, extension: Partial) => Ext -// export function extend() { -// return (schema: S, extension: Ext) => { -// function cloned(u: Param) { return schema(u) } -// for (const k in schema) (cloned as S)[k] = schema[k] -// for (const k in extension) (cloned as Ext)[k] = extension[k] -// return cloned -// } -// } diff --git a/packages/schema/src/exports.ts b/packages/schema/src/exports.ts index 4eba60d3..98b131c2 100644 --- a/packages/schema/src/exports.ts +++ b/packages/schema/src/exports.ts @@ -1,88 +1,2 @@ -export type { - Algebra, - Atoms, - Coalgebra, - Comparator, - Conform, - Const, - Dictionary, - Either, - Entries, - Force, - Functor, - HKT, - Identity, - IndexedAlgebra, - IndexedRAlgebra, - Intersect, - Join, - Kind, - Mut, - Mutable, - NonUnion, - Param, - Primitive, - RAlgebra, - Returns, - Showable, - Tuple, - Type, - TypeConstructor, - TypeError, - TypeName, - UnionToIntersection, - UnionToTuple, - inline, - newtype, - GlobalConfig, - SchemaConfig, - SchemaOptions, -} from '@traversable/registry' -export { - configure, - defaults, - getConfig, - applyOptions, -} from '@traversable/registry' - -export * as t from './namespace.js' - -export * from './extensions.js' - -export * as recurse from './recursive.js' - -export * as Equal from './equals.js' -export type Equal = import('@traversable/registry').Equal - -export * as Predicate from './predicates.js' -export type Predicate = [T] extends [never] - ? import('./schema.js').Predicate - : import('./types.js').Predicate - -export { clone } from './clone.js' - -export type { - Guard, - Typeguard, -} from './types.js' - -export { get, get$ } from './utils.js' - -export { VERSION } from './version.js' - -export { - /** @internal */ - replaceBooleanConstructor as __replaceBooleanConstructor, - /** @internal */ - carryover as __carryover, -} from './schema.js' -export { - /** @internal */ - within as __within, - /** @internal */ - withinBig as __withinBig, -} from './bounded.js' -export { - /** @internal */ - trim as __trim, -} from './recursive.js' +export * from './version.js' +export { getConfig } from '@traversable/schema-core' diff --git a/packages/schema/src/extensions.ts b/packages/schema/src/extensions.ts deleted file mode 100644 index 23f2ac3d..00000000 --- a/packages/schema/src/extensions.ts +++ /dev/null @@ -1,32 +0,0 @@ -export type { - LowerBound as t_LowerBound, - Schema as t_Schema, -} from './schema.js' -export { - enum as t_enum, -} from './enum.js' - -export { - never as t_never, - unknown as t_unknown, - void as t_void, - any as t_any, - null as t_null, - undefined as t_undefined, - symbol as t_symbol, - boolean as t_boolean, - integer as t_integer, - bigint as t_bigint, - number as t_number, - string as t_string, - eq as t_eq, - optional as t_optional, - array as t_array, - record as t_record, - object as t_object, - tuple as t_tuple, - union as t_union, - intersect as t_intersect, - of as t_of, - -} from './schema.js' diff --git a/packages/schema/src/extensions/any.ts b/packages/schema/src/extensions/any.ts new file mode 100644 index 00000000..c4ecbb88 --- /dev/null +++ b/packages/schema/src/extensions/any.ts @@ -0,0 +1,21 @@ +import { toJsonSchema } from './toJsonSchema.js' +import { validate } from './validate.js' +import { toString } from './toString.js' +import { equals } from './equals.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema/src/extensions/array.ts b/packages/schema/src/extensions/array.ts new file mode 100644 index 00000000..0927d4dd --- /dev/null +++ b/packages/schema/src/extensions/array.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + toString: toString + equals: equals + toJsonSchema: toJsonSchema + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toJsonSchema, + validate, + toString, + equals, +} diff --git a/packages/schema/src/extensions/bigint.ts b/packages/schema/src/extensions/bigint.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema/src/extensions/bigint.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema/src/extensions/boolean.ts b/packages/schema/src/extensions/boolean.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema/src/extensions/boolean.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema/src/extensions/eq.ts b/packages/schema/src/extensions/eq.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema/src/extensions/eq.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema/src/extensions/equals.ts b/packages/schema/src/extensions/equals.ts new file mode 100644 index 00000000..013153b1 --- /dev/null +++ b/packages/schema/src/extensions/equals.ts @@ -0,0 +1,3 @@ +export { dummyEquals as equals } +let dummyEquals = (..._: any) => { throw Error('Called dummy equals') } +interface dummyEquals<_ = any> { } diff --git a/packages/schema/src/extensions/integer.ts b/packages/schema/src/extensions/integer.ts new file mode 100644 index 00000000..1f68a7e8 --- /dev/null +++ b/packages/schema/src/extensions/integer.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toString, +} + +export let Extensions = { + toJsonSchema, + validate, +} diff --git a/packages/schema/src/extensions/intersect.ts b/packages/schema/src/extensions/intersect.ts new file mode 100644 index 00000000..0927d4dd --- /dev/null +++ b/packages/schema/src/extensions/intersect.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + toString: toString + equals: equals + toJsonSchema: toJsonSchema + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toJsonSchema, + validate, + toString, + equals, +} diff --git a/packages/schema/src/extensions/never.ts b/packages/schema/src/extensions/never.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema/src/extensions/never.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema/src/extensions/null.ts b/packages/schema/src/extensions/null.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema/src/extensions/null.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema/src/extensions/number.ts b/packages/schema/src/extensions/number.ts new file mode 100644 index 00000000..1a06ace0 --- /dev/null +++ b/packages/schema/src/extensions/number.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + toString: toString + equals: equals + toJsonSchema: toJsonSchema + validate: validate +} + +export let Definitions = { + toString, + equals, +} + +export let Extensions = { + toJsonSchema, + validate, +} diff --git a/packages/schema/src/extensions/object.ts b/packages/schema/src/extensions/object.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema/src/extensions/object.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema/src/extensions/of.ts b/packages/schema/src/extensions/of.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema/src/extensions/of.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema/src/extensions/optional.ts b/packages/schema/src/extensions/optional.ts new file mode 100644 index 00000000..0e7c4478 --- /dev/null +++ b/packages/schema/src/extensions/optional.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} + diff --git a/packages/schema/src/extensions/record.ts b/packages/schema/src/extensions/record.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema/src/extensions/record.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema/src/extensions/string.ts b/packages/schema/src/extensions/string.ts new file mode 100644 index 00000000..c64c1266 --- /dev/null +++ b/packages/schema/src/extensions/string.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + toString, + equals, +} + +export let Extensions = { + toJsonSchema, + validate, +} diff --git a/packages/schema/src/extensions/symbol.ts b/packages/schema/src/extensions/symbol.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema/src/extensions/symbol.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema/src/extensions/toJsonSchema.ts b/packages/schema/src/extensions/toJsonSchema.ts new file mode 100644 index 00000000..16341d68 --- /dev/null +++ b/packages/schema/src/extensions/toJsonSchema.ts @@ -0,0 +1,3 @@ +export { dummyToJsonSchema as toJsonSchema } +let dummyToJsonSchema = (..._: any) => { throw Error('Called dummy toJsonSchema') } +interface dummyToJsonSchema<_ = any> { } diff --git a/packages/schema/src/extensions/toString.ts b/packages/schema/src/extensions/toString.ts new file mode 100644 index 00000000..d7215f1f --- /dev/null +++ b/packages/schema/src/extensions/toString.ts @@ -0,0 +1,3 @@ +export { dummyToString as toString } +let dummyToString = (..._: any) => { throw Error('Called dummy toString') } +interface dummyToString<_ = any> { } diff --git a/packages/schema/src/extensions/tuple.ts b/packages/schema/src/extensions/tuple.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema/src/extensions/tuple.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema/src/extensions/undefined.ts b/packages/schema/src/extensions/undefined.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema/src/extensions/undefined.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema/src/extensions/union.ts b/packages/schema/src/extensions/union.ts new file mode 100644 index 00000000..ee871114 --- /dev/null +++ b/packages/schema/src/extensions/union.ts @@ -0,0 +1,20 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = {} + +export let Extensions = { + toString, + equals, + toJsonSchema, + validate, +} diff --git a/packages/schema/src/extensions/unknown.ts b/packages/schema/src/extensions/unknown.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema/src/extensions/unknown.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema/src/extensions/validate.ts b/packages/schema/src/extensions/validate.ts new file mode 100644 index 00000000..ba09b9fe --- /dev/null +++ b/packages/schema/src/extensions/validate.ts @@ -0,0 +1,3 @@ +export { dummyValidate as validate } +let dummyValidate = (..._: any) => { throw Error('Called dummy validate') } +interface dummyValidate<_ = any> { } diff --git a/packages/schema/src/extensions/void.ts b/packages/schema/src/extensions/void.ts new file mode 100644 index 00000000..7a2b8c12 --- /dev/null +++ b/packages/schema/src/extensions/void.ts @@ -0,0 +1,21 @@ +import { equals } from './equals.js' +import { toJsonSchema } from './toJsonSchema.js' +import { toString } from './toString.js' +import { validate } from './validate.js' + +export interface Types { + equals: equals + toJsonSchema: toJsonSchema + toString: toString + validate: validate +} + +export let Definitions = { + equals, + toJsonSchema, + toString, +} + +export let Extensions = { + validate, +} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 410a4bcb..e572e968 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -1 +1,3 @@ export * from './exports.js' +export * as t from './_namespace.js' +export * from './_namespace.js' diff --git a/packages/schema/src/namespace.ts b/packages/schema/src/namespace.ts deleted file mode 100644 index 92dc84d5..00000000 --- a/packages/schema/src/namespace.ts +++ /dev/null @@ -1,76 +0,0 @@ -export * as recurse from './recursive.js' - -export type { - bottom, - Boundable, - Entry, - F, - Fixpoint, - Free, - invalid, - Leaf, - LowerBound, - Predicate, - ReadonlyArray, - Schema, - Tag, - top, - typeOf as typeof, -} from './schema.js' - -export { - isLeaf, - isNullary, - isNullaryTag, - isBoundable, - isBoundableTag, - isPredicate, - isUnary, - isCore, - Functor, - IndexedFunctor, - fold, - foldWithIndex, - unfold, - tags, -} from './schema.js' - -export { key, has } from './has.js' - -/* data-types & combinators */ -export * from './combinators.js' -export { enum } from './enum.js' -export { - never, - unknown, - any, - void, - null, - undefined, - symbol, - boolean, - integer, - of, - bigint, - number, - string, - nonnullable, - eq, - optional, - array, - readonlyArray, - record, - union, - intersect, - tuple, - object, -} from './schema.js' - -/** - * exported as escape hatches, to prevent collisions with built-in keywords - */ -export type { - typeOf as typeof_, - null_, - void_, -} from './schema.js' diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts deleted file mode 100644 index ba45f4b8..00000000 --- a/packages/schema/src/schema.ts +++ /dev/null @@ -1,866 +0,0 @@ -import type * as T from '@traversable/registry' -import type { SchemaOptions as Options, TypeError } from '@traversable/registry' -import { applyOptions, fn, getConfig, has, omitMethods, parseArgs, symbol, URI } from '@traversable/registry' - -import type { - Guard, - Label, - Predicate as AnyPredicate, - Typeguard, - TypePredicate, - ValidateTuple, -} from './types.js' -import { is as guard } from './predicates.js' -import type { Bounds } from './bounded.js' -import { within, withinBig } from './bounded.js' - -/** @internal */ -const Object_assign = globalThis.Object.assign - -/** @internal */ -const Object_keys = globalThis.Object.keys - -/** @internal */ -const Array_isArray = globalThis.Array.isArray - -/** @internal */ -const Number_isSafeInteger - : (u: unknown) => u is number - = globalThis.Number.isSafeInteger as never - -/** @internal */ -const Math_min = globalThis.Math.min - -/** @internal */ -const Math_max = globalThis.Math.max - -/** @internal */ -export function replaceBooleanConstructor(fn: T): LowerBound -export function replaceBooleanConstructor(fn: T) { - return fn === globalThis.Boolean ? nonnullable : fn -} - -/** @internal */ -export function carryover(x: T, ...ignoreKeys: (keyof T)[]) { - let keys = Object.keys(x).filter((k) => !ignoreKeys.includes(k as never) && x[k as keyof typeof x] != null) - if (keys.length === 0) return {} - else { - let out: { [x: string]: unknown } = {} - for (let k of keys) out[k] = x[k as keyof typeof x] - return out - } -} - -export const isPredicate - : (src: unknown) => src is { (): boolean; (x: S): x is T } - = (src: unknown): src is never => typeof src === 'function' - -export type Source = T extends (_: infer S) => unknown ? S : unknown -export type Target = never | S extends (_: any) => _ is infer T ? T : S -export type Inline = never | of> -export type Predicate = AnyPredicate | Schema -export type Force = never | { -readonly [K in keyof T]: T[K] } -export type Optional = never | - string extends K ? string : K extends K ? S[K] extends bottom | optional ? K : never : never -export type FirstOptionalItem - = S extends readonly [infer H, ...infer T] ? optional extends H ? Offset['length'] : FirstOptionalItem : never - -export type Required = never | - string extends K ? string : K extends K ? S[K] extends bottom | optional ? never : K : never - -export type Entry - = [Schema] extends [S] ? Schema - : S extends { def: unknown } ? S - : S extends Guard ? of - : S extends globalThis.BooleanConstructor ? nonnullable - : S extends (() => infer _ extends boolean) - ? BoolLookup[`${_}`] - : S -export type BoolLookup = never | { - true: top - false: bottom - boolean: unknown_ -} - -export type IntersectType - = Todo extends readonly [infer H, ...infer T] ? IntersectType : Out -export type TupleType = never - | optional extends T[number & keyof T] - ? T extends readonly [infer Head, ...infer Tail] - ? [Head] extends [optional] ? Label< - { [ix in keyof Out]: Out[ix]['_type' & keyof Out[ix]] }, - { [ix in keyof T]: T[ix]['_type' & keyof T[ix]] } - > - : TupleType - : never - : { [ix in keyof T]: T[ix]['_type' & keyof T[ix]] } - - -export type typeOf< - T extends { _type?: unknown }, - _ extends - | T['_type'] - = T['_type'] -> = never | _ - -export interface LowerBound { - (u: unknown): u is any - tag?: string - def?: unknown - _type?: T -} - -export interface Schema - extends TypePredicate, Fn['_type']> { - tag?: Fn['tag'] - def?: Fn['def'] - _type?: Fn['_type'] -} - -export type Unary = - // | enum_ - | eq - | array - | record - | optional - | union - | intersect - | tuple - | object_<{ [x: string]: Unary }> - - -export type F = - | Leaf - // | enum_ - | eq - | array - | record - | optional - | union - | intersect - | tuple - | object_<{ [x: string]: T }> - -export type Fixpoint = - | Leaf - | Unary - -export interface Free extends T.HKT { [-1]: F } - -export function of(typeguard: S): of -export function of(typeguard: S): of> -export function of(typeguard: (Guard) & { tag?: URI.inline, def?: Guard }) { - typeguard.def = typeguard - return Object_assign(typeguard, of.prototype) -} -export interface of { - (u: unknown): u is this['_type'] - _type: Target - tag: URI.inline - def: S -} - -export namespace of { - export let prototype = { tag: URI.inline } - export type type> = never | T - export function def(guard: T): of - /* v8 ignore next 6 */ - export function def(guard: T) { - function InlineSchema(src: unknown) { return guard(src) } - InlineSchema.tag = URI.inline - InlineSchema.def = guard - return InlineSchema - } -} - -export interface top { tag: URI.top, readonly _type: unknown, def: this['_type'] } -export interface bottom { tag: URI.bottom, readonly _type: never, def: this['_type'] } -export interface invalid<_Err> extends TypeError<''>, never_ {} - -export { void_ as void, void_ } -interface void_ extends Typeguard { tag: URI.void, def: this['_type'] } -const void_ = function VoidSchema(src: unknown) { return src === void 0 } -void_.tag = URI.void -void_.def = void 0 - -export { null_ as null, null_ } -interface null_ extends Typeguard { tag: URI.null, def: this['_type'] } -const null_ = function NullSchema(src: unknown) { return src === null } -null_.tag = URI.null -null_.def = null - -export { never_ as never } -interface never_ extends Typeguard { tag: URI.never, def: this['_type'] } -const never_ = function NeverSchema(src: unknown) { return false } -never_.tag = URI.never -never_.def = void 0 as never - -export { unknown_ as unknown } -interface unknown_ extends Typeguard { tag: URI.unknown, def: this['_type'] } -const unknown_ = function UnknownSchema(src: unknown) { return true } -unknown_.tag = URI.unknown -unknown_.def = void 0 - -export { any_ as any } -interface any_ extends Typeguard { tag: URI.any, def: this['_type'] } -const any_ = function AnySchema(src: unknown) { return true } -any_.tag = URI.any -any_.def = void 0 - -export { undefined_ as undefined } -interface undefined_ extends Typeguard { tag: URI.undefined, def: this['_type'] } -const undefined_ = function UndefinedSchema(src: unknown) { return src === void 0 } -undefined_.tag = URI.undefined -undefined_.def = void 0 - -export { symbol_ as symbol } -interface symbol_ extends Typeguard { tag: URI.symbol, def: this['_type'] } -const symbol_ = function SymbolSchema(src: unknown) { return typeof src === 'symbol' } -symbol_.tag = URI.symbol -symbol_.def = Symbol() - -export { boolean_ as boolean } -interface boolean_ extends Typeguard { tag: URI.boolean, def: this['_type'] } -const boolean_ = function BooleanSchema(src: unknown) { return src === true || src === false } -boolean_.tag = URI.boolean -boolean_.def = false - -export { integer } -interface integer extends Typeguard, integer.methods { - tag: URI.integer - def: this['_type'] - minimum?: number - maximum?: number -} - -declare namespace integer { - type Min - = [Self] extends [{ maximum: number }] - ? integer.between<[min: X, max: Self['maximum']]> - : integer.min - type Max - = [Self] extends [{ minimum: number }] - ? integer.between<[min: Self['minimum'], max: X]> - : integer.max - interface methods { - min(minimum: Min): integer.Min - max(maximum: Max): integer.Max - between(minimum: Min, maximum: Max): integer.between<[min: Min, max: Max]> - } - interface min extends integer { minimum: Min } - interface max extends integer { maximum: Max } - interface between extends integer { minimum: Bounds[0], maximum: Bounds[1] } -} -const integer = function IntegerSchema(src: unknown) { return Number_isSafeInteger(src) } -integer.tag = URI.integer -integer.def = 0 -integer.min = function integerMin(minimum) { - return Object_assign( - boundedInteger({ gte: minimum }, carryover(this, 'minimum')), - { minimum }, - ) -} -integer.max = function integerMax(maximum) { - return Object_assign( - boundedInteger({ lte: maximum }, carryover(this, 'maximum')), - { maximum }, - ) -} -integer.between = function integerBetween( - min, - max, - minimum = Math_min(min, max), - maximum = Math_max(min, max), -) { - return Object_assign( - boundedInteger({ gte: minimum, lte: maximum }), - { minimum, maximum }, - ) -} - -export { bigint_ as bigint } -interface bigint_ extends Typeguard, bigint_.methods { - tag: URI.bigint - def: this['_type'] - minimum?: bigint - maximum?: bigint -} -declare namespace bigint_ { - type Min - = [Self] extends [{ maximum: bigint }] - ? bigint_.between<[min: X, max: Self['maximum']]> - : bigint_.min - - type Max - = [Self] extends [{ minimum: bigint }] - ? bigint_.between<[min: Self['minimum'], max: X]> - : bigint_.max - - interface methods extends Typeguard { - min(minimum: Min): bigint_.Min - max(maximum: Max): bigint_.Max - between(minimum: Min, maximum: Max): bigint_.between<[min: Min, max: Max]> - } - interface min extends bigint_ { minimum: Min } - interface max extends bigint_ { maximum: Max } - interface between extends bigint_ { minimum: Bounds[0], maximum: Bounds[1] } -} - -const bigint_ = function BigIntSchema(src: unknown) { return typeof src === 'bigint' } -bigint_.tag = URI.bigint -bigint_.def = 0n -bigint_.min = function bigIntMin(minimum) { - return Object_assign( - boundedBigInt({ gte: minimum }, carryover(this, 'minimum')), - { minimum }, - ) -} -bigint_.max = function bigIntMax(maximum) { - return Object_assign( - boundedBigInt({ lte: maximum }, carryover(this, 'maximum')), - { maximum }, - ) -} -bigint_.between = function bigIntBetween( - min, - max, - minimum = (max < min ? max : min), - maximum = (max < min ? min : max), -) { - return Object_assign( - boundedBigInt({ gte: minimum, lte: maximum }), - { minimum, maximum } - ) -} - -export { number_ as number } -interface number_ extends Typeguard, number_.methods { - tag: URI.number - def: this['_type'] - minimum?: number - maximum?: number - exclusiveMinimum?: number - exclusiveMaximum?: number -} -declare namespace number_ { - interface methods { - min(minimum: Min): number_.Min - max(maximum: Max): number_.Max - moreThan(moreThan: Min): ExclusiveMin - lessThan(lessThan: Max): ExclusiveMax - between(minimum: Min, maximum: Max): number_.between<[min: Min, max: Max]> - } - type Min - = [Self] extends [{ exclusiveMaximum: number }] - ? number_.minStrictMax<[min: X, max: Self['exclusiveMaximum']]> - : [Self] extends [{ maximum: number }] - ? number_.between<[min: X, max: Self['maximum']]> - : number_.min - - type Max - = [Self] extends [{ exclusiveMinimum: number }] - ? number_.maxStrictMin<[Self['exclusiveMinimum'], X]> - : [Self] extends [{ minimum: number }] - ? number_.between<[min: Self['minimum'], max: X]> - : number_.max - - type ExclusiveMin - = [Self] extends [{ exclusiveMaximum: number }] - ? number_.strictlyBetween<[X, Self['exclusiveMaximum']]> - : [Self] extends [{ maximum: number }] - ? number_.maxStrictMin<[min: X, Self['maximum']]> - : number_.moreThan - - type ExclusiveMax - = [Self] extends [{ exclusiveMinimum: number }] - ? number_.strictlyBetween<[Self['exclusiveMinimum'], X]> - : [Self] extends [{ minimum: number }] - ? number_.minStrictMax<[Self['minimum'], min: X]> - : number_.lessThan - - interface min extends number_ { minimum: Min } - interface max extends number_ { maximum: Max } - interface moreThan extends number_ { exclusiveMinimum: Min } - interface lessThan extends number_ { exclusiveMaximum: Max } - interface between extends number_ { minimum: Bounds[0], maximum: Bounds[1] } - interface minStrictMax extends number_ { minimum: Bounds[0], exclusiveMaximum: Bounds[1] } - interface maxStrictMin extends number_ { maximum: Bounds[1], exclusiveMinimum: Bounds[0] } - interface strictlyBetween extends number_ { exclusiveMinimum: Bounds[0], exclusiveMaximum: Bounds[1] } -} - -const number_ = function NumberSchema(src: unknown) { return typeof src === 'number' } -number_.tag = URI.number -number_.def = 0 -number_.min = function numberMin(minimum) { - return Object_assign( - boundedNumber({ gte: minimum }, carryover(this, 'minimum')), - { minimum }, - ) -} -number_.max = function numberMax(maximum) { - return Object_assign( - boundedNumber({ lte: maximum }, carryover(this, 'maximum')), - { maximum }, - ) -} -number_.moreThan = function numberMoreThan(exclusiveMinimum) { - return Object_assign( - boundedNumber({ gt: exclusiveMinimum }, carryover(this, 'exclusiveMinimum')), - { exclusiveMinimum }, - ) -} -number_.lessThan = function numberLessThan(exclusiveMaximum) { - return Object_assign( - boundedNumber({ lt: exclusiveMaximum }, carryover(this, 'exclusiveMaximum')), - { exclusiveMaximum }, - ) -} -number_.between = function numberBetween( - min, - max, - minimum = Math_min(min, max), - maximum = Math_max(min, max), -) { - return Object_assign( - boundedNumber({ gte: minimum, lte: maximum }), - { minimum, maximum }, - ) -} - -export { string_ as string } -interface string_ extends Typeguard, string_.methods { - tag: URI.string - def: this['_type'] - minLength?: number - maxLength?: number -} -declare namespace string_ { - interface methods { - min(minLength: Min): string_.Min - max(maxLength: Max): string_.Max - between(minLength: Min, maxLength: Max): string_.between<[min: Min, max: Max]> - } - type Min - = [Self] extends [{ maxLength: number }] - ? string_.between<[min: Min, max: Self['maxLength']]> - : string_.min - - type Max - = [Self] extends [{ minLength: number }] - ? string_.between<[min: Self['minLength'], max: Max]> - : string_.max - - interface min extends string_ { minLength: Min } - interface max extends string_ { maxLength: Max } - interface between extends string_ { minLength: Bounds[0], maxLength: Bounds[1] } -} -const string_ = function StringSchema(src: unknown) { return typeof src === 'string' } -string_.tag = URI.string -string_.def = '' -string_.min = function stringMinLength(minLength) { - return Object_assign( - boundedString({ gte: minLength }, carryover(this, 'minLength')), - { minLength }, - ) -} -string_.max = function stringMaxLength(maxLength) { - return Object_assign( - boundedString({ lte: maxLength }, carryover(this, 'maxLength')), - { maxLength }, - ) -} -string_.between = function stringBetween( - min, - max, - minLength = Math_min(min, max), - maxLength = Math_max(min, max)) { - return Object_assign( - boundedString({ gte: minLength, lte: maxLength }), - { minLength, maxLength }, - ) -} - -export { nonnullable } -interface nonnullable extends Typeguard<{}> { tag: URI.nonnullable, def: this['_type'] } -const nonnullable = function NonNullableSchema(src: unknown) { return src != null } -nonnullable.tag = URI.nonnullable -nonnullable.def = {} - -export function eq>(value: V, options?: Options): eq> -export function eq(value: V, options?: Options): eq -export function eq(value: V, options?: Options): eq { return eq.def(value, options) } -export interface eq { (u: unknown): u is V, tag: URI.eq, def: V, _type: V } -export namespace eq { - export let prototype = { tag: URI.eq } - export function def(value: T, options?: Options): eq - /* v8 ignore next 1 */ - export function def(x: T, $?: Options) { - const options = applyOptions($) - const eqGuard = isPredicate(x) ? x : (y: unknown) => options.eq.equalsFn(x, y) - function EqSchema(src: unknown) { return eqGuard(src) } - EqSchema.def = x - return Object_assign(EqSchema, eq.prototype) - } -} - -export function optional(schema: S): optional -export function optional(schema: S): optional> -export function optional(schema: S): optional { return optional.def(schema) } -export interface optional { - tag: URI.optional - def: S - [symbol.optional]: number - _type: undefined | S['_type' & keyof S] - (u: unknown): u is this['_type'] -} -export namespace optional { - export let prototype = { tag: URI.optional } - export type type = never | T - export function def(x: T): optional - export function def(x: T) { - const optionalGuard = isPredicate(x) ? guard.optional(x) : (_: any) => true - function OptionalSchema(src: unknown) { return optionalGuard(src) } - OptionalSchema.tag = URI.optional - OptionalSchema.def = x - OptionalSchema[symbol.optional] = 1 - return Object_assign(OptionalSchema, optional.prototype) - } - export const is - : (u: unknown) => u is optional - /* v8 ignore next 1 */ - = has('tag', eq(URI.optional)) as never -} - -export function array(schema: S, readonly: 'readonly'): ReadonlyArray -export function array(schema: S): array -export function array(schema: S): array> -export function array(schema: S): array { return array.def(schema) } -export interface array extends array.methods { - (u: unknown): u is this['_type'] - tag: URI.array - def: S - _type: S['_type' & keyof S][] - minLength?: number - maxLength?: number -} -export declare namespace array { - interface methods { - min(minLength: Min): array.Min - max(maxLength: Max): array.Max - between(minLength: Min, maxLength: Max): array.between<[min: Min, max: Max], S> - } - type Min - = [Self] extends [{ maxLength: number }] - ? array.between<[min: Min, max: Self['maxLength']], Self['def' & keyof Self]> - : array.min - - type Max - = [Self] extends [{ minLength: number }] - ? array.between<[min: Self['minLength'], max: Max], Self['def' & keyof Self]> - : array.max - - interface min extends array { minLength: Min } - interface max extends array { maxLength: Max } - interface between extends array { minLength: Bounds[0], maxLength: Bounds[1] } - type type = never | T -} -export namespace array { - export let prototype = { tag: URI.array } as array - export function def(x: S, prev?: array): array - export function def(x: S, prev?: unknown): array - export function def(x: S, prev?: array): array - /* v8 ignore next 1 */ - export function def(x: S, prev?: unknown): {} { - const arrayGuard = (isPredicate(x) ? guard.array(x) : guard.anyArray) as array - function ArraySchema(src: unknown): src is array['_type'] { return arrayGuard(src) } - ArraySchema.min = function arrayMin(minLength: number) { - return Object_assign( - boundedArray(x, { gte: minLength }, carryover(this, 'minLength' as never)), - { minLength }, - ) - } - ArraySchema.max = function arrayMax(maxLength: number) { - return Object_assign( - boundedArray(x, { lte: maxLength }, carryover(this, 'maxLength' as never)), - { maxLength }, - ) - } - ArraySchema.between = function arrayBetween( - min: number, - max: number, - minLength = Math_min(min, max), - maxLength = Math_max(min, max) - ) { - return Object_assign( - boundedArray(x, { gte: minLength, lte: maxLength }), - { minLength, maxLength }, - ) - } - ArraySchema.def = x - ArraySchema._type = void 0 as never - if (has('minLength', integer)(prev)) ArraySchema.minLength = prev.minLength - if (has('maxLength', integer)(prev)) ArraySchema.maxLength = prev.maxLength - return Object.assign(ArraySchema, array.prototype) - } -} - -export const readonlyArray: { - (schema: S, readonly: 'readonly'): ReadonlyArray - (schema: S): ReadonlyArray> -} = array -export interface ReadonlyArray { - (u: unknown): u is this['_type'] - tag: URI.array - def: S - _type: readonly S['_type' & keyof S][] -} - -export function record(schema: S): record -export function record(schema: S): record> -export function record(schema: S) { return record.def(schema) } -export interface record { (u: unknown): u is this['_type'], tag: URI.record, def: S, _type: Record } -export namespace record { - export let prototype = { tag: URI.record } as record - export type type> = never | T - export function def(x: T): record - /* v8 ignore next 1 */ - export function def(x: T) { - const recordGuard = isPredicate(x) ? guard.record(x) : guard.anyObject - function RecordGuard(src: unknown) { return recordGuard(src) } - RecordGuard.def = x - return Object.assign(RecordGuard, record.prototype) - } -} - -export function union(...schemas: S): union -export function union }>(...schemas: S): union -export function union(...schemas: S): {} { return union.def(schemas) } -export interface union { - (u: unknown): u is this['_type'] - tag: URI.union - def: S - _type: S[number & keyof S]['_type' & keyof S[number & keyof S]] -} -export namespace union { - export let prototype = { tag: URI.union } as union - export type type = never | T - export function def(xs: T): union - /* v8 ignore next 1 */ - export function def(xs: T) { - const anyOf = xs.every(isPredicate) ? guard.union(xs) : guard.unknown - function UnionSchema(src: unknown) { return anyOf(src) } - UnionSchema.def = xs - return Object_assign(UnionSchema, union.prototype) - } -} - -export function intersect(...schemas: S): intersect -export function intersect }>(...schemas: S): intersect -export function intersect(...schemas: S) { return intersect.def(schemas) } -export interface intersect { (u: unknown): u is this['_type'], tag: URI.intersect, def: S, _type: IntersectType } -export namespace intersect { - export let prototype = { tag: URI.intersect } as intersect - export type type> = never | T - export function def(xs: readonly [...T]): intersect - /* v8 ignore next 1 */ - export function def(xs: readonly [...T]) { - const allOf = xs.every(isPredicate) ? guard.intersect(xs) : guard.unknown - function IntersectSchema(src: unknown) { return allOf(src) } - IntersectSchema.def = xs - return Object_assign(IntersectSchema, intersect.prototype) - } -} - -export { tuple } -function tuple(...schemas: tuple.validate): tuple, S>> -function tuple }>(...schemas: tuple.validate): tuple, T>> -function tuple(...args: [...schemas: tuple.validate, options: Options]): tuple, S>> -function tuple }>(...args: [...schemas: tuple.validate, options: Options]): tuple, T>> -function tuple(...schemas: tuple.validate): tuple, S>> -function tuple }>(...schemas: tuple.validate): tuple, T>> -function tuple(...args: | [...S] | [...S, Options]) { return tuple.def(...parseArgs(getConfig().schema, args)) } -interface tuple { (u: unknown): u is this['_type'], tag: URI.tuple, def: S, _type: TupleType, opt: FirstOptionalItem } -namespace tuple { - export let prototype = { tag: URI.tuple } as tuple - export type type> = never | T - export function def(xs: readonly [...T], $?: Options, opt_?: number): tuple - /* v8 ignore next 1 */ - export function def(xs: readonly [...T], $: Options = getConfig().schema, opt_?: number) { - const opt = opt_ || xs.findIndex(optional.is) - const options = { - ...$, minLength: $.optionalTreatment === 'treatUndefinedAndOptionalAsTheSame' ? -1 : xs.findIndex(optional.is) - } satisfies tuple.InternalOptions - const tupleGuard = xs.every(isPredicate) ? guard.tuple(options)(fn.map(xs, replaceBooleanConstructor)) : guard.anyArray - function TupleSchema(src: unknown) { return tupleGuard(src) } - TupleSchema.def = xs - TupleSchema.opt = opt - return Object_assign(TupleSchema, tuple.prototype) - } -} -declare namespace tuple { - type validate = ValidateTuple> - type from - = TypeError extends V[number] ? { [I in keyof V]: V[I] extends TypeError ? invalid> : V[I] } : T - type InternalOptions = { minLength?: number } -} - -export { object_ as object } -function object_< - S extends { [x: string]: Schema }, - T extends { [K in keyof S]: Entry } ->(schemas: S, options?: Options): object_ -function object_< - S extends { [x: string]: Predicate }, - T extends { [K in keyof S]: Entry } ->(schemas: S, options?: Options): object_ -// -function object_(schemas: S, options?: Options) { return object_.def(schemas, options) } -interface object_ { - tag: URI.object - def: S - opt: Optional[] - req: Required[] - _type: object_.type - (u: unknown): u is this['_type'] -} - -namespace object_ { - export let prototype = { tag: URI.object } as object_ - export type Opt = symbol.optional extends keyof S[K] ? never : K - export type Req = symbol.optional extends keyof S[K] ? K : never - export type type = Force< - & { [K in keyof S as Opt]-?: S[K]['_type' & keyof S[K]] } - & { [K in keyof S as Req]+?: S[K]['_type' & keyof S[K]] } - > - export function def(xs: T, $?: Options, opt?: string[]): object_ - /* v8 ignore next 1 */ - export function def(xs: T, $?: Options, opt_?: string[]): {} { - const keys = Object_keys(xs) - const opt = Array_isArray(opt_) ? opt_ : keys.filter((k) => optional.is(xs[k])) - const req = keys.filter((k) => !optional.is(xs[k])) - const objectGuard = guard.record(isPredicate)(xs) - ? guard.object(fn.map(xs, replaceBooleanConstructor), applyOptions($)) - : guard.anyObject - function ObjectSchema(src: unknown) { return objectGuard(src) } - ObjectSchema.def = xs - ObjectSchema.opt = opt - ObjectSchema.req = req - return Object_assign(ObjectSchema, object_.prototype) - } -} - -export type Leaf = typeof leaves[number] -export type LeafTag = Leaf['tag'] -export type Nullary = typeof nullaries[number] -export type NullaryTag = Nullary['tag'] -export type Boundable = typeof boundables[number] -export type BoundableTag = Boundable['tag'] -export type Tag = typeof tags[number] -export type UnaryTag = typeof unaryTags[number] -const hasTag = has('tag', (tag) => typeof tag === 'string') - -export const nullaries = [unknown_, never_, any_, void_, undefined_, null_, symbol_, boolean_] -export const nullaryTags = nullaries.map((x) => x.tag) -export const isNullaryTag = (u: unknown): u is NullaryTag => nullaryTags.includes(u as never) -export const isNullary = (u: unknown): u is Nullary => hasTag(u) && nullaryTags.includes(u.tag as never) - -export const boundables = [integer, bigint_, number_, string_] -export const boundableTags = boundables.map((x) => x.tag) -export const isBoundableTag = (u: unknown): u is BoundableTag => boundableTags.includes(u as never) -export const isBoundable = (u: unknown): u is Boundable => hasTag(u) && boundableTags.includes(u.tag as never) - -export const leaves = [...nullaries, ...boundables] -export const leafTags = leaves.map((leaf) => leaf.tag) -export const isLeaf = (u: unknown): u is Leaf => hasTag(u) && leafTags.includes(u.tag as never) - -export const unaryTags = [URI.optional, URI.eq, URI.array, URI.record, URI.tuple, URI.union, URI.intersect, URI.object] -export const tags = [...leafTags, ...unaryTags] -export const isUnary = (u: unknown): u is Unary => hasTag(u) && unaryTags.includes(u.tag as never) - -export const isCore = (u: unknown): u is Schema => hasTag(u) && tags.includes(u.tag as never) - -export declare namespace Functor { type Index = (keyof any)[] } -export const Functor: T.Functor = { - map(f) { - type T = ReturnType - return (x) => { - switch (true) { - default: return fn.exhaustive(x) - case isLeaf(x): return x - case x.tag === URI.eq: return eq.def(x.def as never) as never - case x.tag === URI.array: return array.def(f(x.def), x) - case x.tag === URI.record: return record.def(f(x.def)) - case x.tag === URI.optional: return optional.def(f(x.def)) - case x.tag === URI.tuple: return tuple.def(fn.map(x.def, f)) - case x.tag === URI.object: return object_.def(fn.map(x.def, f)) - case x.tag === URI.union: return union.def(fn.map(x.def, f)) - case x.tag === URI.intersect: return intersect.def(fn.map(x.def, f)) - } - } - } -} - -export const IndexedFunctor: T.Functor.Ix = { - ...Functor, - mapWithIndex(f) { - type T = ReturnType - return (x, ix) => { - switch (true) { - default: return fn.exhaustive(x) - case isLeaf(x): return x - case x.tag === URI.eq: return eq.def(x.def as never) as never - case x.tag === URI.array: return array.def(f(x.def, ix), x) - case x.tag === URI.record: return record.def(f(x.def, ix)) - case x.tag === URI.optional: return optional.def(f(x.def, ix)) - case x.tag === URI.tuple: return tuple.def(fn.map(x.def, (y, iy) => f(y, [...ix, iy])), x.opt) - case x.tag === URI.object: return object_.def(fn.map(x.def, (y, iy) => f(y, [...ix, iy])), {}, x.opt as never) - case x.tag === URI.union: return union.def(fn.map(x.def, (y, iy) => f(y, [...ix, symbol.union, iy]))) - case x.tag === URI.intersect: return intersect.def(fn.map(x.def, (y, iy) => f(y, [...ix, symbol.intersect, iy]))) - } - } - } -} - -export const unfold = fn.ana(Functor) -export const fold = fn.cata(Functor) -export const foldWithIndex = fn.cataIx(IndexedFunctor) - -function boundedInteger(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & integer -function boundedInteger(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & integer -function boundedInteger(bounds: Bounds, carry?: {}): {} { - return Object_assign(function BoundedIntegerSchema(u: unknown) { - return integer(u) && within(bounds)(u) - }, carry, integer) -} - -function boundedBigInt(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & bigint_ -function boundedBigInt(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & bigint_ -function boundedBigInt(bounds: Bounds, carry?: {}): {} { - return Object_assign(function BoundedBigIntSchema(u: unknown) { - return bigint_(u) && withinBig(bounds)(u) - }, carry, bigint_) -} - -function boundedNumber(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & number_ -function boundedNumber(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & number_ -function boundedNumber(bounds: Bounds, carry?: {}): {} { - return Object_assign(function BoundedNumberSchema(u: unknown) { - return number_(u) && within(bounds)(u) - }, carry, number_) -} - -function boundedString(bounds: Bounds, carry?: Partial): ((u: unknown) => boolean) & Bounds & string_ -function boundedString(bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & string_ -function boundedString(bounds: Bounds, carry?: {}): {} { - return Object_assign(function BoundedStringSchema(u: unknown) { - return string_(u) && within(bounds)(u.length) - }, carry, string_) -} - -function boundedArray(schema: S, bounds: Bounds, carry?: Partial>): ((u: unknown) => boolean) & Bounds & array -function boundedArray(schema: S, bounds: Bounds, carry?: { [x: string]: unknown }): ((u: unknown) => boolean) & Bounds & array -function boundedArray(schema: S, bounds: Bounds, carry?: {}): ((u: unknown) => boolean) & Bounds & array { - return Object_assign(function BoundedArraySchema(u: unknown) { - return Array_isArray(u) && within(bounds)(u.length) - }, carry, array(schema)) -} diff --git a/packages/schema/src/version.ts b/packages/schema/src/version.ts index 660ff1ca..388bbc3e 100644 --- a/packages/schema/src/version.ts +++ b/packages/schema/src/version.ts @@ -1,3 +1,3 @@ import pkg from './__generated__/__manifest__.js' export const VERSION = `${pkg.name}@${pkg.version}` as const -export type VERSION = typeof VERSION +export type VERSION = typeof VERSION \ No newline at end of file diff --git a/packages/schema/test/combinators.test.ts b/packages/schema/test/combinators.test.ts deleted file mode 100644 index 6e0c1907..00000000 --- a/packages/schema/test/combinators.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as vi from 'vitest' -import { t } from '@traversable/schema' -import { fc, test } from '@fast-check/vitest' -import * as Seed from './seed.js' - -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳', () => { - const natural = t.filter(t.integer, x => x >= 0) - const varchar = t.filter(t.string)(x => 0x100 >= x.length) - - const arbitrary = fc.letrec(Seed.seed()).tree.chain((seed) => fc.constant([ - Seed.toSchema(seed), - fc.constant(Seed.toJson(seed)), - ] satisfies [any, any])) - - vi.describe('〖⛳️〗‹‹ ❲t.filter❳', () => { - test.prop([fc.nat()])( - '〖⛳️〗‹ ❲t.filter(t.integer, q)❳: returns true when `q` is satisied', - (x) => vi.assert.isTrue(natural(x)) - ) - test.prop([fc.nat().map((x) => -x).filter((x) => x !== 0)])( - '〖⛳️〗‹ ❲t.filter(t.integer, q)❳: returns false when `q` is not satisfied', - (x) => vi.assert.isFalse(natural(x)) - ) - - test.prop([fc.string({ maxLength: 0x100 })])( - '〖⛳️〗‹ ❲t.filter(t.string, q)❳: returns true when `q` is satisfied', - (x) => vi.assert.isTrue(varchar(x)) - ) - test.prop([fc.string({ minLength: 0x101 })], {})( - '〖⛳️〗‹ ❲t.filter(t.string, q)❳: returns false when `q` is not satisfied', - (x) => vi.assert.isFalse(varchar(x)) - ) - - test.prop([arbitrary, fc.func(fc.boolean())])( - /** - * See also: - * https://www.wisdom.weizmann.ac.il/~/oded/VO/mono1.pdf - */ - '〖⛳️〗‹ ❲t.filter(s, q)❳: is monotone cf. s ∩ q', - ([s, x], q) => vi.assert.equal(t.filter(s, q)(x), s(x) && q(x)) - ) - }) -}) diff --git a/packages/schema/test/fast-check.ts b/packages/schema/test/fast-check.ts deleted file mode 100644 index f99d3983..00000000 --- a/packages/schema/test/fast-check.ts +++ /dev/null @@ -1,92 +0,0 @@ -export * from 'fast-check' -import * as fc from 'fast-check' - -import { symbol as Symbol } from '@traversable/registry' -import type { Guard } from '@traversable/schema' - -export interface Arbitrary extends fc.Arbitrary { - readonly [Symbol.optional]?: true -} - -export type { typeOf as typeof } -type typeOf = S extends fc.Arbitrary ? T : never - -/** @internal */ -const Object_keys = globalThis.Object.keys -/** @internal */ -const Array_isArray = globalThis.Array.isArray -/** @internal */ -const isString: Guard = (u): u is never => typeof u === 'string' -/** @internal */ -const arrayOf - : (p: Guard) => Guard - = (p) => (u): u is never => Array_isArray(u) && u.every(p) -/** @internal */ -const has - : (k: K, p: Guard) => Guard<{ [P in K]: T }> - = (k, p) => (u: unknown): u is never => - !!u && - typeof u === 'object' && - Object.prototype.hasOwnProperty.call(u, k) && - p(u[k as never]) - -/** @internal */ -// const KEYWORD = { -// break: 'break', case: 'case', catch: 'catch', class: 'class', const: 'const', -// continue: 'continue', debugger: 'debugger', default: 'default', delete: 'delete', -// do: 'do', else: 'else', export: 'export', extends: 'extends', false: 'false', -// finally: 'finally', for: 'for', function: 'function', if: 'if', import: 'import', -// in: 'in', instanceof: 'instanceof', new: 'new', null: 'null', return: 'return', -// super: 'super', switch: 'switch', this: 'this', throw: 'throw', true: 'true', -// try: 'try', typeof: 'typeof', var: 'var', void: 'void', while: 'while', -// with: 'with', let: 'let', static: 'static', yield: 'yield', -// } as const satisfies Record - -// const Keywords = [ -// 'break', 'case', 'catch', 'class', 'const', -// 'continue', 'debugger', 'default', 'delete', -// 'do', 'else', 'export', 'extends', 'false', -// 'finally', 'for', 'function', 'if', 'import', -// 'in', 'instanceof', 'new', 'null', 'return', -// 'super', 'switch', 'this', 'throw', 'true', -// 'try', 'typeof', 'var', 'void', 'while', -// 'with', 'let', 'static', 'yield', -// ] - -const PATTERN = { - identifier: '^[$_a-zA-Z][$_a-zA-Z0-9]*$', - // identifierSpec: '[$_\\p{ID_Start}][$_\\u200C\\u200D\\p{ID_Continue}]*', -} as const - -const REGEX = { - identifier: new globalThis.RegExp(PATTERN.identifier, 'u'), - // identifierSpec: new globalThis.RegExp(PATTERN.identifierSpec, 'u'), -} as const - -export type UniqueArrayDefaults = fc.UniqueArrayConstraintsRecommended - -/** - * See also: - * - the MDN docs on - * [identifiers in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers) - */ -export function identifier(constraints?: fc.StringMatchingConstraints): fc.Arbitrary -export function identifier(constraints?: fc.StringMatchingConstraints) { - return fc.stringMatching(REGEX.identifier, constraints) //.filter((ident) => !(ident in KEYWORD)) -} - -export const entries - : (model: fc.Arbitrary, constraints?: UniqueArrayDefaults) => fc.Arbitrary<[k: string, v: T][]> - = (model, constraints) => fc.uniqueArray( - fc.tuple(identifier(), model), - { ...constraints, selector: ([k]) => k }, - ) - -/** - * ### {@link optional `fc.optional`} - */ -export function optional(model: fc.Arbitrary, constraints?: fc.OneOfConstraints): Arbitrary -export function optional(model: fc.Arbitrary, _constraints: fc.OneOfConstraints = {}): fc.Arbitrary { - (model as any)[Symbol.optional] = true; - return model -} diff --git a/packages/schema/test/version.test.ts b/packages/schema/test/version.test.ts index 00cee146..2a564622 100644 --- a/packages/schema/test/version.test.ts +++ b/packages/schema/test/version.test.ts @@ -1,10 +1,10 @@ import * as vi from 'vitest' -import { VERSION } from '@traversable/schema' import pkg from '../package.json' with { type: 'json' } +import { VERSION } from '@traversable/schema' -vi.describe('〖⛳️〗‹‹‹ ❲@traverable/schema❳', () => { - vi.it('〖⛳️〗› ❲VERSION❳', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/schema❳', () => { + vi.it('〖⛳️〗› ❲schema#VERSION❳', () => { const expected = `${pkg.name}@${pkg.version}` vi.assert.equal(VERSION, expected) }) -}) +}) \ No newline at end of file diff --git a/packages/schema/tsconfig.build.json b/packages/schema/tsconfig.build.json index 0a93b6f4..b947e124 100644 --- a/packages/schema/tsconfig.build.json +++ b/packages/schema/tsconfig.build.json @@ -8,6 +8,13 @@ "stripInternal": true }, "references": [ - { "path": "../registry" } + { "path": "../derive-codec" }, + { "path": "../derive-equals" }, + { "path": "../derive-validators" }, + { "path": "../registry" }, + { "path": "../schema-core" }, + { "path": "../schema-generator" }, + { "path": "../schema-to-json-schema" }, + { "path": "../schema-to-string" } ] } diff --git a/packages/schema/tsconfig.src.json b/packages/schema/tsconfig.src.json index 7e6324da..d4bc09fc 100644 --- a/packages/schema/tsconfig.src.json +++ b/packages/schema/tsconfig.src.json @@ -7,7 +7,14 @@ "outDir": "build/src" }, "references": [ - { "path": "../registry" } + { "path": "../derive-codec" }, + { "path": "../derive-equals" }, + { "path": "../derive-validators" }, + { "path": "../registry" }, + { "path": "../schema-core" }, + { "path": "../schema-generator" }, + { "path": "../schema-to-json-schema" }, + { "path": "../schema-to-string" } ], "include": ["src"] } diff --git a/packages/schema/tsconfig.test.json b/packages/schema/tsconfig.test.json index 06b45e93..85850c64 100644 --- a/packages/schema/tsconfig.test.json +++ b/packages/schema/tsconfig.test.json @@ -8,8 +8,14 @@ }, "references": [ { "path": "tsconfig.src.json" }, + { "path": "../derive-codec" }, + { "path": "../derive-equals" }, + { "path": "../derive-validators" }, { "path": "../registry" }, - { "path": "../schema-zod-adapter" }, + { "path": "../schema-core" }, + { "path": "../schema-generator" }, + { "path": "../schema-to-json-schema" }, + { "path": "../schema-to-string" } ], "include": ["test"] } diff --git a/packages/schema/vite.config.ts b/packages/schema/vite.config.ts new file mode 100644 index 00000000..64dba4ad --- /dev/null +++ b/packages/schema/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import sharedConfig from '../../vite.config.js' + +const localConfig = defineConfig({}) + +export default mergeConfig(sharedConfig, localConfig) \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02a62bb1..df532548 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,67 +4,121 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - typescript: 5.8.2 +catalogs: + default: + '@ark/attest': + specifier: ^0.44.8 + version: 0.44.8 + '@babel/cli': + specifier: ^7.25.9 + version: 7.27.0 + '@babel/core': + specifier: ^7.26.0 + version: 7.26.10 + '@babel/plugin-transform-export-namespace-from': + specifier: ^7.25.9 + version: 7.25.9 + '@babel/plugin-transform-modules-commonjs': + specifier: ^7.25.9 + version: 7.26.3 + '@changesets/changelog-github': + specifier: ^0.5.0 + version: 0.5.1 + '@changesets/cli': + specifier: ^2.27.9 + version: 2.28.1 + '@fast-check/vitest': + specifier: ^0.2.0 + version: 0.2.0 + '@types/madge': + specifier: ^5.0.3 + version: 5.0.3 + '@types/node': + specifier: ^22.9.0 + version: 22.14.0 + '@vitest/coverage-v8': + specifier: 3.1.1 + version: 3.1.1 + '@vitest/ui': + specifier: 3.1.1 + version: 3.1.1 + babel-plugin-annotate-pure-calls: + specifier: ^0.4.0 + version: 0.4.0 + fast-check: + specifier: ^4.0.1 + version: 4.1.0 + madge: + specifier: ^8.0.0 + version: 8.0.0 + tinybench: + specifier: ^3.0.4 + version: 3.1.1 + typescript: + specifier: 5.8.2 + version: 5.8.2 + vitest: + specifier: ^3.0.4 + version: 3.1.1 importers: .: devDependencies: '@ark/attest': - specifier: ^0.44.8 + specifier: 'catalog:' version: 0.44.8(typescript@5.8.2) '@babel/cli': - specifier: ^7.25.9 + specifier: 'catalog:' version: 7.27.0(@babel/core@7.26.10) '@babel/core': - specifier: ^7.26.0 + specifier: 'catalog:' version: 7.26.10 '@babel/plugin-transform-export-namespace-from': - specifier: ^7.25.9 + specifier: 'catalog:' version: 7.25.9(@babel/core@7.26.10) '@babel/plugin-transform-modules-commonjs': - specifier: ^7.25.9 + specifier: 'catalog:' version: 7.26.3(@babel/core@7.26.10) '@changesets/changelog-github': - specifier: ^0.5.0 + specifier: 'catalog:' version: 0.5.1 '@changesets/cli': - specifier: ^2.27.9 + specifier: 'catalog:' version: 2.28.1 '@fast-check/vitest': - specifier: ^0.2.0 + specifier: 'catalog:' version: 0.2.0(vitest@3.1.1) '@types/madge': - specifier: ^5.0.3 + specifier: 'catalog:' version: 5.0.3 '@types/node': - specifier: ^22.9.0 + specifier: 'catalog:' version: 22.14.0 '@vitest/coverage-v8': - specifier: 3.1.1 + specifier: 'catalog:' version: 3.1.1(vitest@3.1.1) '@vitest/ui': - specifier: 3.1.1 + specifier: 'catalog:' version: 3.1.1(vitest@3.1.1) babel-plugin-annotate-pure-calls: - specifier: ^0.4.0 + specifier: 'catalog:' version: 0.4.0(@babel/core@7.26.10) fast-check: - specifier: ^4.0.1 + specifier: 'catalog:' version: 4.1.0 madge: - specifier: ^8.0.0 + specifier: 'catalog:' version: 8.0.0(typescript@5.8.2) tinybench: - specifier: ^3.0.4 + specifier: 'catalog:' version: 3.1.1 typescript: - specifier: 5.8.2 + specifier: 'catalog:' version: 5.8.2 vitest: - specifier: ^3.0.4 - version: 3.1.1(@types/node@22.14.0)(@vitest/ui@3.1.1)(yaml@2.7.1) + specifier: 'catalog:' + version: 3.1.1(@types/node@22.14.0)(@vitest/ui@3.1.1)(msw@2.7.3(@types/node@22.14.0)(typescript@5.8.2))(yaml@2.7.1) bin: dependencies: @@ -114,9 +168,9 @@ importers: '@traversable/registry': specifier: workspace:^ version: link:../../packages/registry/dist - '@traversable/schema': + '@traversable/schema-core': specifier: workspace:^ - version: link:../../packages/schema/dist + version: link:../../packages/schema-core/dist '@traversable/schema-seed': specifier: workspace:^ version: link:../../packages/schema-seed/dist @@ -178,9 +232,9 @@ importers: '@traversable/registry': specifier: workspace:^ version: link:../registry/dist - '@traversable/schema': + '@traversable/schema-core': specifier: workspace:^ - version: link:../schema/dist + version: link:../schema-core/dist '@traversable/schema-seed': specifier: workspace:^ version: link:../schema-seed/dist @@ -194,9 +248,9 @@ importers: '@traversable/registry': specifier: workspace:^ version: link:../registry/dist - '@traversable/schema': + '@traversable/schema-core': specifier: workspace:^ - version: link:../schema/dist + version: link:../schema-core/dist '@traversable/schema-seed': specifier: workspace:^ version: link:../schema-seed/dist @@ -213,9 +267,9 @@ importers: '@traversable/registry': specifier: workspace:^ version: link:../registry/dist - '@traversable/schema': + '@traversable/schema-core': specifier: workspace:^ - version: link:../schema/dist + version: link:../schema-core/dist '@traversable/schema-seed': specifier: workspace:^ version: link:../schema-seed/dist @@ -236,26 +290,127 @@ importers: publishDirectory: dist packages/schema: + devDependencies: + '@traversable/derive-codec': + specifier: workspace:^ + version: link:../derive-codec/dist + '@traversable/derive-equals': + specifier: workspace:^ + version: link:../derive-equals/dist + '@traversable/derive-validators': + specifier: workspace:^ + version: link:../derive-validators/dist + '@traversable/registry': + specifier: workspace:^ + version: link:../registry/dist + '@traversable/schema-core': + specifier: workspace:^ + version: link:../schema-core/dist + '@traversable/schema-generator': + specifier: workspace:^ + version: link:../schema-generator/dist + '@traversable/schema-to-json-schema': + specifier: workspace:^ + version: link:../schema-to-json-schema/dist + '@traversable/schema-to-string': + specifier: workspace:^ + version: link:../schema-to-string/dist + publishDirectory: dist + + packages/schema-arbitrary: + dependencies: + fast-check: + specifier: 3 - 4 + version: 3.23.2 + devDependencies: + '@traversable/registry': + specifier: workspace:^ + version: link:../registry/dist + '@traversable/schema-core': + specifier: workspace:^ + version: link:../schema-core/dist + publishDirectory: dist + + packages/schema-core: dependencies: '@traversable/registry': specifier: workspace:^ version: link:../registry/dist devDependencies: + '@sinclair/typebox': + specifier: ^0.34.33 + version: 0.34.33 '@traversable/schema-zod-adapter': specifier: workspace:^ version: link:../schema-zod-adapter/dist '@types/lodash.isequal': specifier: ^4.5.8 version: 4.5.8 + arktype: + specifier: ^2.1.20 + version: 2.1.20 fast-check: specifier: ^3.0.0 version: 3.23.2 lodash.isequal: specifier: ^4.5.0 version: 4.5.0 + valibot: + specifier: 1.0.0-rc.1 + version: 1.0.0-rc.1(typescript@5.8.2) zod: specifier: ^3.24.2 version: 3.24.2 + zod3: + specifier: npm:zod@3 + version: zod@3.24.2 + zod4: + specifier: npm:zod@4.0.0-beta.20250420T053007 + version: zod@4.0.0-beta.20250420T053007 + publishDirectory: dist + + packages/schema-generator: + devDependencies: + '@clack/prompts': + specifier: ^0.10.1 + version: 0.10.1 + '@traversable/derive-equals': + specifier: workspace:^ + version: link:../derive-equals/dist + '@traversable/derive-validators': + specifier: workspace:^ + version: link:../derive-validators/dist + '@traversable/registry': + specifier: workspace:^ + version: link:../registry/dist + '@traversable/schema-core': + specifier: workspace:^ + version: link:../schema-core/dist + '@traversable/schema-to-json-schema': + specifier: workspace:^ + version: link:../schema-to-json-schema/dist + '@traversable/schema-to-string': + specifier: workspace:^ + version: link:../schema-to-string/dist + picocolors: + specifier: ^1.1.1 + version: 1.1.1 + publishDirectory: dist + + packages/schema-jit-compiler: + devDependencies: + '@traversable/registry': + specifier: workspace:^ + version: link:../registry/dist + '@traversable/schema-arbitrary': + specifier: workspace:^ + version: link:../schema-arbitrary/dist + '@traversable/schema-core': + specifier: workspace:^ + version: link:../schema-core/dist + '@traversable/schema-seed': + specifier: workspace:^ + version: link:../schema-seed/dist publishDirectory: dist packages/schema-seed: @@ -266,9 +421,9 @@ importers: '@traversable/registry': specifier: workspace:^ version: link:../registry/dist - '@traversable/schema': + '@traversable/schema-core': specifier: workspace:^ - version: link:../schema/dist + version: link:../schema-core/dist fast-check: specifier: ^3.23.2 version: 3.23.2 @@ -279,9 +434,9 @@ importers: '@traversable/registry': specifier: workspace:^ version: link:../registry/dist - '@traversable/schema': + '@traversable/schema-core': specifier: workspace:^ - version: link:../schema/dist + version: link:../schema-core/dist '@traversable/schema-seed': specifier: workspace:^ version: link:../schema-seed/dist @@ -292,9 +447,9 @@ importers: '@traversable/registry': specifier: workspace:^ version: link:../registry/dist - '@traversable/schema': + '@traversable/schema-core': specifier: workspace:^ - version: link:../schema/dist + version: link:../schema-core/dist '@traversable/schema-seed': specifier: workspace:^ version: link:../schema-seed/dist @@ -339,7 +494,7 @@ packages: resolution: {integrity: sha512-q14L0pIUXthlIo05qAePRygumNv2gC4RRozrU91hb2f9mBhwbxOQ77oW9oZ7VgMtU4Xg7GwceL7ogOT02RdnkA==} hasBin: true peerDependencies: - typescript: 5.8.2 + typescript: '*' '@ark/fs@0.44.5': resolution: {integrity: sha512-cn1NzzFZBX3nj6snDGP4dH9VjTy6VlM2B0Ew1LJlXtOO85aqwcBDzav19AZ34Z1I7yaR241h1zWmC2X++katMw==} @@ -347,12 +502,18 @@ packages: '@ark/schema@0.44.4': resolution: {integrity: sha512-TsZTX+k5J7xsGABsFjVdRUNgViGDMLv73sikBM8JNxC4STe0suTuMNa1OJ/AFP2N+LpJ1zL9tdWlg28PRqAYhg==} + '@ark/schema@0.46.0': + resolution: {integrity: sha512-c2UQdKgP2eqqDArfBqQIJppxJHvNNXuQPeuSPlDML4rjw+f1cu0qAlzOG4b8ujgm9ctIDWwhpyw6gjG5ledIVQ==} + '@ark/util@0.44.4': resolution: {integrity: sha512-zLfNZrsq5Dq+8B0pHJwL/wD3xNBHb8FoP0FuPB455w7HpqVaqO5qPXvn+YoO8v1Y6pNwLVsM9vCIiO221LoODQ==} '@ark/util@0.44.5': resolution: {integrity: sha512-qomwswzzv1aBIEWEWLlts1iM1/nGWDDhSNTYKI4qlpYfFT4+sn3ufcFInjHyD54gHJxpQAhbymUzW3QEylyOSA==} + '@ark/util@0.46.0': + resolution: {integrity: sha512-JPy/NGWn/lvf1WmGCPw2VGpBg5utZraE84I7wli18EDF3p3zc/e9WolT35tINeZO3l7C77SjqRJeAUoT0CvMRg==} + '@babel/cli@7.27.0': resolution: {integrity: sha512-bZfxn8DRxwiVzDO5CEeV+7IqXeCkzI4yYnrQbpwjT76CUyossQc6RYE7n+xfm0/2k40lPaCpW0FhxYs7EBAetw==} engines: {node: '>=6.9.0'} @@ -459,6 +620,15 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@bundled-es-modules/cookie@2.0.1': + resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} + + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + + '@bundled-es-modules/tough-cookie@0.1.6': + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@changesets/apply-release-plan@7.0.10': resolution: {integrity: sha512-wNyeIJ3yDsVspYvHnEz1xQDq18D9ifed3lI+wxRQRK4pArUcuHgCTrHv0QRnnwjhVCQACxZ+CBih3wgOct6UXw==} @@ -520,6 +690,12 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@clack/core@0.4.2': + resolution: {integrity: sha512-NYQfcEy8MWIxrT5Fj8nIVchfRFA26yYKJcvBS7WlUIlw2OmQOY9DhGGXMovyI5J5PpxrCPGkgUi207EBrjpBvg==} + + '@clack/prompts@0.10.1': + resolution: {integrity: sha512-Q0T02vx8ZM9XSv9/Yde0jTmmBQufZhPJfYAg2XrrrxWWaZgq1rr8nU8Hv710BQ1dhoP8rtY7YUdpGej2Qza/cw==} + '@dependents/detective-less@5.0.1': resolution: {integrity: sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==} engines: {node: '>=18'} @@ -791,6 +967,37 @@ packages: resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} + '@inquirer/confirm@5.1.9': + resolution: {integrity: sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.1.10': + resolution: {integrity: sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.11': + resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.6': + resolution: {integrity: sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -823,6 +1030,10 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@mswjs/interceptors@0.37.6': + resolution: {integrity: sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==} + engines: {node: '>=18'} + '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} @@ -838,6 +1049,15 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -945,7 +1165,7 @@ packages: resolution: {integrity: sha512-scZwpc78cJS8dx6LMLemVHqnaAVbPWasx36TxYWUASA63hxPx5XbGbb6pe4cSpT8dWqhBtsPpHZoFTrM1aqx7A==} engines: {node: '>=18.0.0'} peerDependencies: - typescript: 5.8.2 + typescript: ^5.1.0 peerDependenciesMeta: typescript: optional: true @@ -958,7 +1178,7 @@ packages: resolution: {integrity: sha512-6M1Lq2mrH5zfGN++ay+a2KzdPqOh2TB7n6wYPPXA0rxQan8c5NZCvDFF635KS65LSzZDB+2VfFTgoPBERdYkYg==} engines: {node: '>=18.0.0'} peerDependencies: - typescript: 5.8.2 + typescript: ^5.1.0 peerDependenciesMeta: typescript: optional: true @@ -1079,6 +1299,9 @@ packages: cpu: [x64] os: [win32] + '@sinclair/typebox@0.34.33': + resolution: {integrity: sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g==} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -1169,20 +1392,26 @@ packages: '@types/react@19.1.0': resolution: {integrity: sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==} + '@types/statuses@2.0.5': + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@typescript-eslint/eslint-plugin@8.29.0': resolution: {integrity: sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 - typescript: 5.8.2 + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/parser@8.29.0': resolution: {integrity: sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: 5.8.2 + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/scope-manager@8.29.0': resolution: {integrity: sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==} @@ -1193,7 +1422,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: 5.8.2 + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/types@8.29.0': resolution: {integrity: sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==} @@ -1203,14 +1432,14 @@ packages: resolution: {integrity: sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: 5.8.2 + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/utils@8.29.0': resolution: {integrity: sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: 5.8.2 + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/visitor-keys@8.29.0': resolution: {integrity: sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==} @@ -1223,7 +1452,7 @@ packages: '@typescript/vfs@1.6.0': resolution: {integrity: sha512-hvJUjNVeBMp77qPINuUvYXj4FyWeeMMKZkxEATEU3hqBAQ7qdTBCUFT7Sp0Zu0faeEtFf+ldXxMEDr/bk73ISg==} peerDependencies: - typescript: 5.8.2 + typescript: '*' '@vitejs/plugin-react@4.3.4': resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} @@ -1292,6 +1521,9 @@ packages: '@web3-storage/multipart-parser@1.0.0': resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} + '@zod/core@0.8.1': + resolution: {integrity: sha512-djj8hPhxIHcG8ptxITaw/Bout5HJZ9NyRbKr95Eilqwt9R0kvITwUQGDU+n+MVdsBIka5KwztmZSLti22F+P0A==} + '@zxing/text-encoding@0.9.0': resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} @@ -1316,6 +1548,10 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1348,6 +1584,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + arktype@2.1.20: + resolution: {integrity: sha512-IZCEEXaJ8g+Ijd59WtSYwtjnqXiwM8sWQ5EjGamcto7+HVN9eK0C4p0zDlCuAwWhpqr6fIBkxPuYDl4/Mcj/+Q==} + arktype@2.1.9: resolution: {integrity: sha512-bq46shcLpfop4D9acVQN/+quZ+hIGs4OUzoLq2vCaZLdkITOlWkfamBk9abMuC6fbgxW1fu/2PamcQgggWhTwQ==} @@ -1468,9 +1707,17 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1604,13 +1851,13 @@ packages: resolution: {integrity: sha512-pgN43/80MmWVSEi5LUuiVvO/0a9ss5V7fwVfrJ4QzAQRd3cwqU1SfWGXJFcNKUqoD5cS+uIovhw5t/0rSeC5Mw==} engines: {node: '>=18'} peerDependencies: - typescript: 5.8.2 + typescript: ^5.4.4 detective-vue2@2.2.0: resolution: {integrity: sha512-sVg/t6O2z1zna8a/UIV6xL5KUa2cMTQbdTIIvqNM0NIPswp52fe43Nwmbahzj3ww4D844u/vC2PYfiGLvD3zFA==} engines: {node: '>=18'} peerDependencies: - typescript: 5.8.2 + typescript: ^5.4.4 dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} @@ -1938,6 +2185,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql@16.10.0: + resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1957,6 +2208,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2033,6 +2287,9 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2187,7 +2444,7 @@ packages: engines: {node: '>=18'} hasBin: true peerDependencies: - typescript: 5.8.2 + typescript: ^5.4.4 peerDependenciesMeta: typescript: optional: true @@ -2276,9 +2533,23 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.7.3: + resolution: {integrity: sha512-+mycXv8l2fEAjFZ5sjrtjJDmm2ceKGjrNbBr1durRg6VkU9fNUE/gsmQ51hWbHqs+l35W1iM+ZsmOD9Fd6lspw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + multipasta@0.2.5: resolution: {integrity: sha512-c8eMDb1WwZcE02WVjHoOmUVk7fnKU/RmUcosHACglrWAuPQsEJv+E8430sXj6jNc1jHw0zrS16aCjQh4BcEb4A==} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2332,6 +2603,9 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -2397,6 +2671,9 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2464,6 +2741,9 @@ packages: resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} engines: {node: '>=10'} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2477,6 +2757,9 @@ packages: quansync@0.2.10: resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2532,6 +2815,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-dependency-path@4.0.1: resolution: {integrity: sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==} engines: {node: '>=18'} @@ -2625,6 +2911,9 @@ packages: resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} engines: {node: '>=18'} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slash@2.0.0: resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} engines: {node: '>=6'} @@ -2660,6 +2949,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -2669,6 +2962,9 @@ packages: stream-to-array@2.3.0: resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2773,6 +3069,10 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -2784,7 +3084,7 @@ packages: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} peerDependencies: - typescript: 5.8.2 + typescript: '>=4.8.4' ts-graphviz@2.1.6: resolution: {integrity: sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw==} @@ -2801,12 +3101,20 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.39.1: + resolution: {integrity: sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==} + engines: {node: '>=16'} + typescript-eslint@8.29.0: resolution: {integrity: sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: 5.8.2 + typescript: '>=4.8.4 <5.9.0' typescript@5.8.2: resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} @@ -2827,6 +3135,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -2836,6 +3148,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-sync-external-store@1.5.0: resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} peerDependencies: @@ -2850,7 +3165,7 @@ packages: valibot@1.0.0-rc.1: resolution: {integrity: sha512-bTHNpeeQ403xS7qGHF/tw3EC/zkZOU5VdkfIsmRDu1Sp+BJNTNCm6m5HlwOgyW/03lofP+uQiq3R+Poo9wiCEg==} peerDependencies: - typescript: 5.8.2 + typescript: '>=5' peerDependenciesMeta: typescript: optional: true @@ -2966,6 +3281,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3005,17 +3324,32 @@ packages: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + yargs@16.2.0: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + zod@4.0.0-beta.20250420T053007: + resolution: {integrity: sha512-5pp8Q0PNDaNcUptGiBE9akyioJh3RJpagIxrLtAVMR9IxwcSZiOsJD/1/98CyhItdTlI2H91MfhhLzRlU+fifA==} + snapshots: '@ampproject/remapping@2.3.0': @@ -3042,10 +3376,16 @@ snapshots: dependencies: '@ark/util': 0.44.4 + '@ark/schema@0.46.0': + dependencies: + '@ark/util': 0.46.0 + '@ark/util@0.44.4': {} '@ark/util@0.44.5': {} + '@ark/util@0.46.0': {} + '@babel/cli@7.27.0(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -3189,6 +3529,22 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@bundled-es-modules/cookie@2.0.1': + dependencies: + cookie: 0.7.2 + optional: true + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.1 + optional: true + + '@bundled-es-modules/tough-cookie@0.1.6': + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + optional: true + '@changesets/apply-release-plan@7.0.10': dependencies: '@changesets/config': 3.1.1 @@ -3346,6 +3702,17 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 + '@clack/core@0.4.2': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.10.1': + dependencies: + '@clack/core': 0.4.2 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@dependents/detective-less@5.0.1': dependencies: gonzales-pe: 4.3.0 @@ -3534,7 +3901,7 @@ snapshots: '@fast-check/vitest@0.2.0(vitest@3.1.1)': dependencies: fast-check: 3.23.2 - vitest: 3.1.1(@types/node@22.14.0)(@vitest/ui@3.1.1)(yaml@2.7.1) + vitest: 3.1.1(@types/node@22.14.0)(@vitest/ui@3.1.1)(msw@2.7.3(@types/node@22.14.0)(typescript@5.8.2))(yaml@2.7.1) '@humanfs/core@0.19.1': {} @@ -3549,6 +3916,36 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} + '@inquirer/confirm@5.1.9(@types/node@22.14.0)': + dependencies: + '@inquirer/core': 10.1.10(@types/node@22.14.0) + '@inquirer/type': 3.0.6(@types/node@22.14.0) + optionalDependencies: + '@types/node': 22.14.0 + optional: true + + '@inquirer/core@10.1.10(@types/node@22.14.0)': + dependencies: + '@inquirer/figures': 1.0.11 + '@inquirer/type': 3.0.6(@types/node@22.14.0) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.14.0 + optional: true + + '@inquirer/figures@1.0.11': + optional: true + + '@inquirer/type@3.0.6(@types/node@22.14.0)': + optionalDependencies: + '@types/node': 22.14.0 + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3593,6 +3990,16 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@mswjs/interceptors@0.37.6': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + optional: true + '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': optional: true @@ -3608,6 +4015,18 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@open-draft/deferred-promise@2.2.0': + optional: true + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + optional: true + + '@open-draft/until@2.1.0': + optional: true + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -3799,6 +4218,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.39.0': optional: true + '@sinclair/typebox@0.34.33': {} + '@standard-schema/spec@1.0.0': {} '@tanstack/form-core@1.3.0': @@ -3875,7 +4296,7 @@ snapshots: '@types/madge@5.0.3': dependencies: - '@types/node': 22.14.0 + '@types/node': 20.17.30 '@types/node@12.20.55': {} @@ -3895,6 +4316,12 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/statuses@2.0.5': + optional: true + + '@types/tough-cookie@4.0.5': + optional: true + '@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.24.0)(typescript@5.8.2))(eslint@9.24.0)(typescript@5.8.2)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -4015,7 +4442,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.1.1(@types/node@22.14.0)(@vitest/ui@3.1.1)(yaml@2.7.1) + vitest: 3.1.1(@types/node@22.14.0)(@vitest/ui@3.1.1)(msw@2.7.3(@types/node@22.14.0)(typescript@5.8.2))(yaml@2.7.1) transitivePeerDependencies: - supports-color @@ -4026,12 +4453,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.1(vite@6.2.5(@types/node@22.14.0)(yaml@2.7.1))': + '@vitest/mocker@3.1.1(msw@2.7.3(@types/node@22.14.0)(typescript@5.8.2))(vite@6.2.5(@types/node@22.14.0)(yaml@2.7.1))': dependencies: '@vitest/spy': 3.1.1 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: + msw: 2.7.3(@types/node@22.14.0)(typescript@5.8.2) vite: 6.2.5(@types/node@22.14.0)(yaml@2.7.1) '@vitest/pretty-format@3.1.1': @@ -4062,7 +4490,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.12 tinyrainbow: 2.0.0 - vitest: 3.1.1(@types/node@22.14.0)(@vitest/ui@3.1.1)(yaml@2.7.1) + vitest: 3.1.1(@types/node@22.14.0)(@vitest/ui@3.1.1)(msw@2.7.3(@types/node@22.14.0)(typescript@5.8.2))(yaml@2.7.1) '@vitest/utils@3.1.1': dependencies: @@ -4104,6 +4532,8 @@ snapshots: '@web3-storage/multipart-parser@1.0.0': {} + '@zod/core@0.8.1': {} + '@zxing/text-encoding@0.9.0': optional: true @@ -4126,6 +4556,11 @@ snapshots: ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + optional: true + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -4152,6 +4587,11 @@ snapshots: argparse@2.0.1: {} + arktype@2.1.20: + dependencies: + '@ark/schema': 0.46.0 + '@ark/util': 0.46.0 + arktype@2.1.9: dependencies: '@ark/schema': 0.44.4 @@ -4280,12 +4720,22 @@ snapshots: cli-spinners@2.9.2: {} + cli-width@4.1.0: + optional: true + cliui@7.0.4: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + optional: true + clone@1.0.4: {} color-convert@2.0.1: @@ -4789,6 +5239,9 @@ snapshots: graphemer@1.4.0: {} + graphql@16.10.0: + optional: true + has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -4805,6 +5258,9 @@ snapshots: dependencies: function-bind: 1.1.2 + headers-polyfill@4.0.3: + optional: true + html-escaper@2.0.2: {} human-id@4.1.1: {} @@ -4868,6 +5324,9 @@ snapshots: is-interactive@1.0.0: {} + is-node-process@1.2.0: + optional: true + is-number@7.0.0: {} is-obj@1.0.1: {} @@ -5091,8 +5550,37 @@ snapshots: ms@2.1.3: {} + msw@2.7.3(@types/node@22.14.0)(typescript@5.8.2): + dependencies: + '@bundled-es-modules/cookie': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 5.1.9(@types/node@22.14.0) + '@mswjs/interceptors': 0.37.6 + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + graphql: 16.10.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + strict-event-emitter: 0.5.1 + type-fest: 4.39.1 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + optional: true + multipasta@0.2.5: {} + mute-stream@2.0.0: + optional: true + nanoid@3.3.11: {} natural-compare@1.4.0: {} @@ -5145,6 +5633,9 @@ snapshots: outdent@0.5.0: {} + outvariant@1.4.3: + optional: true + p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -5199,6 +5690,9 @@ snapshots: lru-cache: 11.1.0 minipass: 7.1.2 + path-to-regexp@6.3.0: + optional: true + path-type@4.0.0: {} pathe@2.0.3: {} @@ -5260,6 +5754,11 @@ snapshots: dependencies: parse-ms: 2.1.0 + psl@1.15.0: + dependencies: + punycode: 2.3.1 + optional: true + punycode@2.3.1: {} pure-rand@6.1.0: {} @@ -5268,6 +5767,9 @@ snapshots: quansync@0.2.10: {} + querystringify@2.2.0: + optional: true + queue-microtask@1.2.3: {} quote-unquote@1.0.0: {} @@ -5319,6 +5821,9 @@ snapshots: requirejs@2.3.7: {} + requires-port@1.0.0: + optional: true + resolve-dependency-path@4.0.1: {} resolve-from@4.0.0: {} @@ -5420,6 +5925,8 @@ snapshots: mrmime: 2.0.1 totalist: 3.0.1 + sisteransi@1.0.5: {} + slash@2.0.0: {} slash@3.0.0: {} @@ -5448,6 +5955,9 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: + optional: true + std-env@3.9.0: {} stream-slice@0.1.2: {} @@ -5456,6 +5966,9 @@ snapshots: dependencies: any-promise: 1.3.0 + strict-event-emitter@0.5.1: + optional: true + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5545,6 +6058,14 @@ snapshots: totalist@3.0.1: {} + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + optional: true + tr46@0.0.3: {} treeify@1.1.0: {} @@ -5572,6 +6093,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.21.3: + optional: true + + type-fest@4.39.1: + optional: true + typescript-eslint@8.29.0(eslint@9.24.0)(typescript@5.8.2): dependencies: '@typescript-eslint/eslint-plugin': 8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.24.0)(typescript@5.8.2))(eslint@9.24.0)(typescript@5.8.2) @@ -5592,6 +6119,9 @@ snapshots: universalify@0.1.2: {} + universalify@0.2.0: + optional: true + update-browserslist-db@1.1.3(browserslist@4.24.4): dependencies: browserslist: 4.24.4 @@ -5602,6 +6132,12 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + optional: true + use-sync-external-store@1.5.0(react@19.1.0): dependencies: react: 19.1.0 @@ -5651,10 +6187,10 @@ snapshots: fsevents: 2.3.3 yaml: 2.7.1 - vitest@3.1.1(@types/node@22.14.0)(@vitest/ui@3.1.1)(yaml@2.7.1): + vitest@3.1.1(@types/node@22.14.0)(@vitest/ui@3.1.1)(msw@2.7.3(@types/node@22.14.0)(typescript@5.8.2))(yaml@2.7.1): dependencies: '@vitest/expect': 3.1.1 - '@vitest/mocker': 3.1.1(vite@6.2.5(@types/node@22.14.0)(yaml@2.7.1)) + '@vitest/mocker': 3.1.1(msw@2.7.3(@types/node@22.14.0)(typescript@5.8.2))(vite@6.2.5(@types/node@22.14.0)(yaml@2.7.1)) '@vitest/pretty-format': 3.1.1 '@vitest/runner': 3.1.1 '@vitest/snapshot': 3.1.1 @@ -5732,6 +6268,13 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + optional: true + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -5756,6 +6299,9 @@ snapshots: yargs-parser@20.2.9: {} + yargs-parser@21.1.1: + optional: true + yargs@16.2.0: dependencies: cliui: 7.0.4 @@ -5766,6 +6312,24 @@ snapshots: y18n: 5.0.8 yargs-parser: 20.2.9 + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + optional: true + yocto-queue@0.1.0: {} + yoctocolors-cjs@2.1.2: + optional: true + zod@3.24.2: {} + + zod@4.0.0-beta.20250420T053007: + dependencies: + '@zod/core': 0.8.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4e81e10c..0393de29 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,22 @@ packages: - examples/*/ - bin - packages/*/ +catalog: + '@ark/attest': ^0.44.8 + '@babel/cli': ^7.25.9 + '@babel/core': ^7.26.0 + '@babel/plugin-transform-export-namespace-from': ^7.25.9 + '@babel/plugin-transform-modules-commonjs': ^7.25.9 + '@changesets/changelog-github': ^0.5.0 + '@changesets/cli': ^2.27.9 + '@fast-check/vitest': ^0.2.0 + '@types/node': ^22.9.0 + '@types/madge': ^5.0.3 + '@vitest/coverage-v8': 3.1.1 + '@vitest/ui': 3.1.1 + babel-plugin-annotate-pure-calls: ^0.4.0 + fast-check: ^4.0.1 + typescript: 5.8.2 + vitest: ^3.0.4 + madge: ^8.0.0 + tinybench: ^3.0.4 diff --git a/tsconfig.base.json b/tsconfig.base.json index 22fb6db7..b5034be0 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -32,24 +32,56 @@ "@traversable/derive-codec/*": ["packages/derive-codec/src/*.js"], "@traversable/derive-equals": ["packages/derive-equals/src/index.js"], "@traversable/derive-equals/*": ["packages/derive-equals/src/*.js"], - "@traversable/derive-validators": ["packages/derive-validators/src/index.js"], - "@traversable/derive-validators/*": ["packages/derive-validators/src/*.js"], + "@traversable/derive-validators": [ + "packages/derive-validators/src/index.js" + ], + "@traversable/derive-validators/*": [ + "packages/derive-validators/src/*.js" + ], "@traversable/json": ["packages/json/src/index.js"], "@traversable/json/*": ["packages/json/src/*.js"], "@traversable/registry": ["packages/registry/src/index.js"], "@traversable/registry/*": ["packages/registry/src/*.js"], "@traversable/schema": ["packages/schema/src/index.js"], - "@traversable/schema/*": ["packages/schema/src/*.js"], + "@traversable/schema-arbitrary": [ + "packages/schema-arbitrary/src/index.js" + ], + "@traversable/schema-arbitrary/*": ["packages/schema-arbitrary/*.js"], + "@traversable/schema-core": ["packages/schema-core/src/index.js"], + "@traversable/schema-core/*": ["packages/schema-core/src/*.js"], + "@traversable/schema-generator": [ + "packages/schema-generator/src/index.js" + ], + "@traversable/schema-generator/*": ["packages/schema-generator/*.js"], + "@traversable/schema-jit-compiler": [ + "packages/schema-jit-compiler/src/index.js" + ], + "@traversable/schema-jit-compiler/*": [ + "packages/schema-jit-compiler/*.js" + ], "@traversable/schema-seed": ["packages/schema-seed/src/index.js"], "@traversable/schema-seed/*": ["packages/schema-seed/src/*.js"], - "@traversable/schema-to-json-schema": ["packages/schema-to-json-schema/src/index.js"], - "@traversable/schema-to-json-schema/*": ["packages/schema-to-json-schema/src/*.js"], - "@traversable/schema-to-string": ["packages/schema-to-string/src/index.js"], + "@traversable/schema-to-json-schema": [ + "packages/schema-to-json-schema/src/index.js" + ], + "@traversable/schema-to-json-schema/*": [ + "packages/schema-to-json-schema/src/*.js" + ], + "@traversable/schema-to-string": [ + "packages/schema-to-string/src/index.js" + ], "@traversable/schema-to-string/*": ["packages/schema-to-string/src/*.js"], - "@traversable/schema-valibot-adapter": ["packages/schema-valibot-adapter/src/index.js"], - "@traversable/schema-valibot-adapter/*": [ "packages/schema-valibot-adapter/src/*.js" ], - "@traversable/schema-zod-adapter": ["packages/schema-zod-adapter/src/index.js"], - "@traversable/schema-zod-adapter/*": ["packages/schema-zod-adapter/*.js"] + "@traversable/schema-valibot-adapter": [ + "packages/schema-valibot-adapter/src/index.js" + ], + "@traversable/schema-valibot-adapter/*": [ + "packages/schema-valibot-adapter/src/*.js" + ], + "@traversable/schema-zod-adapter": [ + "packages/schema-zod-adapter/src/index.js" + ], + "@traversable/schema-zod-adapter/*": ["packages/schema-zod-adapter/*.js"], + "@traversable/schema/*": ["packages/schema/*.js"] } } } diff --git a/tsconfig.build.json b/tsconfig.build.json index 808c4b4d..e60adcaf 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -7,6 +7,10 @@ { "path": "packages/derive-validators/tsconfig.build.json" }, { "path": "packages/json/tsconfig.build.json" }, { "path": "packages/registry/tsconfig.build.json" }, + { "path": "packages/schema-arbitrary/tsconfig.build.json" }, + { "path": "packages/schema-core/tsconfig.build.json" }, + { "path": "packages/schema-generator/tsconfig.build.json" }, + { "path": "packages/schema-jit-compiler/tsconfig.build.json" }, { "path": "packages/schema-seed/tsconfig.build.json" }, { "path": "packages/schema-to-json-schema/tsconfig.build.json" }, { "path": "packages/schema-to-string/tsconfig.build.json" }, diff --git a/tsconfig.json b/tsconfig.json index 376603e0..c6fdd90a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,10 @@ { "path": "packages/json" }, { "path": "packages/registry" }, { "path": "packages/schema" }, + { "path": "packages/schema-arbitrary" }, + { "path": "packages/schema-core" }, + { "path": "packages/schema-generator" }, + { "path": "packages/schema-jit-compiler" }, { "path": "packages/schema-seed" }, { "path": "packages/schema-to-json-schema" }, { "path": "packages/schema-to-string" }, diff --git a/vite.config.ts b/vite.config.ts index 90858cfb..c5e169ad 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -25,12 +25,12 @@ export default defineConfig({ alias: ALIASES, coverage: { include: [ - 'packages/schema/src/**.ts', + 'packages/schema-core/src/**.ts', ], enabled: true, reporter: ['html'], reportsDirectory: './config/coverage', - thresholds: { "100": true }, + // thresholds: { "100": true }, }, disableConsoleIntercept: true, fakeTimers: { toFake: undefined }, @@ -45,6 +45,7 @@ export default defineConfig({ // include: ['test/**/*.test.ts'], printConsoleTrace: true, sequence: { concurrent: true }, + slowTestThreshold: 400, workspace: [ 'examples/*', 'packages/*', @@ -52,4 +53,3 @@ export default defineConfig({ ], }, }) -