Repository navigation
feat: add recursive and mutually recursive schemas #1741
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
james-tindal
wants to merge
39
commits into
open-circle:main
Choose a base branch
from
james-tindal:feat/recursive-schemas
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+8,500
−89
Open
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
74b52ce
feat: add recursive schemas
yslpn 58e4b74
fix: update reference from 'recursive' to 'recursiveAsync' in lazyAsy…
yslpn 0fcabea
test: verify getter call in recursive and recursiveAsync schemas
yslpn 73df739
feat: add test for converting recursive schema without colliding with…
yslpn 3d356e8
docs: update API list in recursiveAsync documentation to include addi…
yslpn 8911bf6
feat: add support for recursive tuples with rest in recursive schema
yslpn 18289af
refactor: add comments for recursive types
yslpn fd3fd94
fix: add recursive schema types to public api and update documentation
yslpn 6a562d0
test: expand recursive schema test coverage
james-tindal 6ea2946
fix: preserve recursive markers through transforms
james-tindal 6f1167e
feat: add mutual recursion and strengthen recursive schemas
james-tindal b5ec6d1
deduplicate recursive schema machinery
james-tindal 2df2f15
docs: document recursive and mutual recursive schemas
james-tindal 6843c1b
cubic 1: fix: traverse all keys when checking recursive references
james-tindal d5ca171
cubic 3: fix: preserve empty recursive definition names
james-tindal 6df0f60
cubic 4: fix: reuse named definitions for mutual recursive references
james-tindal f1b577b
cubic 6: test: assert recursive parsing preserves configuration values
james-tindal 19f948b
cubic 5: docs: list mutualRecursive JSON Schema support
james-tindal 0d8247b
cubic 7: docs: link recursive from recursiveAsync
james-tindal 0508cd0
cubic 2: feat: support registered generics in recursive types
james-tindal ff87d69
test: reproduce recursive schema review findings
james-tindal a716570
fix: register named mutual targets before definition conversion
james-tindal d2e9237
fix: store prototype-named schema definitions as own properties
james-tindal e1ad175
fix: preserve recursive reference identity for overrides
james-tindal f405bda
fix: preserve pipeline constraints beside recursive references
james-tindal de22ea0
fix: identify recursive references with a private symbol
james-tindal 0286582
fix: preserve all own mutual recursive entries
james-tindal 8063e72
preserve branded primitives in recursive references
james-tindal 460089e
resolve numeric mutual recursive entry keys as strings
james-tindal b930d0f
preserve extra properties on unnamed recursive references
james-tindal 84ea92b
collect recursive references from marker intersections
james-tindal 4b4e6f8
avoid skipping distinct subtypes in recursive reference discovery
james-tindal 97ee3cb
resolve recursive references in function return types
james-tindal 30a6cd8
resolve recursive references in function parameter types
james-tindal cda0ff7
resolve nested union issues in recursive schemas
james-tindal 79b7d55
resolve named references in mutual union subissues
james-tindal 06c3da0
traverse recursive map keys and values independently
james-tindal e12d914
preserve recursive references beside unknown and any generic arguments
james-tindal 00a7fd0
preserve callable properties during recursive type resolution
james-tindal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
226 changes: 226 additions & 0 deletions
226
library/src/schemas/mutualRecursive/_mutualRecursive.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| import type { | ||
| BaseIssue, | ||
| GenericSchema, | ||
| GenericSchemaAsync, | ||
| InferInput, | ||
| InferIssue, | ||
| InferOutput, | ||
| } from '../../types/index.ts'; | ||
| import { | ||
| checkRecursiveReferences, | ||
| createRecursiveSchema, | ||
| createRef, | ||
| type RecursiveMarker, | ||
| type RecursiveOwner, | ||
| type RecursiveReferences, | ||
| type Ref, | ||
| type RefAsync, | ||
| type ResolveRecursiveInput, | ||
| type ResolveRecursiveIssue, | ||
| type ResolveRecursiveOutput, | ||
| } from '../recursive/_recursive.ts'; | ||
| import type { MutualRecursiveSchema } from './mutualRecursive.ts'; | ||
| import type { MutualRecursiveSchemaAsync } from './mutualRecursiveAsync.ts'; | ||
|
|
||
| /** | ||
| * Factory for named synchronous recursive references. | ||
| */ | ||
| export type RefFactory = <const TName extends string = never>( | ||
| name: TName | ||
| ) => Ref<TName>; | ||
|
|
||
| /** | ||
| * Factory for named async recursive references. | ||
| */ | ||
| export type RefFactoryAsync = <const TName extends string = never>( | ||
| name: TName | ||
| ) => RefAsync<TName>; | ||
|
|
||
| /** | ||
| * Creates a factory for named synchronous recursive references. | ||
| * | ||
| * @param owner The owner of the references. | ||
| * | ||
| * @returns A named reference factory. | ||
| * | ||
| * @internal | ||
| */ | ||
| export function createRefFactory( | ||
| owner: RecursiveOwner & { readonly async: false } | ||
| ): RefFactory; | ||
|
|
||
| /** | ||
| * Creates a factory for named async recursive references. | ||
| * | ||
| * @param owner The owner of the references. | ||
| * | ||
| * @returns A named async reference factory. | ||
| * | ||
| * @internal | ||
| */ | ||
| export function createRefFactory( | ||
| owner: RecursiveOwner & { readonly async: true } | ||
| ): RefFactoryAsync; | ||
| export function createRefFactory(owner: RecursiveOwner) { | ||
| return <const TName extends string = never>(name: TName) => | ||
| createRef(owner, name); | ||
| } | ||
|
|
||
| type SyncOrAsyncSchema = GenericSchema | GenericSchemaAsync; | ||
| type Entries = Record<string, SyncOrAsyncSchema>; | ||
|
|
||
| /** | ||
| * Mutual recursive construction options. | ||
| * | ||
| * @internal | ||
| */ | ||
| export type MutualConstruction = { | ||
| readonly reference: RecursiveOwner['reference']; | ||
| } & ( | ||
| | { | ||
| readonly type: 'mutualRecursive'; | ||
| readonly async: false; | ||
| readonly build: (ref: RefFactory) => Record<string, GenericSchema>; | ||
| } | ||
| | { | ||
| readonly type: 'mutualRecursiveAsync'; | ||
| readonly async: true; | ||
| readonly build: (ref: RefFactoryAsync) => Entries; | ||
| } | ||
| ); | ||
|
|
||
| /** | ||
| * Creates the entries for a mutually recursive schema group. | ||
| * | ||
| * @param owner The recursive owner and construction options. | ||
| * @param setEntries The function that installs the entries. | ||
| * | ||
| * @returns A map of recursive schemas. | ||
| * | ||
| * @internal | ||
| */ | ||
| export function createMutualRecursive( | ||
| owner: MutualConstruction & RecursiveOwner, | ||
| setEntries: (entries: Entries) => void | ||
| ): Record<string, Entries[string]> { | ||
| // @ts-expect-error TS can't correlate the owner's sync/async build callback with its matching reference factory. | ||
| const entries = owner.build(createRefFactory(owner)); | ||
| setEntries(entries); | ||
| const seen = new Set<object>(); | ||
| const schemas: Record<PropertyKey, Entries[string]> = Object.create(null); | ||
| for (const key of Reflect.ownKeys(entries)) { | ||
| const entry = (entries as Record<PropertyKey, Entries[string]>)[key]; | ||
| checkRecursiveReferences(entry, owner, seen); | ||
| schemas[key] = createRecursiveSchema< | ||
| Entries[string] & { readonly wrapped: Entries[string] } | ||
| >(entry, owner, owner.type, owner.reference); | ||
| } | ||
| return schemas; | ||
| } | ||
|
|
||
| type EntryInputs<TEntries extends Entries> = { | ||
| [TKey in keyof TEntries as `${TKey & (string | number)}`]: InferInput< | ||
| TEntries[TKey] | ||
| >; | ||
| }; | ||
| type EntryOutputs<TEntries extends Entries> = { | ||
| [TKey in keyof TEntries as `${TKey & (string | number)}`]: InferOutput< | ||
| TEntries[TKey] | ||
| >; | ||
| }; | ||
|
|
||
| type EntryIssues<TEntries extends Entries> = { | ||
| [TKey in keyof TEntries as `${TKey & (string | number)}`]: InferIssue< | ||
| TEntries[TKey] | ||
| >; | ||
| }; | ||
|
|
||
| type ReferenceNames<TReference> = | ||
| TReference extends RecursiveMarker<infer TName> ? TName : never; | ||
|
|
||
| type UnmatchedReferences<TEntries extends Entries> = Exclude< | ||
| ReferenceNames< | ||
| RecursiveReferences<EntryInputs<TEntries> | EntryOutputs<TEntries>> | ||
| >, | ||
| keyof EntryInputs<TEntries> | ||
| >; | ||
|
|
||
| interface Error<TReason extends string> { | ||
| readonly reason: TReason; | ||
| readonly impossible: never; | ||
| } | ||
|
|
||
| /** | ||
| * Rejects references to names absent from the entries. | ||
| * | ||
| * @internal | ||
| */ | ||
| export type CheckReferences<TEntries extends Entries> = [ | ||
| UnmatchedReferences<TEntries>, | ||
| ] extends [never] | ||
| ? unknown | ||
| : Error<`Unmatched recursive reference: '${Extract<UnmatchedReferences<TEntries>, string>}'`>; | ||
|
|
||
| /** | ||
| * Resolved mutual recursive schema interface. | ||
| */ | ||
| // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- A named interface defers recursive type resolution. | ||
| export interface ResolvedRecursiveSchema< | ||
| TWrapped extends GenericSchema, | ||
| TInputRoot, | ||
| TOutputRoot, | ||
| TIssue extends BaseIssue<unknown>, | ||
| TIssueRoot = TIssue, | ||
| > extends MutualRecursiveSchema< | ||
| TWrapped, | ||
| ResolveRecursiveInput<InferInput<TWrapped>, TInputRoot>, | ||
| ResolveRecursiveOutput<InferOutput<TWrapped>, TOutputRoot>, | ||
| ResolveRecursiveIssue<TIssue, TIssueRoot> | ||
| > {} | ||
|
|
||
| /** | ||
| * Resolved mutual recursive schema async interface. | ||
| */ | ||
| // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- A named interface defers recursive type resolution. | ||
| export interface ResolvedRecursiveSchemaAsync< | ||
| TWrapped extends Entries[string], | ||
| TInputRoot, | ||
| TOutputRoot, | ||
| TIssue extends BaseIssue<unknown>, | ||
| TIssueRoot = TIssue, | ||
| > extends MutualRecursiveSchemaAsync< | ||
| TWrapped, | ||
| ResolveRecursiveInput<InferInput<TWrapped>, TInputRoot>, | ||
| ResolveRecursiveOutput<InferOutput<TWrapped>, TOutputRoot>, | ||
| ResolveRecursiveIssue<TIssue, TIssueRoot> | ||
| > {} | ||
|
|
||
| /** | ||
| * Resolves each synchronous entry against the complete entries map. | ||
| * | ||
| * @internal | ||
| */ | ||
| export type RecursiveSchemas<TEntries extends Record<string, GenericSchema>> = { | ||
| [TName in keyof TEntries]: ResolvedRecursiveSchema< | ||
| TEntries[TName], | ||
| EntryInputs<TEntries>, | ||
| EntryOutputs<TEntries>, | ||
| InferIssue<TEntries[keyof TEntries]>, | ||
| EntryIssues<TEntries> | ||
| >; | ||
| }; | ||
|
|
||
| /** | ||
| * Resolves each async entry against the complete entries map. | ||
| * | ||
| * @internal | ||
| */ | ||
| export type RecursiveSchemasAsync<TEntries extends Entries> = { | ||
| [TName in keyof TEntries]: ResolvedRecursiveSchemaAsync< | ||
| TEntries[TName], | ||
| EntryInputs<TEntries>, | ||
| EntryOutputs<TEntries>, | ||
| InferIssue<TEntries[keyof TEntries]>, | ||
| EntryIssues<TEntries> | ||
| >; | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| export { | ||
| mutualRecursive, | ||
| type MutualRecursiveSchema, | ||
| } from './mutualRecursive.ts'; | ||
| export type { | ||
| RefFactory, | ||
| RefFactoryAsync, | ||
| ResolvedRecursiveSchema as ResolvedMutualRecursiveSchema, | ||
| ResolvedRecursiveSchemaAsync as ResolvedMutualRecursiveSchemaAsync, | ||
| } from './_mutualRecursive.ts'; | ||
| export { | ||
| mutualRecursiveAsync, | ||
| type MutualRecursiveSchemaAsync, | ||
| } from './mutualRecursiveAsync.ts'; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: open-circle/valibot
Length of output: 2745
Script executed:
Repository: open-circle/valibot
Length of output: 42497
Script executed:
Repository: open-circle/valibot
Length of output: 16022
Web query:
TypeScript 2.9 release notes keyof numeric literal property names numeric keys typeof object numeric propertyResult:
Script executed:
Repository: open-circle/valibot
Length of output: 11090
Script executed:
node -e "const entries = { 0: 'schema' }; console.log(JSON.stringify(Object.keys(entries)), Object.hasOwn(entries, '0'), entries['0']);"Repository: open-circle/valibot
Length of output: 175
Normalize numeric entry keys in reference types.
When an entry is declared as
0,ref('0')matches at runtime but can be rejected as unmatched by TypeScript. If the check is relaxed, the resolver still maps the reference tonever. Normalize numeric keys in both paths.