@scalar/validation 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/schema.d.ts +7 -3
- package/dist/schema.d.ts.map +1 -1
- package/dist/types.d.ts +8 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -2
- package/.turbo/turbo-build.log +0 -4
- package/src/coerce.test.ts +0 -1138
- package/src/coerce.ts +0 -224
- package/src/helpers/is-object.test.ts +0 -168
- package/src/helpers/is-object.ts +0 -31
- package/src/index.ts +0 -37
- package/src/schema.ts +0 -246
- package/src/typegen.test.ts +0 -214
- package/src/typegen.ts +0 -268
- package/src/types.ts +0 -106
- package/src/validate.test.ts +0 -833
- package/src/validate.ts +0 -106
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -10
- package/vite.config.ts +0 -13
package/src/coerce.ts
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { isObject } from './helpers/is-object'
|
|
2
|
-
import type { Schema } from './schema'
|
|
3
|
-
import type { Static } from './types'
|
|
4
|
-
import { validate } from './validate'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* True when this property schema is only used to discriminate union branches
|
|
8
|
-
* (single literal, or a union of literals). No presence bonus when the value
|
|
9
|
-
* does not match — avoids ties like `type: literal('a')` vs `type: union([lit('b'), lit('c')])`.
|
|
10
|
-
*/
|
|
11
|
-
const isDiscriminatorProperty = (schema: Schema): boolean => {
|
|
12
|
-
if (schema.type === 'optional') {
|
|
13
|
-
return isDiscriminatorProperty(schema.schema)
|
|
14
|
-
}
|
|
15
|
-
if (schema.type === 'literal') {
|
|
16
|
-
return true
|
|
17
|
-
}
|
|
18
|
-
if (schema.type === 'union') {
|
|
19
|
-
return schema.schemas.length > 0 && schema.schemas.every(isDiscriminatorProperty)
|
|
20
|
-
}
|
|
21
|
-
return false
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Computes a "score" indicating how well a value matches a schema,
|
|
26
|
-
* used for picking the best branch in union coercion.
|
|
27
|
-
*
|
|
28
|
-
* Higher score means a closer match. Literals and matching object shapes
|
|
29
|
-
* are weighted more heavily. Objects are scored by shape/literals;
|
|
30
|
-
* arrays/records by structural type; primitives by validation; unions try all branches.
|
|
31
|
-
*/
|
|
32
|
-
const scoreUnion = (schema: Schema, value: unknown): number => {
|
|
33
|
-
if (schema.type === 'object') {
|
|
34
|
-
if (!isObject(value)) {
|
|
35
|
-
return 0
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Missing keys contribute 0 (including optional keys — matches prior union heuristics).
|
|
39
|
-
// Discriminator properties (`literal` or `union` of literals): recurse with scoreUnion;
|
|
40
|
-
// matching values get a high weight (×10) so `type: literal('A')` beats unrelated fields
|
|
41
|
-
// on another branch; mismatches score 0 (no "key present" tie-break).
|
|
42
|
-
// Other properties: scoreUnion plus +1 when the value fails validation so `{ a: null }`
|
|
43
|
-
// can still prefer the branch that declares `a`.
|
|
44
|
-
return Object.keys(schema.properties).reduce<number>((acc, key) => {
|
|
45
|
-
if (!(key in value)) {
|
|
46
|
-
return acc
|
|
47
|
-
}
|
|
48
|
-
const propSchema = schema.properties[key]
|
|
49
|
-
const raw = value[key as keyof typeof value]
|
|
50
|
-
const base = scoreUnion(propSchema, raw)
|
|
51
|
-
if (isDiscriminatorProperty(propSchema)) {
|
|
52
|
-
return acc + (base > 0 ? base * 10 : 0)
|
|
53
|
-
}
|
|
54
|
-
return acc + (base > 0 ? base : 1)
|
|
55
|
-
}, 0)
|
|
56
|
-
}
|
|
57
|
-
if (schema.type === 'array') {
|
|
58
|
-
// Score 1 if value is an array, otherwise 0
|
|
59
|
-
return Array.isArray(value) ? 1 : 0
|
|
60
|
-
}
|
|
61
|
-
if (schema.type === 'record') {
|
|
62
|
-
// TODO: implement smarter scoring for records (just a placeholder for now)
|
|
63
|
-
return isObject(value) ? 1 : 0
|
|
64
|
-
}
|
|
65
|
-
if (schema.type === 'optional') {
|
|
66
|
-
return value === undefined ? 1 : scoreUnion(schema.schema, value)
|
|
67
|
-
}
|
|
68
|
-
if (schema.type === 'union') {
|
|
69
|
-
// For a union, use the highest score among all sub-schemas
|
|
70
|
-
return Math.max(...schema.schemas.map((schema) => scoreUnion(schema, value)))
|
|
71
|
-
}
|
|
72
|
-
if (schema.type === 'intersection') {
|
|
73
|
-
if (schema.schemas.length === 0) {
|
|
74
|
-
return 1
|
|
75
|
-
}
|
|
76
|
-
return schema.schemas.reduce((acc, sub) => acc + scoreUnion(sub, value), 0)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (schema.type === 'lazy') {
|
|
80
|
-
// For a lazy schema, evaluate the inner schema and recurse
|
|
81
|
-
return scoreUnion(schema.schema(), value)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (schema.type === 'evaluate') {
|
|
85
|
-
// For an evaluate schema, evaluate the expression and recurse
|
|
86
|
-
return scoreUnion(schema.schema, schema.expression(value))
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// For primitives and any other type, return 1 if valid, otherwise 0
|
|
90
|
-
return validate(schema, value) ? 1 : 0
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Coerces an unknown value toward the static type implied by `schema`. Values that
|
|
95
|
-
* pass {@link validate} for that branch are kept; otherwise primitives default to
|
|
96
|
-
* `0`, `''`, or `false`, and arrays, records, and objects are built recursively.
|
|
97
|
-
* Unions pick the best-matching branch; `evaluate` runs `expression` before the inner schema.
|
|
98
|
-
*
|
|
99
|
-
* @example
|
|
100
|
-
* ```ts
|
|
101
|
-
* import { coerce, number, object, string } from '@scalar/validation'
|
|
102
|
-
*
|
|
103
|
-
* coerce(number(), 42) // 42
|
|
104
|
-
* coerce(number(), 'nope') // 0 — invalid number uses default
|
|
105
|
-
* coerce(object({ id: number(), name: string() }), { id: '1', name: 'Ada' }) // { id: 0, name: 'Ada' }
|
|
106
|
-
* ```
|
|
107
|
-
*
|
|
108
|
-
* The optional `cache` argument tracks visited object–schema pairs to stop infinite recursion
|
|
109
|
-
* on cyclic graphs; callers normally omit it.
|
|
110
|
-
*/
|
|
111
|
-
export const coerce = <S extends Schema>(
|
|
112
|
-
schema: S,
|
|
113
|
-
value: unknown,
|
|
114
|
-
cache: WeakMap<object, Set<Schema>> = new WeakMap(),
|
|
115
|
-
): Static<S> => {
|
|
116
|
-
// Prevent infinite recursion
|
|
117
|
-
if (isObject(value) && cache.get(value)?.has(schema)) {
|
|
118
|
-
return value as Static<S>
|
|
119
|
-
}
|
|
120
|
-
// Track visited schemas to prevent infinite recursion
|
|
121
|
-
if (isObject(value)) {
|
|
122
|
-
const schemas = cache.get(value) || new Set<Schema>()
|
|
123
|
-
schemas.add(schema)
|
|
124
|
-
cache.set(value, schemas)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// If no schema is provided, return the value as is
|
|
128
|
-
if (!schema) {
|
|
129
|
-
return value as Static<S>
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (schema.type === 'any') {
|
|
133
|
-
return value as unknown as Static<S>
|
|
134
|
-
}
|
|
135
|
-
if (schema.type === 'number') {
|
|
136
|
-
if (validate(schema, value)) {
|
|
137
|
-
return value as Static<S>
|
|
138
|
-
}
|
|
139
|
-
return 0 as Static<S>
|
|
140
|
-
}
|
|
141
|
-
if (schema.type === 'string') {
|
|
142
|
-
if (validate(schema, value)) {
|
|
143
|
-
return value as unknown as Static<S>
|
|
144
|
-
}
|
|
145
|
-
return '' as unknown as Static<S>
|
|
146
|
-
}
|
|
147
|
-
if (schema.type === 'boolean') {
|
|
148
|
-
if (validate(schema, value)) {
|
|
149
|
-
return value as unknown as Static<S>
|
|
150
|
-
}
|
|
151
|
-
return false as unknown as Static<S>
|
|
152
|
-
}
|
|
153
|
-
if (schema.type === 'nullable') {
|
|
154
|
-
return null as unknown as Static<S>
|
|
155
|
-
}
|
|
156
|
-
if (schema.type === 'notDefined') {
|
|
157
|
-
return undefined as unknown as Static<S>
|
|
158
|
-
}
|
|
159
|
-
if (schema.type === 'optional') {
|
|
160
|
-
if (value === undefined) {
|
|
161
|
-
return undefined as unknown as Static<S>
|
|
162
|
-
}
|
|
163
|
-
return coerce(schema.schema, value, cache)
|
|
164
|
-
}
|
|
165
|
-
if (schema.type === 'array') {
|
|
166
|
-
if (!Array.isArray(value)) {
|
|
167
|
-
return [] as unknown as Static<S>
|
|
168
|
-
}
|
|
169
|
-
return value.map((item) => coerce(schema.items, item, cache)) as unknown as Static<S>
|
|
170
|
-
}
|
|
171
|
-
if (schema.type === 'record') {
|
|
172
|
-
if (!isObject(value)) {
|
|
173
|
-
return {} as unknown as Static<S>
|
|
174
|
-
}
|
|
175
|
-
return Object.fromEntries(
|
|
176
|
-
Object.entries(value).map(([key, value]) => [key, coerce(schema.value, value, cache)]),
|
|
177
|
-
) as unknown as Static<S>
|
|
178
|
-
}
|
|
179
|
-
if (schema.type === 'object') {
|
|
180
|
-
const keys = Object.keys(schema.properties)
|
|
181
|
-
const target = isObject(value) ? value : null
|
|
182
|
-
const entries: [string, unknown][] = []
|
|
183
|
-
for (const key of keys) {
|
|
184
|
-
const propSchema = schema.properties[key]
|
|
185
|
-
const raw = target?.[key as keyof typeof target]
|
|
186
|
-
if (propSchema.type === 'optional' && raw === undefined) {
|
|
187
|
-
continue
|
|
188
|
-
}
|
|
189
|
-
entries.push([key, coerce(propSchema, raw, cache)])
|
|
190
|
-
}
|
|
191
|
-
return Object.fromEntries(entries) as unknown as Static<S>
|
|
192
|
-
}
|
|
193
|
-
if (schema.type === 'union') {
|
|
194
|
-
const branch = schema.schemas.reduce(
|
|
195
|
-
(acc, schema) => {
|
|
196
|
-
const score = scoreUnion(schema, value)
|
|
197
|
-
return score > acc.score ? { schema, score } : acc
|
|
198
|
-
},
|
|
199
|
-
{ schema: schema.schemas[0], score: 0 },
|
|
200
|
-
)
|
|
201
|
-
// We need some way to pick one of the union values
|
|
202
|
-
return coerce(branch.schema, value, cache)
|
|
203
|
-
}
|
|
204
|
-
if (schema.type === 'intersection') {
|
|
205
|
-
return schema.schemas.reduce<Record<string, unknown>>(
|
|
206
|
-
(acc, subSchema) => Object.assign(acc, coerce(subSchema, value, cache) as Record<string, unknown>),
|
|
207
|
-
{},
|
|
208
|
-
) as unknown as Static<S>
|
|
209
|
-
}
|
|
210
|
-
if (schema.type === 'literal') {
|
|
211
|
-
return schema.value
|
|
212
|
-
}
|
|
213
|
-
if (schema.type === 'lazy') {
|
|
214
|
-
return coerce(schema.schema(), value, cache)
|
|
215
|
-
}
|
|
216
|
-
if (schema.type === 'evaluate') {
|
|
217
|
-
return coerce(schema.schema, schema.expression(value), cache)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// We need to assert here that schema has the type never so we know we handle all cases
|
|
221
|
-
const _exhaustive: never = schema
|
|
222
|
-
console.warn('Unknown schema type:', _exhaustive)
|
|
223
|
-
return value as unknown as Static<S>
|
|
224
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { isObject } from './is-object'
|
|
4
|
-
|
|
5
|
-
describe('isObject', () => {
|
|
6
|
-
it('returns true for plain empty objects', () => {
|
|
7
|
-
expect(isObject({})).toBe(true)
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it('returns true for objects with properties', () => {
|
|
11
|
-
expect(isObject({ a: 1 })).toBe(true)
|
|
12
|
-
expect(isObject({ a: 1, b: 2, c: 3 })).toBe(true)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('returns true for objects with different value types', () => {
|
|
16
|
-
const obj = {
|
|
17
|
-
string: 'hello',
|
|
18
|
-
number: 42,
|
|
19
|
-
boolean: true,
|
|
20
|
-
null: null,
|
|
21
|
-
undefined: undefined,
|
|
22
|
-
array: [],
|
|
23
|
-
nested: { x: 1 },
|
|
24
|
-
}
|
|
25
|
-
expect(isObject(obj)).toBe(true)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('returns true for objects created with Object.create(null)', () => {
|
|
29
|
-
const obj = Object.create(null)
|
|
30
|
-
expect(isObject(obj)).toBe(true)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('returns false for Date objects', () => {
|
|
34
|
-
expect(isObject(new Date())).toBe(false)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('returns false for RegExp objects', () => {
|
|
38
|
-
expect(isObject(/test/)).toBe(false)
|
|
39
|
-
expect(isObject(new RegExp('test'))).toBe(false)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('returns false for Error objects', () => {
|
|
43
|
-
expect(isObject(new Error('test'))).toBe(false)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('returns false for Map objects', () => {
|
|
47
|
-
expect(isObject(new Map())).toBe(false)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('returns false for Set objects', () => {
|
|
51
|
-
expect(isObject(new Set())).toBe(false)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('returns false for WeakMap objects', () => {
|
|
55
|
-
expect(isObject(new WeakMap())).toBe(false)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('returns false for WeakSet objects', () => {
|
|
59
|
-
expect(isObject(new WeakSet())).toBe(false)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('returns false for Promise objects', () => {
|
|
63
|
-
expect(isObject(Promise.resolve())).toBe(false)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('returns false for arrays', () => {
|
|
67
|
-
expect(isObject([])).toBe(false)
|
|
68
|
-
expect(isObject([1, 2, 3])).toBe(false)
|
|
69
|
-
expect(isObject(new Array(10))).toBe(false)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('returns false for null', () => {
|
|
73
|
-
expect(isObject(null)).toBe(false)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('returns false for undefined', () => {
|
|
77
|
-
expect(isObject(undefined)).toBe(false)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('returns false for numbers', () => {
|
|
81
|
-
expect(isObject(0)).toBe(false)
|
|
82
|
-
expect(isObject(123)).toBe(false)
|
|
83
|
-
expect(isObject(-456)).toBe(false)
|
|
84
|
-
expect(isObject(3.14)).toBe(false)
|
|
85
|
-
expect(isObject(Number.NaN)).toBe(false)
|
|
86
|
-
expect(isObject(Number.POSITIVE_INFINITY)).toBe(false)
|
|
87
|
-
expect(isObject(Number.NEGATIVE_INFINITY)).toBe(false)
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('returns false for strings', () => {
|
|
91
|
-
expect(isObject('')).toBe(false)
|
|
92
|
-
expect(isObject('string')).toBe(false)
|
|
93
|
-
expect(isObject('hello world')).toBe(false)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('returns false for booleans', () => {
|
|
97
|
-
expect(isObject(true)).toBe(false)
|
|
98
|
-
expect(isObject(false)).toBe(false)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('returns false for functions', () => {
|
|
102
|
-
expect(
|
|
103
|
-
isObject(() => {
|
|
104
|
-
return
|
|
105
|
-
}),
|
|
106
|
-
).toBe(false)
|
|
107
|
-
expect(
|
|
108
|
-
isObject(() => {
|
|
109
|
-
return
|
|
110
|
-
}),
|
|
111
|
-
).toBe(false)
|
|
112
|
-
expect(
|
|
113
|
-
isObject(async () => {
|
|
114
|
-
return await Promise.resolve()
|
|
115
|
-
}),
|
|
116
|
-
).toBe(false)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('returns false for symbols', () => {
|
|
120
|
-
expect(isObject(Symbol('test'))).toBe(false)
|
|
121
|
-
expect(isObject(Symbol.for('test'))).toBe(false)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('returns false for BigInt values', () => {
|
|
125
|
-
expect(isObject(BigInt(123))).toBe(false)
|
|
126
|
-
expect(isObject(123n)).toBe(false)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('works correctly as a type guard', () => {
|
|
130
|
-
const value: unknown = { a: 1, b: 2 }
|
|
131
|
-
|
|
132
|
-
if (isObject(value)) {
|
|
133
|
-
// TypeScript should narrow the type to Record<string, unknown>
|
|
134
|
-
const keys = Object.keys(value)
|
|
135
|
-
expect(keys).toEqual(['a', 'b'])
|
|
136
|
-
expect(value.a).toBe(1)
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('handles objects with nested structures', () => {
|
|
141
|
-
const obj = {
|
|
142
|
-
nested: {
|
|
143
|
-
deeper: {
|
|
144
|
-
value: 'test',
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
}
|
|
148
|
-
expect(isObject(obj)).toBe(true)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('returns false for class instances', () => {
|
|
152
|
-
class CustomClass {
|
|
153
|
-
prop = 'value'
|
|
154
|
-
}
|
|
155
|
-
const instance = new CustomClass()
|
|
156
|
-
expect(isObject(instance)).toBe(false)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('handles frozen objects', () => {
|
|
160
|
-
const obj = Object.freeze({ a: 1 })
|
|
161
|
-
expect(isObject(obj)).toBe(true)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('handles sealed objects', () => {
|
|
165
|
-
const obj = Object.seal({ a: 1 })
|
|
166
|
-
expect(isObject(obj)).toBe(true)
|
|
167
|
-
})
|
|
168
|
-
})
|
package/src/helpers/is-object.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stolen from |@scalar/helpers/object/is-object.ts|
|
|
3
|
-
* so we don't have to depend on it.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Returns true if the provided value is a record object
|
|
8
|
-
* (i.e. not null, not an array, and has an actual object as the prototype).
|
|
9
|
-
*
|
|
10
|
-
* Differs from the previous isObject in that it returns false for Date,
|
|
11
|
-
* RegExp, Error, Map, Set, WeakMap, WeakSet, Promise, and other non-plain objects.
|
|
12
|
-
*
|
|
13
|
-
* Examples:
|
|
14
|
-
* isObject({}) // true
|
|
15
|
-
* isObject({ a: 1 }) // true
|
|
16
|
-
* isObject([]) // false (Array)
|
|
17
|
-
* isObject(null) // false
|
|
18
|
-
* isObject(123) // false
|
|
19
|
-
* isObject('string') // false
|
|
20
|
-
* isObject(new Error('test')) // false
|
|
21
|
-
* isObject(new Date()) // false
|
|
22
|
-
* isObject(Object.create(null)) // true
|
|
23
|
-
*/
|
|
24
|
-
export const isObject = (value: unknown): value is Record<string | number | symbol, unknown> => {
|
|
25
|
-
if (value === null || typeof value !== 'object') {
|
|
26
|
-
return false
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const proto = Object.getPrototypeOf(value)
|
|
30
|
-
return proto === Object.prototype || proto === null
|
|
31
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export { coerce } from './coerce'
|
|
2
|
-
export {
|
|
3
|
-
type AnySchema,
|
|
4
|
-
type ArraySchema,
|
|
5
|
-
type BooleanSchema,
|
|
6
|
-
type EvaluateSchema,
|
|
7
|
-
type IntersectionSchema,
|
|
8
|
-
type LazySchema,
|
|
9
|
-
type LiteralSchema,
|
|
10
|
-
type NotDefinedSchema,
|
|
11
|
-
type NullableSchema,
|
|
12
|
-
type NumberSchema,
|
|
13
|
-
type ObjectSchema,
|
|
14
|
-
type OptionalSchema,
|
|
15
|
-
type RecordSchema,
|
|
16
|
-
type Schema,
|
|
17
|
-
type StringSchema,
|
|
18
|
-
type UnionSchema,
|
|
19
|
-
any,
|
|
20
|
-
array,
|
|
21
|
-
boolean,
|
|
22
|
-
evaluate,
|
|
23
|
-
intersection,
|
|
24
|
-
lazy,
|
|
25
|
-
literal,
|
|
26
|
-
notDefined,
|
|
27
|
-
nullable,
|
|
28
|
-
number,
|
|
29
|
-
object,
|
|
30
|
-
optional,
|
|
31
|
-
record,
|
|
32
|
-
string,
|
|
33
|
-
union,
|
|
34
|
-
} from './schema'
|
|
35
|
-
export { type GenerateTypesOptions, generateTypes } from './typegen'
|
|
36
|
-
export type { Static } from './types'
|
|
37
|
-
export { validate } from './validate'
|
package/src/schema.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional metadata for type generation and documentation.
|
|
3
|
-
* - typeName: Used as the exported TypeScript type name if valid.
|
|
4
|
-
* - typeComment: Adds a JSDoc comment to the generated type declaration.
|
|
5
|
-
*/
|
|
6
|
-
type Documentation = Partial<{
|
|
7
|
-
/** Adds a JSDoc comment to the generated type declaration. */
|
|
8
|
-
typeComment: string
|
|
9
|
-
/** Used as the exported TypeScript type name if valid. */
|
|
10
|
-
typeName: string
|
|
11
|
-
}>
|
|
12
|
-
|
|
13
|
-
/** Schema for finite numeric values. {@link Static} resolves to `number`. */
|
|
14
|
-
export type NumberSchema = {
|
|
15
|
-
type: 'number'
|
|
16
|
-
} & Documentation
|
|
17
|
-
|
|
18
|
-
/** Schema for string values. {@link Static} resolves to `string`. */
|
|
19
|
-
export type StringSchema = {
|
|
20
|
-
type: 'string'
|
|
21
|
-
} & Documentation
|
|
22
|
-
|
|
23
|
-
/** Schema for boolean values. {@link Static} resolves to `boolean`. */
|
|
24
|
-
export type BooleanSchema = {
|
|
25
|
-
type: 'boolean'
|
|
26
|
-
} & Documentation
|
|
27
|
-
|
|
28
|
-
/** Schema for `null`. {@link Static} resolves to `null`. */
|
|
29
|
-
export type NullableSchema = {
|
|
30
|
-
type: 'nullable'
|
|
31
|
-
} & Documentation
|
|
32
|
-
|
|
33
|
-
/** Schema for a missing or omitted value. {@link Static} resolves to `undefined`. */
|
|
34
|
-
export type NotDefinedSchema = {
|
|
35
|
-
type: 'notDefined'
|
|
36
|
-
} & Documentation
|
|
37
|
-
|
|
38
|
-
/** Schema that accepts any value without narrowing. {@link Static} resolves to `any`. */
|
|
39
|
-
export type AnySchema = {
|
|
40
|
-
type: 'any'
|
|
41
|
-
} & Documentation
|
|
42
|
-
|
|
43
|
-
/** Schema for homogeneous lists. {@link Static} resolves to an array of the item static type. */
|
|
44
|
-
export type ArraySchema<Item extends Schema> = {
|
|
45
|
-
type: 'array'
|
|
46
|
-
items: Item
|
|
47
|
-
} & Documentation
|
|
48
|
-
|
|
49
|
-
/** Schema for key-value maps with uniform value shape. Keys are constrained to string or number schemas. */
|
|
50
|
-
export type RecordSchema<Key extends StringSchema | NumberSchema | AnySchema, Value extends Schema> = {
|
|
51
|
-
type: 'record'
|
|
52
|
-
key: Key
|
|
53
|
-
value: Value
|
|
54
|
-
} & Documentation
|
|
55
|
-
|
|
56
|
-
/** Schema for objects with a fixed set of named properties, each with its own schema. */
|
|
57
|
-
export type ObjectSchema<Properties extends Record<string, Schema>> = {
|
|
58
|
-
type: 'object'
|
|
59
|
-
properties: Properties
|
|
60
|
-
} & Documentation
|
|
61
|
-
|
|
62
|
-
/** Schema that matches if any member schema matches (discriminated union when literals or object tags differ). */
|
|
63
|
-
export type UnionSchema<Schemas extends Schema[]> = {
|
|
64
|
-
type: 'union'
|
|
65
|
-
schemas: Schemas
|
|
66
|
-
} & Documentation
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Schema that accepts `undefined` or a value matching the inner schema.
|
|
70
|
-
* In {@link Static} and type generation, object properties use `key?:` instead of `T | undefined`.
|
|
71
|
-
*/
|
|
72
|
-
export type OptionalSchema<S extends Schema> = {
|
|
73
|
-
type: 'optional'
|
|
74
|
-
schema: S
|
|
75
|
-
} & Documentation
|
|
76
|
-
|
|
77
|
-
export type IntersectionSchema<Schemas extends readonly ObjectSchema<any>[]> = {
|
|
78
|
-
type: 'intersection'
|
|
79
|
-
schemas: Schemas
|
|
80
|
-
} & Documentation
|
|
81
|
-
|
|
82
|
-
/** Schema for a single exact constant (string, number, boolean, or bigint). {@link Static} is that literal type. */
|
|
83
|
-
export type LiteralSchema<T extends string | number | boolean | bigint> = {
|
|
84
|
-
type: 'literal'
|
|
85
|
-
value: T
|
|
86
|
-
} & Documentation
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Schema for self-referential or recursive types (such as trees or linked lists).
|
|
90
|
-
* The `schema` property is a factory function returning a schema instance, allowing
|
|
91
|
-
* references to itself without causing circular definition errors at type-level.
|
|
92
|
-
*/
|
|
93
|
-
export type LazySchema<S extends () => Schema> = {
|
|
94
|
-
type: 'lazy'
|
|
95
|
-
schema: S
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Schema that runs a coercion or transform (`expression`) on the input, then validates with the inner schema.
|
|
100
|
-
* Use when parsing needs a preprocessing step before the usual rules apply.
|
|
101
|
-
*/
|
|
102
|
-
export type EvaluateSchema<S extends Schema> = {
|
|
103
|
-
type: 'evaluate'
|
|
104
|
-
expression: (value: unknown) => unknown
|
|
105
|
-
schema: S
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export type Schema =
|
|
109
|
-
| NumberSchema
|
|
110
|
-
| StringSchema
|
|
111
|
-
| BooleanSchema
|
|
112
|
-
| NullableSchema
|
|
113
|
-
| NotDefinedSchema
|
|
114
|
-
| AnySchema
|
|
115
|
-
| ArraySchema<any>
|
|
116
|
-
| RecordSchema<any, any>
|
|
117
|
-
| ObjectSchema<Record<string, any>>
|
|
118
|
-
| UnionSchema<any[]>
|
|
119
|
-
| OptionalSchema<any>
|
|
120
|
-
| IntersectionSchema<readonly ObjectSchema<any>[]>
|
|
121
|
-
| LiteralSchema<any>
|
|
122
|
-
| LazySchema<any>
|
|
123
|
-
| EvaluateSchema<any>
|
|
124
|
-
|
|
125
|
-
const number = (options?: Documentation): NumberSchema => ({
|
|
126
|
-
type: 'number',
|
|
127
|
-
typeName: options?.typeName,
|
|
128
|
-
typeComment: options?.typeComment,
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
const string = (options?: Documentation): StringSchema => ({
|
|
132
|
-
type: 'string',
|
|
133
|
-
typeName: options?.typeName,
|
|
134
|
-
typeComment: options?.typeComment,
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
const boolean = (options?: Documentation): BooleanSchema => ({
|
|
138
|
-
type: 'boolean',
|
|
139
|
-
typeName: options?.typeName,
|
|
140
|
-
typeComment: options?.typeComment,
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
const nullable = (options?: Documentation): NullableSchema => ({
|
|
144
|
-
type: 'nullable',
|
|
145
|
-
typeName: options?.typeName,
|
|
146
|
-
typeComment: options?.typeComment,
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
const notDefined = (options?: Documentation): NotDefinedSchema => ({
|
|
150
|
-
type: 'notDefined',
|
|
151
|
-
typeName: options?.typeName,
|
|
152
|
-
typeComment: options?.typeComment,
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
const any = (options?: Documentation): AnySchema => ({
|
|
156
|
-
type: 'any',
|
|
157
|
-
typeName: options?.typeName,
|
|
158
|
-
typeComment: options?.typeComment,
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
const array = <Item extends Schema>(items: Item, options?: Documentation): ArraySchema<Item> => ({
|
|
162
|
-
type: 'array',
|
|
163
|
-
items,
|
|
164
|
-
typeName: options?.typeName,
|
|
165
|
-
typeComment: options?.typeComment,
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
const record = <Key extends StringSchema | AnySchema, Value extends Schema>(
|
|
169
|
-
key: Key,
|
|
170
|
-
value: Value,
|
|
171
|
-
options?: Documentation,
|
|
172
|
-
): RecordSchema<Key, Value> => ({
|
|
173
|
-
type: 'record',
|
|
174
|
-
key,
|
|
175
|
-
value,
|
|
176
|
-
typeName: options?.typeName,
|
|
177
|
-
typeComment: options?.typeComment,
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
const object = <Properties extends Record<string, Schema>>(
|
|
181
|
-
properties: Properties,
|
|
182
|
-
options?: Documentation,
|
|
183
|
-
): ObjectSchema<Properties> => ({
|
|
184
|
-
type: 'object',
|
|
185
|
-
properties,
|
|
186
|
-
typeName: options?.typeName,
|
|
187
|
-
typeComment: options?.typeComment,
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
const union = <Schemas extends Schema[]>(schemas: Schemas, options?: Documentation): UnionSchema<Schemas> => ({
|
|
191
|
-
type: 'union',
|
|
192
|
-
schemas,
|
|
193
|
-
typeName: options?.typeName,
|
|
194
|
-
typeComment: options?.typeComment,
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
const intersection = <Schemas extends readonly ObjectSchema<any>[]>(
|
|
198
|
-
schemas: Schemas,
|
|
199
|
-
options?: Documentation,
|
|
200
|
-
): IntersectionSchema<Schemas> => ({
|
|
201
|
-
type: 'intersection',
|
|
202
|
-
schemas,
|
|
203
|
-
typeName: options?.typeName,
|
|
204
|
-
typeComment: options?.typeComment,
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
const optional = <S extends Schema>(schema: S, options?: Documentation): OptionalSchema<S> => ({
|
|
208
|
-
type: 'optional',
|
|
209
|
-
schema,
|
|
210
|
-
typeName: options?.typeName,
|
|
211
|
-
typeComment: options?.typeComment,
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
const literal = <Value extends string | number | boolean | bigint>(value: Value): LiteralSchema<Value> => ({
|
|
215
|
-
type: 'literal',
|
|
216
|
-
value,
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
const lazy = <S extends () => Schema>(schema: S): LazySchema<S> => ({
|
|
220
|
-
type: 'lazy',
|
|
221
|
-
schema,
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
const evaluate = <S extends Schema>(expression: (value: unknown) => unknown, schema: S): EvaluateSchema<S> => ({
|
|
225
|
-
type: 'evaluate',
|
|
226
|
-
expression,
|
|
227
|
-
schema,
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
export {
|
|
231
|
-
number,
|
|
232
|
-
string,
|
|
233
|
-
boolean,
|
|
234
|
-
nullable,
|
|
235
|
-
notDefined,
|
|
236
|
-
any,
|
|
237
|
-
array,
|
|
238
|
-
record,
|
|
239
|
-
object,
|
|
240
|
-
union,
|
|
241
|
-
intersection,
|
|
242
|
-
optional,
|
|
243
|
-
literal,
|
|
244
|
-
lazy,
|
|
245
|
-
evaluate,
|
|
246
|
-
}
|