@scalar/validation 0.1.0 → 0.3.0
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/coerce.d.ts.map +1 -1
- package/dist/coerce.js +60 -9
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/schema.d.ts +52 -24
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +43 -12
- package/dist/typegen.d.ts +25 -0
- package/dist/typegen.d.ts.map +1 -0
- package/dist/typegen.js +221 -0
- package/dist/types.d.ts +23 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +17 -2
- package/package.json +1 -1
- package/src/coerce.test.ts +87 -0
- package/src/coerce.ts +64 -11
- package/src/index.ts +17 -0
- package/src/schema.ts +93 -21
- package/src/typegen.test.ts +214 -0
- package/src/typegen.ts +268 -0
- package/src/types.ts +46 -8
- package/src/validate.test.ts +53 -0
- package/src/validate.ts +17 -2
package/src/types.ts
CHANGED
|
@@ -3,13 +3,16 @@ import type {
|
|
|
3
3
|
ArraySchema,
|
|
4
4
|
BooleanSchema,
|
|
5
5
|
EvaluateSchema,
|
|
6
|
+
IntersectionSchema,
|
|
6
7
|
LazySchema,
|
|
7
8
|
LiteralSchema,
|
|
8
9
|
NotDefinedSchema,
|
|
9
10
|
NullableSchema,
|
|
10
11
|
NumberSchema,
|
|
11
12
|
ObjectSchema,
|
|
13
|
+
OptionalSchema,
|
|
12
14
|
RecordSchema,
|
|
15
|
+
Schema,
|
|
13
16
|
StringSchema,
|
|
14
17
|
UnionSchema,
|
|
15
18
|
} from './schema'
|
|
@@ -17,6 +20,37 @@ import type {
|
|
|
17
20
|
// Export Static type with depth limit to prevent infinite recursion
|
|
18
21
|
export type Static<T> = _Static<T, 10>
|
|
19
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Folds intersection member schemas into an intersection of their static types.
|
|
25
|
+
* Uses `Schema` for tuple positions (not a narrower alias) so `infer First extends …` does not
|
|
26
|
+
* reject valid tuple elements and collapse to `{}`.
|
|
27
|
+
*/
|
|
28
|
+
type IntersectObjectStatics<Schemas extends readonly Schema[], Depth extends number> = Schemas extends readonly []
|
|
29
|
+
? {}
|
|
30
|
+
: Schemas extends readonly [infer First extends Schema, ...infer Rest extends readonly Schema[]]
|
|
31
|
+
? _Static<First, Depth> & IntersectObjectStatics<Rest, Depth>
|
|
32
|
+
: {}
|
|
33
|
+
|
|
34
|
+
type OptionalPropertyKeys<P> = {
|
|
35
|
+
[K in keyof P]: P[K] extends OptionalSchema<any> ? K : never
|
|
36
|
+
}[keyof P]
|
|
37
|
+
|
|
38
|
+
type RequiredPropertyKeys<P> = {
|
|
39
|
+
[K in keyof P]: P[K] extends OptionalSchema<any> ? never : K
|
|
40
|
+
}[keyof P]
|
|
41
|
+
|
|
42
|
+
type OptionalSchemaInner<S> = S extends OptionalSchema<infer Inner> ? Inner : never
|
|
43
|
+
|
|
44
|
+
type ObjectStatics<Properties, Depth extends number> = [keyof Properties] extends [never]
|
|
45
|
+
? {}
|
|
46
|
+
: OptionalPropertyKeys<Properties> extends never
|
|
47
|
+
? { [K in keyof Properties]: _Static<Properties[K], Prev<Depth>> }
|
|
48
|
+
: RequiredPropertyKeys<Properties> extends never
|
|
49
|
+
? { [K in OptionalPropertyKeys<Properties>]?: _Static<OptionalSchemaInner<Properties[K]>, Prev<Depth>> }
|
|
50
|
+
: { [K in RequiredPropertyKeys<Properties>]: _Static<Properties[K], Prev<Depth>> } & {
|
|
51
|
+
[K in OptionalPropertyKeys<Properties>]?: _Static<OptionalSchemaInner<Properties[K]>, Prev<Depth>>
|
|
52
|
+
}
|
|
53
|
+
|
|
20
54
|
// Internal type with depth counter
|
|
21
55
|
type _Static<T, Depth extends number = 10> = Depth extends 0
|
|
22
56
|
? any
|
|
@@ -39,14 +73,18 @@ type _Static<T, Depth extends number = 10> = Depth extends 0
|
|
|
39
73
|
: T extends RecordSchema<infer Key, infer Value>
|
|
40
74
|
? Record<_Static<Key, Prev<Depth>> & PropertyKey, _Static<Value, Prev<Depth>>>
|
|
41
75
|
: T extends ObjectSchema<infer Properties>
|
|
42
|
-
?
|
|
43
|
-
: T extends
|
|
44
|
-
? _Static<
|
|
45
|
-
: T extends
|
|
46
|
-
?
|
|
47
|
-
: T extends
|
|
48
|
-
? _Static<
|
|
49
|
-
:
|
|
76
|
+
? ObjectStatics<Properties, Depth>
|
|
77
|
+
: T extends OptionalSchema<infer S>
|
|
78
|
+
? _Static<S, Prev<Depth>> | undefined
|
|
79
|
+
: T extends IntersectionSchema<infer Schemas>
|
|
80
|
+
? IntersectObjectStatics<Schemas, Prev<Depth>>
|
|
81
|
+
: T extends UnionSchema<infer Schemas>
|
|
82
|
+
? _Static<Schemas[number], Prev<Depth>>
|
|
83
|
+
: T extends EvaluateSchema<infer S>
|
|
84
|
+
? _Static<S, Prev<Depth>>
|
|
85
|
+
: T extends LazySchema<infer S>
|
|
86
|
+
? _Static<ReturnType<S>, Prev<Depth>>
|
|
87
|
+
: never
|
|
50
88
|
|
|
51
89
|
// Helper type to decrement depth counter
|
|
52
90
|
type Prev<T extends number> = T extends 10
|
package/src/validate.test.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
array,
|
|
6
6
|
boolean,
|
|
7
7
|
evaluate,
|
|
8
|
+
intersection,
|
|
8
9
|
lazy,
|
|
9
10
|
literal,
|
|
10
11
|
notDefined,
|
|
@@ -652,6 +653,58 @@ describe('union', () => {
|
|
|
652
653
|
})
|
|
653
654
|
})
|
|
654
655
|
|
|
656
|
+
describe('intersection', () => {
|
|
657
|
+
it('passes when the value satisfies every member object schema', () => {
|
|
658
|
+
const T = intersection([object({ a: number(), b: number() }), object({ c: string(), d: string() })])
|
|
659
|
+
expect(validate(T, { a: 1, b: 2, c: 'x', d: 'y' })).toBe(true)
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
it('fails when one member object schema fails', () => {
|
|
663
|
+
const T = intersection([object({ a: number(), b: number() }), object({ c: string(), d: string() })])
|
|
664
|
+
expect(validate(T, { a: 1, b: 2, c: 'x', d: 3 })).toBe(false)
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
it('fails when the first member fails even if later members would pass', () => {
|
|
668
|
+
const T = intersection([object({ x: literal(1) }), object({ y: string() })])
|
|
669
|
+
expect(validate(T, { x: 2, y: 'ok' })).toBe(false)
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
it('requires overlapping keys to satisfy every arm that declares them', () => {
|
|
673
|
+
const T = intersection([object({ id: number() }), object({ id: string() })])
|
|
674
|
+
expect(validate(T, { id: 1 })).toBe(false)
|
|
675
|
+
expect(validate(T, { id: '1' })).toBe(false)
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
it('rejects non-plain objects before member checks', () => {
|
|
679
|
+
const T = intersection([object({ x: number() }), object({ y: number() })])
|
|
680
|
+
expect(validate(T, null)).toBe(false)
|
|
681
|
+
expect(validate(T, undefined)).toBe(false)
|
|
682
|
+
expect(validate(T, 0)).toBe(false)
|
|
683
|
+
expect(validate(T, [])).toBe(false)
|
|
684
|
+
expect(validate(T, new Date())).toBe(false)
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
it('treats an empty intersection as vacuously valid', () => {
|
|
688
|
+
const T = intersection([])
|
|
689
|
+
expect(validate(T, null)).toBe(true)
|
|
690
|
+
expect(validate(T, { a: 1 })).toBe(true)
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
it('matches a single member the same as that object schema alone', () => {
|
|
694
|
+
const O = object({ x: number() })
|
|
695
|
+
const T = intersection([O])
|
|
696
|
+
expect(validate(T, { x: 1 })).toBe(true)
|
|
697
|
+
expect(validate(T, {})).toBe(false)
|
|
698
|
+
expect(validate(O, { x: 1 })).toBe(validate(T, { x: 1 }))
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
it('validates members that use lazy schemas', () => {
|
|
702
|
+
const T = intersection([object({ a: number() }), object({ nested: lazy(() => object({ z: string() })) })])
|
|
703
|
+
expect(validate(T, { a: 1, nested: { z: 'ok' } })).toBe(true)
|
|
704
|
+
expect(validate(T, { a: 1, nested: { z: 1 } })).toBe(false)
|
|
705
|
+
})
|
|
706
|
+
})
|
|
707
|
+
|
|
655
708
|
describe('notDefined', () => {
|
|
656
709
|
const T = notDefined()
|
|
657
710
|
it('Should fail string', () => {
|
package/src/validate.ts
CHANGED
|
@@ -14,10 +14,12 @@ import type { Schema } from './schema'
|
|
|
14
14
|
* - 'notDefined': Only `undefined` is valid.
|
|
15
15
|
* - 'array': Array with all items validated recursively.
|
|
16
16
|
* - 'record': Object with string/number keys and values, checked recursively.
|
|
17
|
-
* - 'object':
|
|
17
|
+
* - 'object': Plain object with fixed property keys, each validated recursively.
|
|
18
18
|
* - 'union': Accepts if value matches any of the listed schemas.
|
|
19
|
+
* - 'optional': Accepts `undefined` or a value matching the inner schema.
|
|
20
|
+
* - 'intersection': Accepts if value matches every member schema (members are object schemas; value must be a plain object).
|
|
19
21
|
* - 'literal': Exact match with a literal value.
|
|
20
|
-
* - '
|
|
22
|
+
* - 'lazy': Delegates to the schema returned by the factory.
|
|
21
23
|
* - 'evaluate': Transforms value then validates against an inner schema.
|
|
22
24
|
*
|
|
23
25
|
* @example
|
|
@@ -72,9 +74,22 @@ export const validate = (schema: Schema | undefined, value: unknown): boolean =>
|
|
|
72
74
|
const schemaKeys = Object.keys(schema.properties)
|
|
73
75
|
return schemaKeys.every((key) => validate(schema.properties[key], value[key]))
|
|
74
76
|
}
|
|
77
|
+
if (schema.type === 'optional') {
|
|
78
|
+
return value === undefined || validate(schema.schema, value)
|
|
79
|
+
}
|
|
75
80
|
if (schema.type === 'union') {
|
|
76
81
|
return schema.schemas.some((schema) => validate(schema, value))
|
|
77
82
|
}
|
|
83
|
+
if (schema.type === 'intersection') {
|
|
84
|
+
if (schema.schemas.length === 0) {
|
|
85
|
+
// Vacuous: no constraints (matches `Array.prototype.every` on an empty list).
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
if (!isObject(value)) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
return schema.schemas.every((subSchema) => validate(subSchema, value))
|
|
92
|
+
}
|
|
78
93
|
if (schema.type === 'literal') {
|
|
79
94
|
return value === schema.value
|
|
80
95
|
}
|