@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/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
- ? { [K in keyof Properties]: _Static<Properties[K], Prev<Depth>> }
43
- : T extends UnionSchema<infer Schemas>
44
- ? _Static<Schemas[number], Prev<Depth>>
45
- : T extends EvaluateSchema<infer S>
46
- ? _Static<S, Prev<Depth>>
47
- : T extends LazySchema<infer S>
48
- ? _Static<ReturnType<S>, Prev<Depth>>
49
- : never
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
@@ -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': Object with fixed property keys, each validated recursively.
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
- * - 'recursive': Schema referring to itself for nested validation (e.g. trees).
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
  }