@scalar/validation
Advanced tools
| import type { Schema } from './schema.js'; | ||
| export type GenerateTypesOptions = { | ||
| maxDepth?: number; | ||
| /** | ||
| * ISO 8601 timestamp printed in the autogenerated file banner. | ||
| * Defaults to the time of the `generateTypes` call when the banner is emitted. | ||
| */ | ||
| generatedAt?: string; | ||
| /** | ||
| * When set to a valid TypeScript identifier, wraps all emitted `export type` declarations (and any | ||
| * trailing root type) in `export namespace Name { ... }` so consumers reference `Name.SomeType`. | ||
| */ | ||
| namespace?: string; | ||
| }; | ||
| /** | ||
| * Returns TypeScript for the schema: named `typeName` nodes become `export type` aliases (once each), | ||
| * referenced by name elsewhere. With no named nodes, returns a single inline type expression (same as before). | ||
| * | ||
| * When at least one named type is emitted, the result is prefixed with a banner stating the output is | ||
| * autogenerated and must not be edited manually, plus a generation timestamp. | ||
| * | ||
| * Pass `namespace` to wrap declarations in `export namespace … { … }`. | ||
| */ | ||
| export declare const generateTypes: (schema: Schema, options?: GenerateTypesOptions) => string; | ||
| //# sourceMappingURL=typegen.d.ts.map |
| {"version":3,"file":"typegen.d.ts","sourceRoot":"","sources":["../src/typegen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAItC,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAcD;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,EAAE,UAAU,oBAAoB,KAAG,MAuB9E,CAAA"} |
+221
| const DEFAULT_MAX_DEPTH = 10; | ||
| /** | ||
| * Returns TypeScript for the schema: named `typeName` nodes become `export type` aliases (once each), | ||
| * referenced by name elsewhere. With no named nodes, returns a single inline type expression (same as before). | ||
| * | ||
| * When at least one named type is emitted, the result is prefixed with a banner stating the output is | ||
| * autogenerated and must not be edited manually, plus a generation timestamp. | ||
| * | ||
| * Pass `namespace` to wrap declarations in `export namespace … { … }`. | ||
| */ | ||
| export const generateTypes = (schema, options) => { | ||
| const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH; | ||
| const ctx = { | ||
| definitions: new Map(), | ||
| declarations: [], | ||
| inProgress: new Set(), | ||
| }; | ||
| const root = emitSchema(schema, maxDepth, ctx, ''); | ||
| if (ctx.declarations.length === 0) { | ||
| return root; | ||
| } | ||
| const declStrings = ctx.declarations.map(formatNamedDeclaration); | ||
| const body = declStrings.join('\n\n'); | ||
| const lastDeclared = ctx.declarations.at(-1)?.name; | ||
| let content = lastDeclared === root ? body : `${body}\n\n${root}`; | ||
| const ns = options?.namespace; | ||
| if (ns && isValidTypeScriptIdentifier(ns)) { | ||
| content = wrapDeclarationsInNamespace(ns, content); | ||
| } | ||
| const generatedAt = options?.generatedAt ?? new Date().toISOString(); | ||
| return `${formatAutogeneratedFileBanner(generatedAt)}${content}`; | ||
| }; | ||
| const formatAutogeneratedFileBanner = (generatedAt) => `/**\n * This file is autogenerated. Do not edit it.\n *\n * Generated at: ${generatedAt}\n */\n\n`; | ||
| const wrapDeclarationsInNamespace = (namespace, content) => { | ||
| const body = content | ||
| .split('\n') | ||
| .map((line) => (line === '' ? '' : ` ${line}`)) | ||
| .join('\n'); | ||
| return `export namespace ${namespace} {\n${body}\n}\n`; | ||
| }; | ||
| const formatNamedDeclaration = (d) => { | ||
| const commentBlock = d.comment ? formatTypeCommentAsJsDoc(d.comment) : ''; | ||
| const comment = commentBlock ? `${commentBlock}\n` : ''; | ||
| return `${comment}export type ${d.name} = ${d.body}`; | ||
| }; | ||
| /** Inline JSDoc for one line; multi-line comments use a newline after the opening delimiter and ` * ` on each line. */ | ||
| const formatTypeCommentAsJsDoc = (comment) => { | ||
| const lines = comment.split(/\r?\n/); | ||
| const trimmed = lines.map((line) => line.trim()); | ||
| while (trimmed.length > 0 && trimmed[0] === '') { | ||
| trimmed.shift(); | ||
| } | ||
| while (trimmed.length > 0 && trimmed.at(-1) === '') { | ||
| trimmed.pop(); | ||
| } | ||
| if (trimmed.length === 0) { | ||
| return ''; | ||
| } | ||
| if (trimmed.length === 1) { | ||
| return `/** ${trimmed[0]} */`; | ||
| } | ||
| const body = trimmed.map((line) => ` * ${line}`).join('\n'); | ||
| return `/** \n${body}\n */`; | ||
| }; | ||
| /** Prefixes each line of a JSDoc block with `indent` for object property comments. */ | ||
| const formatTypeCommentAsIndentedJsDoc = (indent, comment) => { | ||
| const doc = formatTypeCommentAsJsDoc(comment); | ||
| if (!doc) { | ||
| return ''; | ||
| } | ||
| return doc | ||
| .split('\n') | ||
| .map((line) => `${indent}${line}`) | ||
| .join('\n'); | ||
| }; | ||
| const getTypeName = (schema) => { | ||
| if (schema.type === 'lazy' || schema.type === 'evaluate') { | ||
| return undefined; | ||
| } | ||
| const name = schema.typeName; | ||
| if (!name || !isValidTypeScriptIdentifier(name)) { | ||
| return undefined; | ||
| } | ||
| return name; | ||
| }; | ||
| const getTypeComment = (schema) => { | ||
| if (schema.type === 'lazy' || schema.type === 'evaluate') { | ||
| return undefined; | ||
| } | ||
| if (schema.type === 'optional') { | ||
| return schema.typeComment ?? getTypeComment(schema.schema); | ||
| } | ||
| return schema.typeComment; | ||
| }; | ||
| const isValidTypeScriptIdentifier = (name) => /^[$_A-Za-z][$_\w]*$/.test(name); | ||
| const emitSchema = (schema, depth, ctx, braceIndent) => { | ||
| const name = getTypeName(schema); | ||
| if (name) { | ||
| if (ctx.definitions.has(name)) { | ||
| return name; | ||
| } | ||
| if (ctx.inProgress.has(name)) { | ||
| return name; | ||
| } | ||
| ctx.inProgress.add(name); | ||
| const body = structuralEmit(schema, depth, ctx, ''); | ||
| ctx.inProgress.delete(name); | ||
| if (!ctx.definitions.has(name)) { | ||
| ctx.definitions.set(name, body); | ||
| ctx.declarations.push({ | ||
| name, | ||
| body, | ||
| comment: getTypeComment(schema), | ||
| }); | ||
| } | ||
| return name; | ||
| } | ||
| return structuralEmit(schema, depth, ctx, braceIndent); | ||
| }; | ||
| const structuralEmit = (schema, depth, ctx, braceIndent) => { | ||
| if (depth <= 0) { | ||
| return 'any'; | ||
| } | ||
| const next = depth - 1; | ||
| switch (schema.type) { | ||
| case 'number': | ||
| return 'number'; | ||
| case 'string': | ||
| return 'string'; | ||
| case 'boolean': | ||
| return 'boolean'; | ||
| case 'nullable': | ||
| return 'null'; | ||
| case 'notDefined': | ||
| return 'undefined'; | ||
| case 'any': | ||
| return 'any'; | ||
| case 'array': { | ||
| const item = emitSchema(schema.items, next, ctx, braceIndent); | ||
| return needsArrayItemParen(item) ? `(${item})[]` : `${item}[]`; | ||
| } | ||
| case 'record': { | ||
| const key = emitSchema(schema.key, next, ctx, braceIndent); | ||
| const value = emitSchema(schema.value, next, ctx, braceIndent); | ||
| return `Record<${key}, ${value}>`; | ||
| } | ||
| case 'object': { | ||
| const entries = Object.entries(schema.properties); | ||
| if (entries.length === 0) { | ||
| return '{}'; | ||
| } | ||
| const keyIndent = `${braceIndent} `; | ||
| const props = entries.map(([key, child]) => { | ||
| const tsKey = /^[$_a-zA-Z][$_\w]*$/.test(key) ? key : JSON.stringify(key); | ||
| const optionalProp = child.type === 'optional'; | ||
| const valueSchema = optionalProp ? child.schema : child; | ||
| const value = emitSchema(valueSchema, next, ctx, keyIndent); | ||
| const propComment = getTypeComment(child); | ||
| const docBlock = propComment ? formatTypeCommentAsIndentedJsDoc(keyIndent, propComment) : ''; | ||
| const propLine = optionalProp ? `${keyIndent}${tsKey}?: ${value};` : `${keyIndent}${tsKey}: ${value};`; | ||
| return docBlock ? `${docBlock}\n${propLine}` : propLine; | ||
| }); | ||
| return `{\n${props.join('\n')}\n${braceIndent}}`; | ||
| } | ||
| case 'optional': { | ||
| const inner = emitSchema(schema.schema, next, ctx, braceIndent); | ||
| return `${wrapUnionMember(inner)} | undefined`; | ||
| } | ||
| case 'union': | ||
| if (schema.schemas.length === 0) { | ||
| return 'never'; | ||
| } | ||
| return schema.schemas.map((s) => wrapUnionMember(emitSchema(s, next, ctx, braceIndent))).join(' | '); | ||
| case 'intersection': | ||
| if (schema.schemas.length === 0) { | ||
| return 'unknown'; | ||
| } | ||
| return schema.schemas.map((s) => wrapIntersectionMember(emitSchema(s, next, ctx, braceIndent))).join(' & '); | ||
| case 'literal': | ||
| return literalToTs(schema.value); | ||
| case 'lazy': | ||
| return emitSchema(schema.schema(), next, ctx, braceIndent); | ||
| case 'evaluate': | ||
| return emitSchema(schema.schema, next, ctx, braceIndent); | ||
| default: { | ||
| const _exhaustive = schema; | ||
| return _exhaustive; | ||
| } | ||
| } | ||
| }; | ||
| const literalToTs = (value) => { | ||
| if (typeof value === 'bigint') { | ||
| return `${value}n`; | ||
| } | ||
| return JSON.stringify(value); | ||
| }; | ||
| const needsArrayItemParen = (t) => { | ||
| if (t === 'number' || t === 'string' || t === 'boolean' || t === 'null' || t === 'undefined' || t === 'any') { | ||
| return false; | ||
| } | ||
| // Union: `A | B[]` is `A | (B[])`; intersection: `A & B[]` is `A & (B[])`. Wrap the whole item type. | ||
| return t.includes(' | ') || t.includes(' & '); | ||
| }; | ||
| const wrapUnionMember = (t) => { | ||
| if (t === 'number' || t === 'string' || t === 'boolean' || t === 'null' || t === 'undefined' || t === 'any') { | ||
| return t; | ||
| } | ||
| if (/^(?:-?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?|-?\d+n|"(?:[^"\\]|\\.)*"|true|false)$/.test(t)) { | ||
| return t; | ||
| } | ||
| if (/^[$_A-Za-z][$_\w]*$/.test(t)) { | ||
| return t; | ||
| } | ||
| return `(${t})`; | ||
| }; | ||
| const wrapIntersectionMember = (t) => { | ||
| if (t.includes(' | ') || t.includes(' & ')) { | ||
| return `(${t})`; | ||
| } | ||
| return t; | ||
| }; |
| import { describe, expect, it } from 'vitest' | ||
| import { | ||
| any, | ||
| array, | ||
| boolean, | ||
| evaluate, | ||
| lazy, | ||
| intersection, | ||
| literal, | ||
| notDefined, | ||
| nullable, | ||
| number, | ||
| object, | ||
| optional, | ||
| record, | ||
| string, | ||
| union, | ||
| } from '@/schema' | ||
| import { generateTypes } from '@/typegen' | ||
| const fixedGeneratedAt = '2000-01-01T00:00:00.000Z' | ||
| const autogeneratedBanner = `/**\n * This file is autogenerated. Do not edit it.\n *\n * Generated at: ${fixedGeneratedAt}\n */\n\n` | ||
| describe('typegen', () => { | ||
| it('emits primitives and special scalars', () => { | ||
| expect(generateTypes(number())).toBe('number') | ||
| expect(generateTypes(string())).toBe('string') | ||
| expect(generateTypes(boolean())).toBe('boolean') | ||
| expect(generateTypes(nullable())).toBe('null') | ||
| expect(generateTypes(notDefined())).toBe('undefined') | ||
| expect(generateTypes(any())).toBe('any') | ||
| }) | ||
| it('emits arrays with parentheses when the item is a union or intersection', () => { | ||
| expect(generateTypes(array(number()))).toBe('number[]') | ||
| expect(generateTypes(array(union([number(), string()])))).toBe('(number | string)[]') | ||
| expect(generateTypes(array(intersection([object({ a: number() }), object({ b: string() })])))).toBe( | ||
| '({\n a: number;\n} & {\n b: string;\n})[]', | ||
| ) | ||
| }) | ||
| it('emits records and objects', () => { | ||
| expect(generateTypes(record(string(), number()))).toBe('Record<string, number>') | ||
| expect( | ||
| generateTypes( | ||
| object({ | ||
| id: number(), | ||
| name: string(), | ||
| }), | ||
| ), | ||
| ).toBe('{\n id: number;\n name: string;\n}') | ||
| expect(generateTypes(object({}))).toBe('{}') | ||
| }) | ||
| it('indents nested object properties', () => { | ||
| const car = object({ | ||
| make: string(), | ||
| model: string(), | ||
| year: number(), | ||
| test: object({ test: string() }), | ||
| }) | ||
| expect(generateTypes(car)).toBe( | ||
| '{\n make: string;\n model: string;\n year: number;\n test: {\n test: string;\n };\n}', | ||
| ) | ||
| }) | ||
| it('extracts typeName into a single export and references by name', () => { | ||
| const user = object( | ||
| { | ||
| id: number(), | ||
| name: string(), | ||
| }, | ||
| { typeName: 'User' }, | ||
| ) | ||
| expect(generateTypes(user, { generatedAt: fixedGeneratedAt })).toBe( | ||
| `${autogeneratedBanner}export type User = {\n id: number;\n name: string;\n}`, | ||
| ) | ||
| }) | ||
| it('does not duplicate named types when the same name appears multiple times', () => { | ||
| const user = object({ id: number() }, { typeName: 'User' }) | ||
| const doc = object( | ||
| { | ||
| author: user, | ||
| editor: user, | ||
| }, | ||
| { typeName: 'Document' }, | ||
| ) | ||
| const out = generateTypes(doc, { generatedAt: fixedGeneratedAt }) | ||
| expect(out.match(/export type User/g)?.length).toBe(1) | ||
| expect(out).toContain('author: User') | ||
| expect(out).toContain('editor: User') | ||
| expect(out).toContain('export type Document =') | ||
| }) | ||
| it('includes typeComment as JSDoc on named exports', () => { | ||
| const t = object({}, { typeName: 'Empty', typeComment: 'No fields.' }) | ||
| expect(generateTypes(t, { generatedAt: fixedGeneratedAt })).toBe( | ||
| `${autogeneratedBanner}/** No fields. */\nexport type Empty = {}`, | ||
| ) | ||
| }) | ||
| it('formats multi-line typeComment as a JSDoc block', () => { | ||
| const t = object({}, { typeName: 'T', typeComment: 'Line 1\nLine 2' }) | ||
| expect(generateTypes(t, { generatedAt: fixedGeneratedAt })).toBe( | ||
| `${autogeneratedBanner}/** \n * Line 1\n * Line 2\n */\nexport type T = {}`, | ||
| ) | ||
| }) | ||
| it('prefixes file-style output with an autogenerated banner and timestamp', () => { | ||
| const t = object({ x: number() }, { typeName: 'T' }) | ||
| const out = generateTypes(t, { generatedAt: fixedGeneratedAt }) | ||
| expect(out.startsWith(autogeneratedBanner)).toBe(true) | ||
| expect(out).toContain('Generated at: 2000-01-01T00:00:00.000Z') | ||
| }) | ||
| it('wraps named types in export namespace when namespace option is set', () => { | ||
| const user = object( | ||
| { | ||
| id: number(), | ||
| name: string(), | ||
| }, | ||
| { typeName: 'User' }, | ||
| ) | ||
| expect(generateTypes(user, { generatedAt: fixedGeneratedAt, namespace: 'Models' })).toBe( | ||
| `${autogeneratedBanner}export namespace Models {\n export type User = {\n id: number;\n name: string;\n }\n}\n`, | ||
| ) | ||
| }) | ||
| it('keeps cross-references unqualified inside a namespace', () => { | ||
| const user = object({ id: number() }, { typeName: 'User' }) | ||
| const doc = object({ author: user }, { typeName: 'Document' }) | ||
| const out = generateTypes(doc, { generatedAt: fixedGeneratedAt, namespace: 'Api' }) | ||
| expect(out).toContain('export namespace Api {') | ||
| expect(out).toContain('author: User') | ||
| expect(out.match(/export type User/g)?.length).toBe(1) | ||
| }) | ||
| it('ignores namespace when it is not a valid TypeScript identifier', () => { | ||
| const t = object({ x: number() }, { typeName: 'T' }) | ||
| const out = generateTypes(t, { generatedAt: fixedGeneratedAt, namespace: 'not-valid' }) | ||
| expect(out).not.toContain('export namespace') | ||
| expect(out).toContain('export type T =') | ||
| }) | ||
| it('includes typeComment as JSDoc on object properties', () => { | ||
| const t = object({ | ||
| id: number({ typeComment: 'Unique id.' }), | ||
| name: string({ typeComment: 'Display name.' }), | ||
| }) | ||
| expect(generateTypes(t)).toBe('{\n /** Unique id. */\n id: number;\n /** Display name. */\n name: string;\n}') | ||
| }) | ||
| it('indents multi-line property typeComment in nested objects', () => { | ||
| const t = object({ | ||
| outer: string({ typeComment: 'A\nB' }), | ||
| inner: object({ | ||
| x: number({ typeComment: 'Nested.' }), | ||
| }), | ||
| }) | ||
| expect(generateTypes(t)).toBe( | ||
| '{\n /** \n * A\n * B\n */\n outer: string;\n inner: {\n /** Nested. */\n x: number;\n };\n}', | ||
| ) | ||
| }) | ||
| it('ignores invalid typeName and keeps inline shape', () => { | ||
| const bad = object({ x: number() }, { typeName: 'not-valid' }) | ||
| expect(generateTypes(bad)).toBe('{\n x: number;\n}') | ||
| }) | ||
| it('quotes object keys that are not valid identifiers', () => { | ||
| expect(generateTypes(object({ 'foo-bar': string() }))).toBe('{\n "foo-bar": string;\n}') | ||
| }) | ||
| it('emits unions and optionals', () => { | ||
| expect(generateTypes(union([literal(1), literal(2)]))).toBe('1 | 2') | ||
| expect(generateTypes(optional(number()))).toBe('number | undefined') | ||
| }) | ||
| it('emits never for an empty union and unknown for an empty intersection', () => { | ||
| expect(generateTypes(union([]))).toBe('never') | ||
| expect(generateTypes(intersection([]))).toBe('unknown') | ||
| }) | ||
| it('emits optional object properties with ? instead of | undefined', () => { | ||
| expect( | ||
| generateTypes( | ||
| object({ | ||
| id: number(), | ||
| name: optional(string()), | ||
| }), | ||
| ), | ||
| ).toBe('{\n id: number;\n name?: string;\n}') | ||
| }) | ||
| it('emits literal bigint', () => { | ||
| expect(generateTypes(literal(10n))).toBe('10n') | ||
| }) | ||
| it('unwraps evaluate to the inner schema type', () => { | ||
| expect(generateTypes(evaluate((v) => v, number()))).toBe('number') | ||
| }) | ||
| it('resolves lazy schemas', () => { | ||
| const schema = lazy(() => object({ n: number() })) | ||
| expect(generateTypes(schema)).toBe('{\n n: number;\n}') | ||
| }) | ||
| it('returns any when max depth is exhausted', () => { | ||
| expect(generateTypes(number(), { maxDepth: 0 })).toBe('any') | ||
| }) | ||
| }) |
+268
| import type { Schema } from './schema' | ||
| const DEFAULT_MAX_DEPTH = 10 | ||
| export type GenerateTypesOptions = { | ||
| maxDepth?: number | ||
| /** | ||
| * ISO 8601 timestamp printed in the autogenerated file banner. | ||
| * Defaults to the time of the `generateTypes` call when the banner is emitted. | ||
| */ | ||
| generatedAt?: string | ||
| /** | ||
| * When set to a valid TypeScript identifier, wraps all emitted `export type` declarations (and any | ||
| * trailing root type) in `export namespace Name { ... }` so consumers reference `Name.SomeType`. | ||
| */ | ||
| namespace?: string | ||
| } | ||
| type NamedDeclaration = { | ||
| name: string | ||
| body: string | ||
| comment?: string | ||
| } | ||
| type TypeGenContext = { | ||
| definitions: Map<string, string> | ||
| declarations: NamedDeclaration[] | ||
| inProgress: Set<string> | ||
| } | ||
| /** | ||
| * Returns TypeScript for the schema: named `typeName` nodes become `export type` aliases (once each), | ||
| * referenced by name elsewhere. With no named nodes, returns a single inline type expression (same as before). | ||
| * | ||
| * When at least one named type is emitted, the result is prefixed with a banner stating the output is | ||
| * autogenerated and must not be edited manually, plus a generation timestamp. | ||
| * | ||
| * Pass `namespace` to wrap declarations in `export namespace … { … }`. | ||
| */ | ||
| export const generateTypes = (schema: Schema, options?: GenerateTypesOptions): string => { | ||
| const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH | ||
| const ctx: TypeGenContext = { | ||
| definitions: new Map(), | ||
| declarations: [], | ||
| inProgress: new Set(), | ||
| } | ||
| const root = emitSchema(schema, maxDepth, ctx, '') | ||
| if (ctx.declarations.length === 0) { | ||
| return root | ||
| } | ||
| const declStrings = ctx.declarations.map(formatNamedDeclaration) | ||
| const body = declStrings.join('\n\n') | ||
| const lastDeclared = ctx.declarations.at(-1)?.name | ||
| let content = lastDeclared === root ? body : `${body}\n\n${root}` | ||
| const ns = options?.namespace | ||
| if (ns && isValidTypeScriptIdentifier(ns)) { | ||
| content = wrapDeclarationsInNamespace(ns, content) | ||
| } | ||
| const generatedAt = options?.generatedAt ?? new Date().toISOString() | ||
| return `${formatAutogeneratedFileBanner(generatedAt)}${content}` | ||
| } | ||
| const formatAutogeneratedFileBanner = (generatedAt: string): string => | ||
| `/**\n * This file is autogenerated. Do not edit it.\n *\n * Generated at: ${generatedAt}\n */\n\n` | ||
| const wrapDeclarationsInNamespace = (namespace: string, content: string): string => { | ||
| const body = content | ||
| .split('\n') | ||
| .map((line) => (line === '' ? '' : ` ${line}`)) | ||
| .join('\n') | ||
| return `export namespace ${namespace} {\n${body}\n}\n` | ||
| } | ||
| const formatNamedDeclaration = (d: NamedDeclaration): string => { | ||
| const commentBlock = d.comment ? formatTypeCommentAsJsDoc(d.comment) : '' | ||
| const comment = commentBlock ? `${commentBlock}\n` : '' | ||
| return `${comment}export type ${d.name} = ${d.body}` | ||
| } | ||
| /** Inline JSDoc for one line; multi-line comments use a newline after the opening delimiter and ` * ` on each line. */ | ||
| const formatTypeCommentAsJsDoc = (comment: string): string => { | ||
| const lines = comment.split(/\r?\n/) | ||
| const trimmed = lines.map((line) => line.trim()) | ||
| while (trimmed.length > 0 && trimmed[0] === '') { | ||
| trimmed.shift() | ||
| } | ||
| while (trimmed.length > 0 && trimmed.at(-1) === '') { | ||
| trimmed.pop() | ||
| } | ||
| if (trimmed.length === 0) { | ||
| return '' | ||
| } | ||
| if (trimmed.length === 1) { | ||
| return `/** ${trimmed[0]} */` | ||
| } | ||
| const body = trimmed.map((line) => ` * ${line}`).join('\n') | ||
| return `/** \n${body}\n */` | ||
| } | ||
| /** Prefixes each line of a JSDoc block with `indent` for object property comments. */ | ||
| const formatTypeCommentAsIndentedJsDoc = (indent: string, comment: string): string => { | ||
| const doc = formatTypeCommentAsJsDoc(comment) | ||
| if (!doc) { | ||
| return '' | ||
| } | ||
| return doc | ||
| .split('\n') | ||
| .map((line) => `${indent}${line}`) | ||
| .join('\n') | ||
| } | ||
| const getTypeName = (schema: Schema): string | undefined => { | ||
| if (schema.type === 'lazy' || schema.type === 'evaluate') { | ||
| return undefined | ||
| } | ||
| const name = schema.typeName | ||
| if (!name || !isValidTypeScriptIdentifier(name)) { | ||
| return undefined | ||
| } | ||
| return name | ||
| } | ||
| const getTypeComment = (schema: Schema): string | undefined => { | ||
| if (schema.type === 'lazy' || schema.type === 'evaluate') { | ||
| return undefined | ||
| } | ||
| if (schema.type === 'optional') { | ||
| return schema.typeComment ?? getTypeComment(schema.schema) | ||
| } | ||
| return schema.typeComment | ||
| } | ||
| const isValidTypeScriptIdentifier = (name: string): boolean => /^[$_A-Za-z][$_\w]*$/.test(name) | ||
| const emitSchema = (schema: Schema, depth: number, ctx: TypeGenContext, braceIndent: string): string => { | ||
| const name = getTypeName(schema) | ||
| if (name) { | ||
| if (ctx.definitions.has(name)) { | ||
| return name | ||
| } | ||
| if (ctx.inProgress.has(name)) { | ||
| return name | ||
| } | ||
| ctx.inProgress.add(name) | ||
| const body = structuralEmit(schema, depth, ctx, '') | ||
| ctx.inProgress.delete(name) | ||
| if (!ctx.definitions.has(name)) { | ||
| ctx.definitions.set(name, body) | ||
| ctx.declarations.push({ | ||
| name, | ||
| body, | ||
| comment: getTypeComment(schema), | ||
| }) | ||
| } | ||
| return name | ||
| } | ||
| return structuralEmit(schema, depth, ctx, braceIndent) | ||
| } | ||
| const structuralEmit = (schema: Schema, depth: number, ctx: TypeGenContext, braceIndent: string): string => { | ||
| if (depth <= 0) { | ||
| return 'any' | ||
| } | ||
| const next = depth - 1 | ||
| switch (schema.type) { | ||
| case 'number': | ||
| return 'number' | ||
| case 'string': | ||
| return 'string' | ||
| case 'boolean': | ||
| return 'boolean' | ||
| case 'nullable': | ||
| return 'null' | ||
| case 'notDefined': | ||
| return 'undefined' | ||
| case 'any': | ||
| return 'any' | ||
| case 'array': { | ||
| const item = emitSchema(schema.items, next, ctx, braceIndent) | ||
| return needsArrayItemParen(item) ? `(${item})[]` : `${item}[]` | ||
| } | ||
| case 'record': { | ||
| const key = emitSchema(schema.key, next, ctx, braceIndent) | ||
| const value = emitSchema(schema.value, next, ctx, braceIndent) | ||
| return `Record<${key}, ${value}>` | ||
| } | ||
| case 'object': { | ||
| const entries = Object.entries(schema.properties) | ||
| if (entries.length === 0) { | ||
| return '{}' | ||
| } | ||
| const keyIndent = `${braceIndent} ` | ||
| const props = entries.map(([key, child]) => { | ||
| const tsKey = /^[$_a-zA-Z][$_\w]*$/.test(key) ? key : JSON.stringify(key) | ||
| const optionalProp = child.type === 'optional' | ||
| const valueSchema = optionalProp ? child.schema : child | ||
| const value = emitSchema(valueSchema, next, ctx, keyIndent) | ||
| const propComment = getTypeComment(child) | ||
| const docBlock = propComment ? formatTypeCommentAsIndentedJsDoc(keyIndent, propComment) : '' | ||
| const propLine = optionalProp ? `${keyIndent}${tsKey}?: ${value};` : `${keyIndent}${tsKey}: ${value};` | ||
| return docBlock ? `${docBlock}\n${propLine}` : propLine | ||
| }) | ||
| return `{\n${props.join('\n')}\n${braceIndent}}` | ||
| } | ||
| case 'optional': { | ||
| const inner = emitSchema(schema.schema, next, ctx, braceIndent) | ||
| return `${wrapUnionMember(inner)} | undefined` | ||
| } | ||
| case 'union': | ||
| if (schema.schemas.length === 0) { | ||
| return 'never' | ||
| } | ||
| return schema.schemas.map((s) => wrapUnionMember(emitSchema(s, next, ctx, braceIndent))).join(' | ') | ||
| case 'intersection': | ||
| if (schema.schemas.length === 0) { | ||
| return 'unknown' | ||
| } | ||
| return schema.schemas.map((s) => wrapIntersectionMember(emitSchema(s, next, ctx, braceIndent))).join(' & ') | ||
| case 'literal': | ||
| return literalToTs(schema.value) | ||
| case 'lazy': | ||
| return emitSchema(schema.schema(), next, ctx, braceIndent) | ||
| case 'evaluate': | ||
| return emitSchema(schema.schema, next, ctx, braceIndent) | ||
| default: { | ||
| const _exhaustive: never = schema | ||
| return _exhaustive | ||
| } | ||
| } | ||
| } | ||
| const literalToTs = (value: string | number | boolean | bigint): string => { | ||
| if (typeof value === 'bigint') { | ||
| return `${value}n` | ||
| } | ||
| return JSON.stringify(value) | ||
| } | ||
| const needsArrayItemParen = (t: string): boolean => { | ||
| if (t === 'number' || t === 'string' || t === 'boolean' || t === 'null' || t === 'undefined' || t === 'any') { | ||
| return false | ||
| } | ||
| // Union: `A | B[]` is `A | (B[])`; intersection: `A & B[]` is `A & (B[])`. Wrap the whole item type. | ||
| return t.includes(' | ') || t.includes(' & ') | ||
| } | ||
| const wrapUnionMember = (t: string): string => { | ||
| if (t === 'number' || t === 'string' || t === 'boolean' || t === 'null' || t === 'undefined' || t === 'any') { | ||
| return t | ||
| } | ||
| if (/^(?:-?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?|-?\d+n|"(?:[^"\\]|\\.)*"|true|false)$/.test(t)) { | ||
| return t | ||
| } | ||
| if (/^[$_A-Za-z][$_\w]*$/.test(t)) { | ||
| return t | ||
| } | ||
| return `(${t})` | ||
| } | ||
| const wrapIntersectionMember = (t: string): string => { | ||
| if (t.includes(' | ') || t.includes(' & ')) { | ||
| return `(${t})` | ||
| } | ||
| return t | ||
| } |
| > @scalar/validation@0.1.0 build /home/runner/work/scalar/scalar/packages/validation | ||
| > @scalar/validation@0.2.0 build /home/runner/work/scalar/scalar/packages/validation | ||
| > tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json | ||
+6
-0
| # @scalar/validation | ||
| ## 0.2.0 | ||
| ### Minor Changes | ||
| - [#8600](https://github.com/scalar/scalar/pull/8600): feat: first class support for optional and intersection types | ||
| ## 0.1.0 | ||
@@ -4,0 +10,0 @@ |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"coerce.d.ts","sourceRoot":"","sources":["../src/coerce.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAwDrC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,SAAS,MAAM,EACrC,QAAQ,CAAC,EACT,OAAO,OAAO,EACd,QAAO,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAiB,KAClD,MAAM,CAAC,CAAC,CA0FV,CAAA"} | ||
| {"version":3,"file":"coerce.d.ts","sourceRoot":"","sources":["../src/coerce.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AA0FrC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,SAAS,MAAM,EACrC,QAAQ,CAAC,EACT,OAAO,OAAO,EACd,QAAO,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAiB,KAClD,MAAM,CAAC,CAAC,CA6GV,CAAA"} |
+60
-9
| import { isObject } from './helpers/is-object.js'; | ||
| import { validate } from './validate.js'; | ||
| /** | ||
| * True when this property schema is only used to discriminate union branches | ||
| * (single literal, or a union of literals). No presence bonus when the value | ||
| * does not match — avoids ties like `type: literal('a')` vs `type: union([lit('b'), lit('c')])`. | ||
| */ | ||
| const isDiscriminatorProperty = (schema) => { | ||
| if (schema.type === 'optional') { | ||
| return isDiscriminatorProperty(schema.schema); | ||
| } | ||
| if (schema.type === 'literal') { | ||
| return true; | ||
| } | ||
| if (schema.type === 'union') { | ||
| return schema.schemas.length > 0 && schema.schemas.every(isDiscriminatorProperty); | ||
| } | ||
| return false; | ||
| }; | ||
| /** | ||
| * Computes a "score" indicating how well a value matches a schema, | ||
@@ -16,12 +33,19 @@ * used for picking the best branch in union coercion. | ||
| } | ||
| // For each key in the schema's properties: | ||
| // - +10 if the value matches an explicit literal for the key. | ||
| // - +1 if the property exists (not literal match). | ||
| // Missing keys contribute 0 (including optional keys — matches prior union heuristics). | ||
| // Discriminator properties (`literal` or `union` of literals): recurse with scoreUnion; | ||
| // matching values get a high weight (×10) so `type: literal('A')` beats unrelated fields | ||
| // on another branch; mismatches score 0 (no "key present" tie-break). | ||
| // Other properties: scoreUnion plus +1 when the value fails validation so `{ a: null }` | ||
| // can still prefer the branch that declares `a`. | ||
| return Object.keys(schema.properties).reduce((acc, key) => { | ||
| const exists = key in value; | ||
| const isLiteralMatch = schema.properties[key].type === 'literal' && value[key] === schema.properties[key].value; | ||
| if (isLiteralMatch) { | ||
| return acc + 10; | ||
| if (!(key in value)) { | ||
| return acc; | ||
| } | ||
| return acc + (exists ? 1 : 0); | ||
| const propSchema = schema.properties[key]; | ||
| const raw = value[key]; | ||
| const base = scoreUnion(propSchema, raw); | ||
| if (isDiscriminatorProperty(propSchema)) { | ||
| return acc + (base > 0 ? base * 10 : 0); | ||
| } | ||
| return acc + (base > 0 ? base : 1); | ||
| }, 0); | ||
@@ -37,2 +61,5 @@ } | ||
| } | ||
| if (schema.type === 'optional') { | ||
| return value === undefined ? 1 : scoreUnion(schema.schema, value); | ||
| } | ||
| if (schema.type === 'union') { | ||
@@ -42,2 +69,8 @@ // For a union, use the highest score among all sub-schemas | ||
| } | ||
| if (schema.type === 'intersection') { | ||
| if (schema.schemas.length === 0) { | ||
| return 1; | ||
| } | ||
| return schema.schemas.reduce((acc, sub) => acc + scoreUnion(sub, value), 0); | ||
| } | ||
| if (schema.type === 'lazy') { | ||
@@ -114,2 +147,8 @@ // For a lazy schema, evaluate the inner schema and recurse | ||
| } | ||
| if (schema.type === 'optional') { | ||
| if (value === undefined) { | ||
| return undefined; | ||
| } | ||
| return coerce(schema.schema, value, cache); | ||
| } | ||
| if (schema.type === 'array') { | ||
@@ -130,3 +169,12 @@ if (!Array.isArray(value)) { | ||
| const target = isObject(value) ? value : null; | ||
| return Object.fromEntries(keys.map((key) => [key, coerce(schema.properties[key], target?.[key], cache)])); | ||
| const entries = []; | ||
| for (const key of keys) { | ||
| const propSchema = schema.properties[key]; | ||
| const raw = target?.[key]; | ||
| if (propSchema.type === 'optional' && raw === undefined) { | ||
| continue; | ||
| } | ||
| entries.push([key, coerce(propSchema, raw, cache)]); | ||
| } | ||
| return Object.fromEntries(entries); | ||
| } | ||
@@ -141,2 +189,5 @@ if (schema.type === 'union') { | ||
| } | ||
| if (schema.type === 'intersection') { | ||
| return schema.schemas.reduce((acc, subSchema) => Object.assign(acc, coerce(subSchema, value, cache)), {}); | ||
| } | ||
| if (schema.type === 'literal') { | ||
@@ -143,0 +194,0 @@ return schema.value; |
+2
-1
| export { coerce } from './coerce.js'; | ||
| export { type Schema, any, array, boolean, evaluate, lazy, literal, notDefined, nullable, number, object, optional, record, string, union, } from './schema.js'; | ||
| export { type AnySchema, type ArraySchema, type BooleanSchema, type EvaluateSchema, type IntersectionSchema, type LazySchema, type LiteralSchema, type NotDefinedSchema, type NullableSchema, type NumberSchema, type ObjectSchema, type OptionalSchema, type RecordSchema, type Schema, type StringSchema, type UnionSchema, any, array, boolean, evaluate, intersection, lazy, literal, notDefined, nullable, number, object, optional, record, string, union, } from './schema.js'; | ||
| export { type GenerateTypesOptions, generateTypes } from './typegen.js'; | ||
| export type { Static } from './types.js'; | ||
| export { validate } from './validate.js'; | ||
| //# sourceMappingURL=index.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EACL,KAAK,MAAM,EACX,GAAG,EACH,KAAK,EACL,OAAO,EACP,QAAQ,EACR,IAAI,EACJ,OAAO,EACP,UAAU,EACV,QAAQ,EACR,MAAM,EACN,MAAM,EACN,QAAQ,EACR,MAAM,EACN,MAAM,EACN,KAAK,GACN,MAAM,UAAU,CAAA;AACjB,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA"} | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EACL,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,GAAG,EACH,KAAK,EACL,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,IAAI,EACJ,OAAO,EACP,UAAU,EACV,QAAQ,EACR,MAAM,EACN,MAAM,EACN,QAAQ,EACR,MAAM,EACN,MAAM,EACN,KAAK,GACN,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACpE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA"} |
+2
-1
| export { coerce } from './coerce.js'; | ||
| export { any, array, boolean, evaluate, lazy, literal, notDefined, nullable, number, object, optional, record, string, union, } from './schema.js'; | ||
| export { any, array, boolean, evaluate, intersection, lazy, literal, notDefined, nullable, number, object, optional, record, string, union, } from './schema.js'; | ||
| export { generateTypes } from './typegen.js'; | ||
| export { validate } from './validate.js'; |
+48
-24
@@ -0,25 +1,36 @@ | ||
| /** | ||
| * Optional metadata for type generation and documentation. | ||
| * - typeName: Used as the exported TypeScript type name if valid. | ||
| * - typeComment: Adds a JSDoc comment to the generated type declaration. | ||
| */ | ||
| type Documentation = Partial<{ | ||
| /** Adds a JSDoc comment to the generated type declaration. */ | ||
| typeComment: string; | ||
| /** Used as the exported TypeScript type name if valid. */ | ||
| typeName: string; | ||
| }>; | ||
| /** Schema for finite numeric values. {@link Static} resolves to `number`. */ | ||
| export type NumberSchema = { | ||
| type: 'number'; | ||
| }; | ||
| } & Documentation; | ||
| /** Schema for string values. {@link Static} resolves to `string`. */ | ||
| export type StringSchema = { | ||
| type: 'string'; | ||
| }; | ||
| } & Documentation; | ||
| /** Schema for boolean values. {@link Static} resolves to `boolean`. */ | ||
| export type BooleanSchema = { | ||
| type: 'boolean'; | ||
| }; | ||
| } & Documentation; | ||
| /** Schema for `null`. {@link Static} resolves to `null`. */ | ||
| export type NullableSchema = { | ||
| type: 'nullable'; | ||
| }; | ||
| } & Documentation; | ||
| /** Schema for a missing or omitted value. {@link Static} resolves to `undefined`. */ | ||
| export type NotDefinedSchema = { | ||
| type: 'notDefined'; | ||
| }; | ||
| } & Documentation; | ||
| /** Schema that accepts any value without narrowing. {@link Static} resolves to `any`. */ | ||
| export type AnySchema = { | ||
| type: 'any'; | ||
| }; | ||
| } & Documentation; | ||
| /** Schema for homogeneous lists. {@link Static} resolves to an array of the item static type. */ | ||
@@ -29,3 +40,3 @@ export type ArraySchema<Item extends Schema> = { | ||
| items: Item; | ||
| }; | ||
| } & Documentation; | ||
| /** Schema for key-value maps with uniform value shape. Keys are constrained to string or number schemas. */ | ||
@@ -36,3 +47,3 @@ export type RecordSchema<Key extends StringSchema | NumberSchema | AnySchema, Value extends Schema> = { | ||
| value: Value; | ||
| }; | ||
| } & Documentation; | ||
| /** Schema for objects with a fixed set of named properties, each with its own schema. */ | ||
@@ -42,3 +53,3 @@ export type ObjectSchema<Properties extends Record<string, Schema>> = { | ||
| properties: Properties; | ||
| }; | ||
| } & Documentation; | ||
| /** Schema that matches if any member schema matches (discriminated union when literals or object tags differ). */ | ||
@@ -48,3 +59,15 @@ export type UnionSchema<Schemas extends Schema[]> = { | ||
| schemas: Schemas; | ||
| }; | ||
| } & Documentation; | ||
| /** | ||
| * Schema that accepts `undefined` or a value matching the inner schema. | ||
| * In {@link Static} and type generation, object properties use `key?:` instead of `T | undefined`. | ||
| */ | ||
| export type OptionalSchema<S extends Schema> = { | ||
| type: 'optional'; | ||
| schema: S; | ||
| } & Documentation; | ||
| export type IntersectionSchema<Schemas extends readonly ObjectSchema<any>[]> = { | ||
| type: 'intersection'; | ||
| schemas: Schemas; | ||
| } & Documentation; | ||
| /** Schema for a single exact constant (string, number, boolean, or bigint). {@link Static} is that literal type. */ | ||
@@ -54,3 +77,3 @@ export type LiteralSchema<T extends string | number | boolean | bigint> = { | ||
| value: T; | ||
| }; | ||
| } & Documentation; | ||
| /** | ||
@@ -74,18 +97,19 @@ * Schema for self-referential or recursive types (such as trees or linked lists). | ||
| }; | ||
| export type Schema = NumberSchema | StringSchema | BooleanSchema | NullableSchema | NotDefinedSchema | AnySchema | ArraySchema<any> | RecordSchema<any, any> | ObjectSchema<Record<string, any>> | UnionSchema<any[]> | LiteralSchema<any> | LazySchema<any> | EvaluateSchema<any>; | ||
| declare const number: () => NumberSchema; | ||
| declare const string: () => StringSchema; | ||
| declare const boolean: () => BooleanSchema; | ||
| declare const nullable: () => NullableSchema; | ||
| declare const notDefined: () => NotDefinedSchema; | ||
| declare const any: () => AnySchema; | ||
| declare const array: <Item extends Schema>(items: Item) => ArraySchema<Item>; | ||
| declare const record: <Key extends StringSchema | AnySchema, Value extends Schema>(key: Key, value: Value) => RecordSchema<Key, Value>; | ||
| declare const object: <Properties extends Record<string, Schema>>(properties: Properties) => ObjectSchema<Properties>; | ||
| declare const union: <Schemas extends Schema[]>(schemas: Schemas) => UnionSchema<Schemas>; | ||
| declare const optional: <S extends Schema>(schema: S) => UnionSchema<(NotDefinedSchema | S)[]>; | ||
| export type Schema = NumberSchema | StringSchema | BooleanSchema | NullableSchema | NotDefinedSchema | AnySchema | ArraySchema<any> | RecordSchema<any, any> | ObjectSchema<Record<string, any>> | UnionSchema<any[]> | OptionalSchema<any> | IntersectionSchema<readonly ObjectSchema<any>[]> | LiteralSchema<any> | LazySchema<any> | EvaluateSchema<any>; | ||
| declare const number: (options?: Documentation) => NumberSchema; | ||
| declare const string: (options?: Documentation) => StringSchema; | ||
| declare const boolean: (options?: Documentation) => BooleanSchema; | ||
| declare const nullable: (options?: Documentation) => NullableSchema; | ||
| declare const notDefined: (options?: Documentation) => NotDefinedSchema; | ||
| declare const any: (options?: Documentation) => AnySchema; | ||
| declare const array: <Item extends Schema>(items: Item, options?: Documentation) => ArraySchema<Item>; | ||
| declare const record: <Key extends StringSchema | AnySchema, Value extends Schema>(key: Key, value: Value, options?: Documentation) => RecordSchema<Key, Value>; | ||
| declare const object: <Properties extends Record<string, Schema>>(properties: Properties, options?: Documentation) => ObjectSchema<Properties>; | ||
| declare const union: <Schemas extends Schema[]>(schemas: Schemas, options?: Documentation) => UnionSchema<Schemas>; | ||
| declare const intersection: <Schemas extends readonly ObjectSchema<any>[]>(schemas: Schemas, options?: Documentation) => IntersectionSchema<Schemas>; | ||
| declare const optional: <S extends Schema>(schema: S, options?: Documentation) => OptionalSchema<S>; | ||
| declare const literal: <Value extends string | number | boolean | bigint>(value: Value) => LiteralSchema<Value>; | ||
| declare const lazy: <S extends () => Schema>(schema: S) => LazySchema<S>; | ||
| declare const evaluate: <S extends Schema>(expression: (value: unknown) => unknown, schema: S) => EvaluateSchema<S>; | ||
| export { number, string, boolean, nullable, notDefined, any, array, record, object, union, optional, literal, lazy, evaluate, }; | ||
| export { number, string, boolean, nullable, notDefined, any, array, record, object, union, intersection, optional, literal, lazy, evaluate, }; | ||
| //# sourceMappingURL=schema.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"} |
+43
-12
@@ -1,37 +0,68 @@ | ||
| const number = () => ({ | ||
| const number = (options) => ({ | ||
| type: 'number', | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }); | ||
| const string = () => ({ | ||
| const string = (options) => ({ | ||
| type: 'string', | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }); | ||
| const boolean = () => ({ | ||
| const boolean = (options) => ({ | ||
| type: 'boolean', | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }); | ||
| const nullable = () => ({ | ||
| const nullable = (options) => ({ | ||
| type: 'nullable', | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }); | ||
| const notDefined = () => ({ | ||
| const notDefined = (options) => ({ | ||
| type: 'notDefined', | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }); | ||
| const any = () => ({ | ||
| const any = (options) => ({ | ||
| type: 'any', | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }); | ||
| const array = (items) => ({ | ||
| const array = (items, options) => ({ | ||
| type: 'array', | ||
| items, | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }); | ||
| const record = (key, value) => ({ | ||
| const record = (key, value, options) => ({ | ||
| type: 'record', | ||
| key, | ||
| value, | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }); | ||
| const object = (properties) => ({ | ||
| const object = (properties, options) => ({ | ||
| type: 'object', | ||
| properties, | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }); | ||
| const union = (schemas) => ({ | ||
| const union = (schemas, options) => ({ | ||
| type: 'union', | ||
| schemas, | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }); | ||
| const optional = (schema) => union([schema, notDefined()]); | ||
| const intersection = (schemas, options) => ({ | ||
| type: 'intersection', | ||
| schemas, | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }); | ||
| const optional = (schema, options) => ({ | ||
| type: 'optional', | ||
| schema, | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }); | ||
| const literal = (value) => ({ | ||
@@ -50,2 +81,2 @@ type: 'literal', | ||
| }); | ||
| export { number, string, boolean, nullable, notDefined, any, array, record, object, union, optional, literal, lazy, evaluate, }; | ||
| export { number, string, boolean, nullable, notDefined, any, array, record, object, union, intersection, optional, literal, lazy, evaluate, }; |
+19
-3
@@ -1,8 +0,24 @@ | ||
| import type { AnySchema, ArraySchema, BooleanSchema, EvaluateSchema, LazySchema, LiteralSchema, NotDefinedSchema, NullableSchema, NumberSchema, ObjectSchema, RecordSchema, StringSchema, UnionSchema } from './schema.js'; | ||
| import type { AnySchema, ArraySchema, BooleanSchema, EvaluateSchema, IntersectionSchema, LazySchema, LiteralSchema, NotDefinedSchema, NullableSchema, NumberSchema, ObjectSchema, OptionalSchema, RecordSchema, StringSchema, UnionSchema } from './schema.js'; | ||
| export type Static<T> = _Static<T, 10>; | ||
| type _Static<T, Depth extends number = 10> = Depth extends 0 ? any : T extends LiteralSchema<infer Value> ? Value : T extends NumberSchema ? number : T extends StringSchema ? string : T extends BooleanSchema ? boolean : T extends NullableSchema ? null : T extends NotDefinedSchema ? undefined : T extends AnySchema ? any : T extends ArraySchema<infer Item> ? Array<_Static<Item, Prev<Depth>>> : T extends RecordSchema<infer Key, infer Value> ? Record<_Static<Key, Prev<Depth>> & PropertyKey, _Static<Value, Prev<Depth>>> : T extends ObjectSchema<infer Properties> ? { | ||
| /** Folds a tuple of object schemas into an intersection of their static object types. */ | ||
| type IntersectObjectStatics<Schemas extends readonly ObjectSchema<any>[], Depth extends number> = Schemas extends readonly [infer First extends ObjectSchema<any>, ...infer Rest extends readonly ObjectSchema<any>[]] ? _Static<First, Depth> & IntersectObjectStatics<Rest, Depth> : {}; | ||
| type OptionalPropertyKeys<P> = { | ||
| [K in keyof P]: P[K] extends OptionalSchema<any> ? K : never; | ||
| }[keyof P]; | ||
| type RequiredPropertyKeys<P> = { | ||
| [K in keyof P]: P[K] extends OptionalSchema<any> ? never : K; | ||
| }[keyof P]; | ||
| type OptionalSchemaInner<S> = S extends OptionalSchema<infer Inner> ? Inner : never; | ||
| type ObjectStatics<Properties, Depth extends number> = [keyof Properties] extends [never] ? {} : OptionalPropertyKeys<Properties> extends never ? { | ||
| [K in keyof Properties]: _Static<Properties[K], Prev<Depth>>; | ||
| } : T extends UnionSchema<infer Schemas> ? _Static<Schemas[number], Prev<Depth>> : T extends EvaluateSchema<infer S> ? _Static<S, Prev<Depth>> : T extends LazySchema<infer S> ? _Static<ReturnType<S>, Prev<Depth>> : never; | ||
| } : RequiredPropertyKeys<Properties> extends never ? { | ||
| [K in OptionalPropertyKeys<Properties>]?: _Static<OptionalSchemaInner<Properties[K]>, Prev<Depth>>; | ||
| } : { | ||
| [K in RequiredPropertyKeys<Properties>]: _Static<Properties[K], Prev<Depth>>; | ||
| } & { | ||
| [K in OptionalPropertyKeys<Properties>]?: _Static<OptionalSchemaInner<Properties[K]>, Prev<Depth>>; | ||
| }; | ||
| type _Static<T, Depth extends number = 10> = Depth extends 0 ? any : T extends LiteralSchema<infer Value> ? Value : T extends NumberSchema ? number : T extends StringSchema ? string : T extends BooleanSchema ? boolean : T extends NullableSchema ? null : T extends NotDefinedSchema ? undefined : T extends AnySchema ? any : T extends ArraySchema<infer Item> ? Array<_Static<Item, Prev<Depth>>> : T extends RecordSchema<infer Key, infer Value> ? Record<_Static<Key, Prev<Depth>> & PropertyKey, _Static<Value, Prev<Depth>>> : T extends ObjectSchema<infer Properties> ? ObjectStatics<Properties, Depth> : T extends OptionalSchema<infer S> ? _Static<S, Prev<Depth>> | undefined : T extends IntersectionSchema<infer Schemas extends readonly ObjectSchema<any>[]> ? IntersectObjectStatics<Schemas, Prev<Depth>> : T extends UnionSchema<infer Schemas> ? _Static<Schemas[number], Prev<Depth>> : T extends EvaluateSchema<infer S> ? _Static<S, Prev<Depth>> : T extends LazySchema<infer S> ? _Static<ReturnType<S>, Prev<Depth>> : never; | ||
| type Prev<T extends number> = T extends 10 ? 9 : T extends 9 ? 8 : T extends 8 ? 7 : T extends 7 ? 6 : T extends 6 ? 5 : T extends 5 ? 4 : T extends 4 ? 3 : T extends 3 ? 2 : T extends 2 ? 1 : T extends 1 ? 0 : 0; | ||
| export {}; | ||
| //# sourceMappingURL=types.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,WAAW,EACX,aAAa,EACb,cAAc,EACd,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACZ,MAAM,UAAU,CAAA;AAGjB,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AAGtC,KAAK,OAAO,CAAC,CAAC,EAAE,KAAK,SAAS,MAAM,GAAG,EAAE,IAAI,KAAK,SAAS,CAAC,GACxD,GAAG,GACH,CAAC,SAAS,aAAa,CAAC,MAAM,KAAK,CAAC,GAClC,KAAK,GACL,CAAC,SAAS,YAAY,GACpB,MAAM,GACN,CAAC,SAAS,YAAY,GACpB,MAAM,GACN,CAAC,SAAS,aAAa,GACrB,OAAO,GACP,CAAC,SAAS,cAAc,GACtB,IAAI,GACJ,CAAC,SAAS,gBAAgB,GACxB,SAAS,GACT,CAAC,SAAS,SAAS,GACjB,GAAG,GACH,CAAC,SAAS,WAAW,CAAC,MAAM,IAAI,CAAC,GAC/B,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GACjC,CAAC,SAAS,YAAY,CAAC,MAAM,GAAG,EAAE,MAAM,KAAK,CAAC,GAC5C,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,WAAW,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAC5E,CAAC,SAAS,YAAY,CAAC,MAAM,UAAU,CAAC,GACtC;KAAG,CAAC,IAAI,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;CAAE,GAChE,CAAC,SAAS,WAAW,CAAC,MAAM,OAAO,CAAC,GAClC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GACrC,CAAC,SAAS,cAAc,CAAC,MAAM,CAAC,CAAC,GAC/B,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GACvB,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAC3B,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GACnC,KAAK,CAAA;AAGnC,KAAK,IAAI,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,EAAE,GACtC,CAAC,GACD,CAAC,SAAS,CAAC,GACT,CAAC,GACD,CAAC,SAAS,CAAC,GACT,CAAC,GACD,CAAC,SAAS,CAAC,GACT,CAAC,GACD,CAAC,SAAS,CAAC,GACT,CAAC,GACD,CAAC,SAAS,CAAC,GACT,CAAC,GACD,CAAC,SAAS,CAAC,GACT,CAAC,GACD,CAAC,SAAS,CAAC,GACT,CAAC,GACD,CAAC,SAAS,CAAC,GACT,CAAC,GACD,CAAC,SAAS,CAAC,GACT,CAAC,GACD,CAAC,CAAA"} | ||
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"} |
@@ -15,6 +15,8 @@ import type { Schema } from './schema.js'; | ||
| * - 'record': Object with string/number keys and values, checked recursively. | ||
| * - 'object': Object with fixed property keys, each validated recursively. | ||
| * - 'object': Plain object with fixed property keys, each validated recursively. | ||
| * - 'union': Accepts if value matches any of the listed schemas. | ||
| * - 'optional': Accepts `undefined` or a value matching the inner schema. | ||
| * - 'intersection': Accepts if value matches every member schema (members are object schemas; value must be a plain object). | ||
| * - 'literal': Exact match with a literal value. | ||
| * - 'recursive': Schema referring to itself for nested validation (e.g. trees). | ||
| * - 'lazy': Delegates to the schema returned by the factory. | ||
| * - 'evaluate': Transforms value then validates against an inner schema. | ||
@@ -21,0 +23,0 @@ * |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,QAAQ,GAAI,QAAQ,MAAM,GAAG,SAAS,EAAE,OAAO,OAAO,KAAG,OAwDrE,CAAA"} | ||
| {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,QAAQ,GAAI,QAAQ,MAAM,GAAG,SAAS,EAAE,OAAO,OAAO,KAAG,OAqErE,CAAA"} |
+17
-2
@@ -15,6 +15,8 @@ import { isObject } from './helpers/is-object.js'; | ||
| * - 'record': Object with string/number keys and values, checked recursively. | ||
| * - 'object': Object with fixed property keys, each validated recursively. | ||
| * - 'object': Plain object with fixed property keys, each validated recursively. | ||
| * - 'union': Accepts if value matches any of the listed schemas. | ||
| * - 'optional': Accepts `undefined` or a value matching the inner schema. | ||
| * - 'intersection': Accepts if value matches every member schema (members are object schemas; value must be a plain object). | ||
| * - 'literal': Exact match with a literal value. | ||
| * - 'recursive': Schema referring to itself for nested validation (e.g. trees). | ||
| * - 'lazy': Delegates to the schema returned by the factory. | ||
| * - 'evaluate': Transforms value then validates against an inner schema. | ||
@@ -73,5 +75,18 @@ * | ||
| } | ||
| if (schema.type === 'optional') { | ||
| return value === undefined || validate(schema.schema, value); | ||
| } | ||
| if (schema.type === 'union') { | ||
| return schema.schemas.some((schema) => validate(schema, value)); | ||
| } | ||
| if (schema.type === 'intersection') { | ||
| if (schema.schemas.length === 0) { | ||
| // Vacuous: no constraints (matches `Array.prototype.every` on an empty list). | ||
| return true; | ||
| } | ||
| if (!isObject(value)) { | ||
| return false; | ||
| } | ||
| return schema.schemas.every((subSchema) => validate(subSchema, value)); | ||
| } | ||
| if (schema.type === 'literal') { | ||
@@ -78,0 +93,0 @@ return value === schema.value; |
+1
-1
@@ -18,3 +18,3 @@ { | ||
| ], | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "engines": { | ||
@@ -21,0 +21,0 @@ "node": ">=20" |
+1
-1
@@ -73,3 +73,3 @@ # `@scalar/validation` | ||
| | `union([a, b, …])` | Matches any member | Union of branches | | ||
| | `optional(s)` | Shorthand for `union([s, notDefined()])` | `Static<s> \| undefined` | | ||
| | `optional(s)` | `undefined` or matches `s` | `Static<s> \| undefined`; in `object({ … })`, property becomes `key?: Static<s>` | | ||
| | `lazy(() => schema)` | Defers schema (recursion) | Inferred from inner schema | | ||
@@ -76,0 +76,0 @@ | `evaluate(fn, schema)` | Runs `fn(value)` then validates `schema` | `Static<schema>` | |
+67
-0
@@ -9,2 +9,3 @@ import { describe, expect, it } from 'vitest' | ||
| evaluate, | ||
| intersection, | ||
| lazy, | ||
@@ -421,2 +422,11 @@ literal, | ||
| }) | ||
| it('omits optional properties when the value is undefined', () => { | ||
| const T = object({ | ||
| id: number(), | ||
| name: optional(string()), | ||
| }) | ||
| expect(coerce(T, { id: 1 })).toEqual({ id: 1 }) | ||
| expect(coerce(T, { id: 1, name: undefined })).toEqual({ id: 1 }) | ||
| expect(coerce(T, { id: 1, name: 'x' })).toEqual({ id: 1, name: 'x' }) | ||
| }) | ||
| }) | ||
@@ -923,4 +933,61 @@ | ||
| }) | ||
| it('picks the branch whose type discriminator matches a union of literals', () => { | ||
| const T = union([ | ||
| object({ | ||
| type: literal('a'), | ||
| a: string(), | ||
| }), | ||
| object({ | ||
| type: union([literal('b'), literal('c')]), | ||
| b: string(), | ||
| }), | ||
| ]) | ||
| expect(coerce(T, { type: 'a' })).toEqual({ type: 'a', a: '' }) | ||
| expect(coerce(T, { type: 'b' })).toEqual({ type: 'b', b: '' }) | ||
| expect(coerce(T, { type: 'c' })).toEqual({ type: 'c', b: '' }) | ||
| }) | ||
| }) | ||
| describe('intersection', () => { | ||
| const T = intersection([ | ||
| object({ | ||
| a: number(), | ||
| b: number(), | ||
| }), | ||
| object({ | ||
| c: string(), | ||
| d: string(), | ||
| }), | ||
| ]) | ||
| it('merges coerced properties from each object schema', () => { | ||
| const result = coerce(T, { a: 1, b: 2, c: 'x', d: 'y' }) | ||
| expect(result).toEqual({ a: 1, b: 2, c: 'x', d: 'y' }) | ||
| }) | ||
| it('fills missing keys per branch from the same input value', () => { | ||
| const result = coerce(T, { a: 'nope', c: 123 }) | ||
| expect(result).toEqual({ a: 0, b: 0, c: '', d: '' }) | ||
| }) | ||
| it('later branch wins on overlapping keys', () => { | ||
| const overlap = intersection([object({ x: number() }), object({ x: string() })]) | ||
| const result = coerce(overlap, { x: 1 }) | ||
| expect(result).toEqual({ x: '' }) | ||
| }) | ||
| it('wins in a union when every member validates and summed score beats narrower members', () => { | ||
| const A = object({ type: literal('A'), onlyA: number() }) | ||
| const B = object({ type: literal('B'), onlyB: string() }) | ||
| const both = intersection([ | ||
| object({ type: literal('A'), shared: number() }), | ||
| object({ shared: number(), extra: string() }), | ||
| ]) | ||
| const T = union([A, B, both]) | ||
| // Intersection merges only its declared keys; it outscores A here because both sub-objects validate. | ||
| expect(coerce(T, { type: 'A', onlyA: 1, shared: 2, extra: 'ok' })).toEqual({ | ||
| type: 'A', | ||
| shared: 2, | ||
| extra: 'ok', | ||
| }) | ||
| }) | ||
| }) | ||
| describe('notDefined', () => { | ||
@@ -927,0 +994,0 @@ const T = notDefined() |
+64
-11
@@ -7,2 +7,20 @@ import { isObject } from './helpers/is-object' | ||
| /** | ||
| * True when this property schema is only used to discriminate union branches | ||
| * (single literal, or a union of literals). No presence bonus when the value | ||
| * does not match — avoids ties like `type: literal('a')` vs `type: union([lit('b'), lit('c')])`. | ||
| */ | ||
| const isDiscriminatorProperty = (schema: Schema): boolean => { | ||
| if (schema.type === 'optional') { | ||
| return isDiscriminatorProperty(schema.schema) | ||
| } | ||
| if (schema.type === 'literal') { | ||
| return true | ||
| } | ||
| if (schema.type === 'union') { | ||
| return schema.schemas.length > 0 && schema.schemas.every(isDiscriminatorProperty) | ||
| } | ||
| return false | ||
| } | ||
| /** | ||
| * Computes a "score" indicating how well a value matches a schema, | ||
@@ -21,12 +39,19 @@ * used for picking the best branch in union coercion. | ||
| // For each key in the schema's properties: | ||
| // - +10 if the value matches an explicit literal for the key. | ||
| // - +1 if the property exists (not literal match). | ||
| // Missing keys contribute 0 (including optional keys — matches prior union heuristics). | ||
| // Discriminator properties (`literal` or `union` of literals): recurse with scoreUnion; | ||
| // matching values get a high weight (×10) so `type: literal('A')` beats unrelated fields | ||
| // on another branch; mismatches score 0 (no "key present" tie-break). | ||
| // Other properties: scoreUnion plus +1 when the value fails validation so `{ a: null }` | ||
| // can still prefer the branch that declares `a`. | ||
| return Object.keys(schema.properties).reduce<number>((acc, key) => { | ||
| const exists = key in value | ||
| const isLiteralMatch = schema.properties[key].type === 'literal' && value[key] === schema.properties[key].value | ||
| if (isLiteralMatch) { | ||
| return acc + 10 | ||
| if (!(key in value)) { | ||
| return acc | ||
| } | ||
| return acc + (exists ? 1 : 0) | ||
| const propSchema = schema.properties[key] | ||
| const raw = value[key as keyof typeof value] | ||
| const base = scoreUnion(propSchema, raw) | ||
| if (isDiscriminatorProperty(propSchema)) { | ||
| return acc + (base > 0 ? base * 10 : 0) | ||
| } | ||
| return acc + (base > 0 ? base : 1) | ||
| }, 0) | ||
@@ -42,2 +67,5 @@ } | ||
| } | ||
| if (schema.type === 'optional') { | ||
| return value === undefined ? 1 : scoreUnion(schema.schema, value) | ||
| } | ||
| if (schema.type === 'union') { | ||
@@ -47,2 +75,8 @@ // For a union, use the highest score among all sub-schemas | ||
| } | ||
| if (schema.type === 'intersection') { | ||
| if (schema.schemas.length === 0) { | ||
| return 1 | ||
| } | ||
| return schema.schemas.reduce((acc, sub) => acc + scoreUnion(sub, value), 0) | ||
| } | ||
@@ -129,2 +163,8 @@ if (schema.type === 'lazy') { | ||
| } | ||
| if (schema.type === 'optional') { | ||
| if (value === undefined) { | ||
| return undefined as unknown as Static<S> | ||
| } | ||
| return coerce(schema.schema, value, cache) | ||
| } | ||
| if (schema.type === 'array') { | ||
@@ -147,5 +187,12 @@ if (!Array.isArray(value)) { | ||
| const target = isObject(value) ? value : null | ||
| return Object.fromEntries( | ||
| keys.map((key) => [key, coerce(schema.properties[key], target?.[key], cache)]), | ||
| ) as unknown as Static<S> | ||
| const entries: [string, unknown][] = [] | ||
| for (const key of keys) { | ||
| const propSchema = schema.properties[key] | ||
| const raw = target?.[key as keyof typeof target] | ||
| if (propSchema.type === 'optional' && raw === undefined) { | ||
| continue | ||
| } | ||
| entries.push([key, coerce(propSchema, raw, cache)]) | ||
| } | ||
| return Object.fromEntries(entries) as unknown as Static<S> | ||
| } | ||
@@ -163,2 +210,8 @@ if (schema.type === 'union') { | ||
| } | ||
| if (schema.type === 'intersection') { | ||
| return schema.schemas.reduce<Record<string, unknown>>( | ||
| (acc, subSchema) => Object.assign(acc, coerce(subSchema, value, cache) as Record<string, unknown>), | ||
| {}, | ||
| ) as unknown as Static<S> | ||
| } | ||
| if (schema.type === 'literal') { | ||
@@ -165,0 +218,0 @@ return schema.value |
+17
-0
| export { coerce } from './coerce' | ||
| export { | ||
| type AnySchema, | ||
| type ArraySchema, | ||
| type BooleanSchema, | ||
| type EvaluateSchema, | ||
| type IntersectionSchema, | ||
| type LazySchema, | ||
| type LiteralSchema, | ||
| type NotDefinedSchema, | ||
| type NullableSchema, | ||
| type NumberSchema, | ||
| type ObjectSchema, | ||
| type OptionalSchema, | ||
| type RecordSchema, | ||
| type Schema, | ||
| type StringSchema, | ||
| type UnionSchema, | ||
| any, | ||
@@ -8,2 +23,3 @@ array, | ||
| evaluate, | ||
| intersection, | ||
| lazy, | ||
@@ -20,3 +36,4 @@ literal, | ||
| } from './schema' | ||
| export { type GenerateTypesOptions, generateTypes } from './typegen' | ||
| export type { Static } from './types' | ||
| export { validate } from './validate' |
+89
-21
@@ -0,5 +1,17 @@ | ||
| /** | ||
| * Optional metadata for type generation and documentation. | ||
| * - typeName: Used as the exported TypeScript type name if valid. | ||
| * - typeComment: Adds a JSDoc comment to the generated type declaration. | ||
| */ | ||
| type Documentation = Partial<{ | ||
| /** Adds a JSDoc comment to the generated type declaration. */ | ||
| typeComment: string | ||
| /** Used as the exported TypeScript type name if valid. */ | ||
| typeName: string | ||
| }> | ||
| /** Schema for finite numeric values. {@link Static} resolves to `number`. */ | ||
| export type NumberSchema = { | ||
| type: 'number' | ||
| } | ||
| } & Documentation | ||
@@ -9,3 +21,3 @@ /** Schema for string values. {@link Static} resolves to `string`. */ | ||
| type: 'string' | ||
| } | ||
| } & Documentation | ||
@@ -15,3 +27,3 @@ /** Schema for boolean values. {@link Static} resolves to `boolean`. */ | ||
| type: 'boolean' | ||
| } | ||
| } & Documentation | ||
@@ -21,3 +33,3 @@ /** Schema for `null`. {@link Static} resolves to `null`. */ | ||
| type: 'nullable' | ||
| } | ||
| } & Documentation | ||
@@ -27,3 +39,3 @@ /** Schema for a missing or omitted value. {@link Static} resolves to `undefined`. */ | ||
| type: 'notDefined' | ||
| } | ||
| } & Documentation | ||
@@ -33,3 +45,3 @@ /** Schema that accepts any value without narrowing. {@link Static} resolves to `any`. */ | ||
| type: 'any' | ||
| } | ||
| } & Documentation | ||
@@ -40,3 +52,3 @@ /** Schema for homogeneous lists. {@link Static} resolves to an array of the item static type. */ | ||
| items: Item | ||
| } | ||
| } & Documentation | ||
@@ -48,3 +60,3 @@ /** Schema for key-value maps with uniform value shape. Keys are constrained to string or number schemas. */ | ||
| value: Value | ||
| } | ||
| } & Documentation | ||
@@ -55,3 +67,3 @@ /** Schema for objects with a fixed set of named properties, each with its own schema. */ | ||
| properties: Properties | ||
| } | ||
| } & Documentation | ||
@@ -62,4 +74,18 @@ /** Schema that matches if any member schema matches (discriminated union when literals or object tags differ). */ | ||
| schemas: Schemas | ||
| } | ||
| } & Documentation | ||
| /** | ||
| * Schema that accepts `undefined` or a value matching the inner schema. | ||
| * In {@link Static} and type generation, object properties use `key?:` instead of `T | undefined`. | ||
| */ | ||
| export type OptionalSchema<S extends Schema> = { | ||
| type: 'optional' | ||
| schema: S | ||
| } & Documentation | ||
| export type IntersectionSchema<Schemas extends readonly ObjectSchema<any>[]> = { | ||
| type: 'intersection' | ||
| schemas: Schemas | ||
| } & Documentation | ||
| /** Schema for a single exact constant (string, number, boolean, or bigint). {@link Static} is that literal type. */ | ||
@@ -69,3 +95,3 @@ export type LiteralSchema<T extends string | number | boolean | bigint> = { | ||
| value: T | ||
| } | ||
| } & Documentation | ||
@@ -103,2 +129,4 @@ /** | ||
| | UnionSchema<any[]> | ||
| | OptionalSchema<any> | ||
| | IntersectionSchema<readonly ObjectSchema<any>[]> | ||
| | LiteralSchema<any> | ||
@@ -108,29 +136,43 @@ | LazySchema<any> | ||
| const number = (): NumberSchema => ({ | ||
| const number = (options?: Documentation): NumberSchema => ({ | ||
| type: 'number', | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }) | ||
| const string = (): StringSchema => ({ | ||
| const string = (options?: Documentation): StringSchema => ({ | ||
| type: 'string', | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }) | ||
| const boolean = (): BooleanSchema => ({ | ||
| const boolean = (options?: Documentation): BooleanSchema => ({ | ||
| type: 'boolean', | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }) | ||
| const nullable = (): NullableSchema => ({ | ||
| const nullable = (options?: Documentation): NullableSchema => ({ | ||
| type: 'nullable', | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }) | ||
| const notDefined = (): NotDefinedSchema => ({ | ||
| const notDefined = (options?: Documentation): NotDefinedSchema => ({ | ||
| type: 'notDefined', | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }) | ||
| const any = (): AnySchema => ({ | ||
| const any = (options?: Documentation): AnySchema => ({ | ||
| type: 'any', | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }) | ||
| const array = <Item extends Schema>(items: Item): ArraySchema<Item> => ({ | ||
| const array = <Item extends Schema>(items: Item, options?: Documentation): ArraySchema<Item> => ({ | ||
| type: 'array', | ||
| items, | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }) | ||
@@ -141,2 +183,3 @@ | ||
| value: Value, | ||
| options?: Documentation, | ||
| ): RecordSchema<Key, Value> => ({ | ||
@@ -146,16 +189,40 @@ type: 'record', | ||
| value, | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }) | ||
| const object = <Properties extends Record<string, Schema>>(properties: Properties): ObjectSchema<Properties> => ({ | ||
| const object = <Properties extends Record<string, Schema>>( | ||
| properties: Properties, | ||
| options?: Documentation, | ||
| ): ObjectSchema<Properties> => ({ | ||
| type: 'object', | ||
| properties, | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }) | ||
| const union = <Schemas extends Schema[]>(schemas: Schemas): UnionSchema<Schemas> => ({ | ||
| const union = <Schemas extends Schema[]>(schemas: Schemas, options?: Documentation): UnionSchema<Schemas> => ({ | ||
| type: 'union', | ||
| schemas, | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }) | ||
| const optional = <S extends Schema>(schema: S) => union([schema, notDefined()]) | ||
| const intersection = <Schemas extends readonly ObjectSchema<any>[]>( | ||
| schemas: Schemas, | ||
| options?: Documentation, | ||
| ): IntersectionSchema<Schemas> => ({ | ||
| type: 'intersection', | ||
| schemas, | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }) | ||
| const optional = <S extends Schema>(schema: S, options?: Documentation): OptionalSchema<S> => ({ | ||
| type: 'optional', | ||
| schema, | ||
| typeName: options?.typeName, | ||
| typeComment: options?.typeComment, | ||
| }) | ||
| const literal = <Value extends string | number | boolean | bigint>(value: Value): LiteralSchema<Value> => ({ | ||
@@ -188,2 +255,3 @@ type: 'literal', | ||
| union, | ||
| intersection, | ||
| optional, | ||
@@ -190,0 +258,0 @@ literal, |
+42
-8
@@ -6,2 +6,3 @@ import type { | ||
| EvaluateSchema, | ||
| IntersectionSchema, | ||
| LazySchema, | ||
@@ -13,2 +14,3 @@ LiteralSchema, | ||
| ObjectSchema, | ||
| OptionalSchema, | ||
| RecordSchema, | ||
@@ -22,2 +24,30 @@ StringSchema, | ||
| /** Folds a tuple of object schemas into an intersection of their static object types. */ | ||
| type IntersectObjectStatics< | ||
| Schemas extends readonly ObjectSchema<any>[], | ||
| Depth extends number, | ||
| > = Schemas extends readonly [infer First extends ObjectSchema<any>, ...infer Rest extends readonly ObjectSchema<any>[]] | ||
| ? _Static<First, Depth> & IntersectObjectStatics<Rest, Depth> | ||
| : {} | ||
| type OptionalPropertyKeys<P> = { | ||
| [K in keyof P]: P[K] extends OptionalSchema<any> ? K : never | ||
| }[keyof P] | ||
| type RequiredPropertyKeys<P> = { | ||
| [K in keyof P]: P[K] extends OptionalSchema<any> ? never : K | ||
| }[keyof P] | ||
| type OptionalSchemaInner<S> = S extends OptionalSchema<infer Inner> ? Inner : never | ||
| type ObjectStatics<Properties, Depth extends number> = [keyof Properties] extends [never] | ||
| ? {} | ||
| : OptionalPropertyKeys<Properties> extends never | ||
| ? { [K in keyof Properties]: _Static<Properties[K], Prev<Depth>> } | ||
| : RequiredPropertyKeys<Properties> extends never | ||
| ? { [K in OptionalPropertyKeys<Properties>]?: _Static<OptionalSchemaInner<Properties[K]>, Prev<Depth>> } | ||
| : { [K in RequiredPropertyKeys<Properties>]: _Static<Properties[K], Prev<Depth>> } & { | ||
| [K in OptionalPropertyKeys<Properties>]?: _Static<OptionalSchemaInner<Properties[K]>, Prev<Depth>> | ||
| } | ||
| // Internal type with depth counter | ||
@@ -45,10 +75,14 @@ type _Static<T, Depth extends number = 10> = Depth extends 0 | ||
| : T extends ObjectSchema<infer Properties> | ||
| ? { [K in keyof Properties]: _Static<Properties[K], Prev<Depth>> } | ||
| : T extends UnionSchema<infer Schemas> | ||
| ? _Static<Schemas[number], Prev<Depth>> | ||
| : T extends EvaluateSchema<infer S> | ||
| ? _Static<S, Prev<Depth>> | ||
| : T extends LazySchema<infer S> | ||
| ? _Static<ReturnType<S>, Prev<Depth>> | ||
| : never | ||
| ? ObjectStatics<Properties, Depth> | ||
| : T extends OptionalSchema<infer S> | ||
| ? _Static<S, Prev<Depth>> | undefined | ||
| : T extends IntersectionSchema<infer Schemas extends readonly ObjectSchema<any>[]> | ||
| ? IntersectObjectStatics<Schemas, Prev<Depth>> | ||
| : T extends UnionSchema<infer Schemas> | ||
| ? _Static<Schemas[number], Prev<Depth>> | ||
| : T extends EvaluateSchema<infer S> | ||
| ? _Static<S, Prev<Depth>> | ||
| : T extends LazySchema<infer S> | ||
| ? _Static<ReturnType<S>, Prev<Depth>> | ||
| : never | ||
@@ -55,0 +89,0 @@ // Helper type to decrement depth counter |
+53
-0
@@ -8,2 +8,3 @@ import { describe, expect, it } from 'vitest' | ||
| evaluate, | ||
| intersection, | ||
| lazy, | ||
@@ -656,2 +657,54 @@ literal, | ||
| describe('intersection', () => { | ||
| it('passes when the value satisfies every member object schema', () => { | ||
| const T = intersection([object({ a: number(), b: number() }), object({ c: string(), d: string() })]) | ||
| expect(validate(T, { a: 1, b: 2, c: 'x', d: 'y' })).toBe(true) | ||
| }) | ||
| it('fails when one member object schema fails', () => { | ||
| const T = intersection([object({ a: number(), b: number() }), object({ c: string(), d: string() })]) | ||
| expect(validate(T, { a: 1, b: 2, c: 'x', d: 3 })).toBe(false) | ||
| }) | ||
| it('fails when the first member fails even if later members would pass', () => { | ||
| const T = intersection([object({ x: literal(1) }), object({ y: string() })]) | ||
| expect(validate(T, { x: 2, y: 'ok' })).toBe(false) | ||
| }) | ||
| it('requires overlapping keys to satisfy every arm that declares them', () => { | ||
| const T = intersection([object({ id: number() }), object({ id: string() })]) | ||
| expect(validate(T, { id: 1 })).toBe(false) | ||
| expect(validate(T, { id: '1' })).toBe(false) | ||
| }) | ||
| it('rejects non-plain objects before member checks', () => { | ||
| const T = intersection([object({ x: number() }), object({ y: number() })]) | ||
| expect(validate(T, null)).toBe(false) | ||
| expect(validate(T, undefined)).toBe(false) | ||
| expect(validate(T, 0)).toBe(false) | ||
| expect(validate(T, [])).toBe(false) | ||
| expect(validate(T, new Date())).toBe(false) | ||
| }) | ||
| it('treats an empty intersection as vacuously valid', () => { | ||
| const T = intersection([]) | ||
| expect(validate(T, null)).toBe(true) | ||
| expect(validate(T, { a: 1 })).toBe(true) | ||
| }) | ||
| it('matches a single member the same as that object schema alone', () => { | ||
| const O = object({ x: number() }) | ||
| const T = intersection([O]) | ||
| expect(validate(T, { x: 1 })).toBe(true) | ||
| expect(validate(T, {})).toBe(false) | ||
| expect(validate(O, { x: 1 })).toBe(validate(T, { x: 1 })) | ||
| }) | ||
| it('validates members that use lazy schemas', () => { | ||
| const T = intersection([object({ a: number() }), object({ nested: lazy(() => object({ z: string() })) })]) | ||
| expect(validate(T, { a: 1, nested: { z: 'ok' } })).toBe(true) | ||
| expect(validate(T, { a: 1, nested: { z: 1 } })).toBe(false) | ||
| }) | ||
| }) | ||
| describe('notDefined', () => { | ||
@@ -658,0 +711,0 @@ const T = notDefined() |
+17
-2
@@ -17,6 +17,8 @@ import { isObject } from './helpers/is-object' | ||
| * - 'record': Object with string/number keys and values, checked recursively. | ||
| * - 'object': Object with fixed property keys, each validated recursively. | ||
| * - 'object': Plain object with fixed property keys, each validated recursively. | ||
| * - 'union': Accepts if value matches any of the listed schemas. | ||
| * - 'optional': Accepts `undefined` or a value matching the inner schema. | ||
| * - 'intersection': Accepts if value matches every member schema (members are object schemas; value must be a plain object). | ||
| * - 'literal': Exact match with a literal value. | ||
| * - 'recursive': Schema referring to itself for nested validation (e.g. trees). | ||
| * - 'lazy': Delegates to the schema returned by the factory. | ||
| * - 'evaluate': Transforms value then validates against an inner schema. | ||
@@ -76,5 +78,18 @@ * | ||
| } | ||
| if (schema.type === 'optional') { | ||
| return value === undefined || validate(schema.schema, value) | ||
| } | ||
| if (schema.type === 'union') { | ||
| return schema.schemas.some((schema) => validate(schema, value)) | ||
| } | ||
| if (schema.type === 'intersection') { | ||
| if (schema.schemas.length === 0) { | ||
| // Vacuous: no constraints (matches `Array.prototype.every` on an empty list). | ||
| return true | ||
| } | ||
| if (!isObject(value)) { | ||
| return false | ||
| } | ||
| return schema.schemas.every((subSchema) => validate(subSchema, value)) | ||
| } | ||
| if (schema.type === 'literal') { | ||
@@ -81,0 +96,0 @@ return value === schema.value |
148361
47.71%40
14.29%4088
37.32%