@atproto/lex-schema 0.0.2 → 0.0.3

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.
Files changed (289) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/core/$type.d.ts +6 -3
  3. package/dist/core/$type.d.ts.map +1 -1
  4. package/dist/core/$type.js +1 -0
  5. package/dist/core/$type.js.map +1 -1
  6. package/dist/core/record-key.d.ts +3 -3
  7. package/dist/core/record-key.d.ts.map +1 -1
  8. package/dist/core/record-key.js +12 -6
  9. package/dist/core/record-key.js.map +1 -1
  10. package/dist/core/result.d.ts.map +1 -1
  11. package/dist/core/result.js +6 -0
  12. package/dist/core/result.js.map +1 -1
  13. package/dist/core/string-format.d.ts +30 -27
  14. package/dist/core/string-format.d.ts.map +1 -1
  15. package/dist/core/string-format.js +56 -42
  16. package/dist/core/string-format.js.map +1 -1
  17. package/dist/core/types.d.ts +9 -1
  18. package/dist/core/types.d.ts.map +1 -1
  19. package/dist/core/types.js.map +1 -1
  20. package/dist/external.d.ts +31 -28
  21. package/dist/external.d.ts.map +1 -1
  22. package/dist/external.js +33 -17
  23. package/dist/external.js.map +1 -1
  24. package/dist/schema/_parameters.d.ts +2 -2
  25. package/dist/schema/_parameters.d.ts.map +1 -1
  26. package/dist/schema/array.d.ts +5 -6
  27. package/dist/schema/array.d.ts.map +1 -1
  28. package/dist/schema/array.js +5 -6
  29. package/dist/schema/array.js.map +1 -1
  30. package/dist/schema/blob.d.ts +2 -3
  31. package/dist/schema/blob.d.ts.map +1 -1
  32. package/dist/schema/blob.js +1 -2
  33. package/dist/schema/blob.js.map +1 -1
  34. package/dist/schema/boolean.d.ts +4 -5
  35. package/dist/schema/boolean.d.ts.map +1 -1
  36. package/dist/schema/boolean.js +2 -3
  37. package/dist/schema/boolean.js.map +1 -1
  38. package/dist/schema/bytes.d.ts +3 -4
  39. package/dist/schema/bytes.d.ts.map +1 -1
  40. package/dist/schema/bytes.js +2 -3
  41. package/dist/schema/bytes.js.map +1 -1
  42. package/dist/schema/cid.d.ts +13 -6
  43. package/dist/schema/cid.d.ts.map +1 -1
  44. package/dist/schema/cid.js +2 -4
  45. package/dist/schema/cid.js.map +1 -1
  46. package/dist/schema/custom.d.ts +3 -4
  47. package/dist/schema/custom.d.ts.map +1 -1
  48. package/dist/schema/custom.js +4 -3
  49. package/dist/schema/custom.js.map +1 -1
  50. package/dist/schema/dict.d.ts +3 -3
  51. package/dist/schema/dict.d.ts.map +1 -1
  52. package/dist/schema/dict.js +1 -1
  53. package/dist/schema/dict.js.map +1 -1
  54. package/dist/schema/discriminated-union.d.ts +15 -24
  55. package/dist/schema/discriminated-union.d.ts.map +1 -1
  56. package/dist/schema/discriminated-union.js +40 -64
  57. package/dist/schema/discriminated-union.js.map +1 -1
  58. package/dist/schema/enum.d.ts +8 -4
  59. package/dist/schema/enum.d.ts.map +1 -1
  60. package/dist/schema/enum.js +5 -3
  61. package/dist/schema/enum.js.map +1 -1
  62. package/dist/schema/integer.d.ts +3 -4
  63. package/dist/schema/integer.d.ts.map +1 -1
  64. package/dist/schema/integer.js +3 -4
  65. package/dist/schema/integer.js.map +1 -1
  66. package/dist/schema/intersection.d.ts +22 -14
  67. package/dist/schema/intersection.d.ts.map +1 -1
  68. package/dist/schema/intersection.js +12 -22
  69. package/dist/schema/intersection.js.map +1 -1
  70. package/dist/schema/literal.d.ts +7 -3
  71. package/dist/schema/literal.d.ts.map +1 -1
  72. package/dist/schema/literal.js +5 -3
  73. package/dist/schema/literal.js.map +1 -1
  74. package/dist/schema/never.d.ts +2 -2
  75. package/dist/schema/never.d.ts.map +1 -1
  76. package/dist/schema/never.js +1 -1
  77. package/dist/schema/never.js.map +1 -1
  78. package/dist/schema/null.d.ts +2 -3
  79. package/dist/schema/null.d.ts.map +1 -1
  80. package/dist/schema/null.js +1 -2
  81. package/dist/schema/null.js.map +1 -1
  82. package/dist/schema/nullable.d.ts +7 -0
  83. package/dist/schema/nullable.d.ts.map +1 -0
  84. package/dist/schema/nullable.js +19 -0
  85. package/dist/schema/nullable.js.map +1 -0
  86. package/dist/schema/object.d.ts +10 -44
  87. package/dist/schema/object.d.ts.map +1 -1
  88. package/dist/schema/object.js +10 -46
  89. package/dist/schema/object.js.map +1 -1
  90. package/dist/schema/optional.d.ts +7 -0
  91. package/dist/schema/optional.d.ts.map +1 -0
  92. package/dist/schema/optional.js +25 -0
  93. package/dist/schema/optional.js.map +1 -0
  94. package/dist/schema/params.d.ts +14 -19
  95. package/dist/schema/params.d.ts.map +1 -1
  96. package/dist/schema/params.js +10 -24
  97. package/dist/schema/params.js.map +1 -1
  98. package/dist/schema/payload.d.ts +4 -4
  99. package/dist/schema/payload.d.ts.map +1 -1
  100. package/dist/schema/payload.js.map +1 -1
  101. package/dist/schema/permission-set.d.ts +6 -6
  102. package/dist/schema/permission-set.d.ts.map +1 -1
  103. package/dist/schema/permission-set.js +1 -2
  104. package/dist/schema/permission-set.js.map +1 -1
  105. package/dist/schema/permission.d.ts +0 -1
  106. package/dist/schema/permission.d.ts.map +1 -1
  107. package/dist/schema/permission.js +0 -1
  108. package/dist/schema/permission.js.map +1 -1
  109. package/dist/schema/procedure.d.ts +8 -9
  110. package/dist/schema/procedure.d.ts.map +1 -1
  111. package/dist/schema/procedure.js +0 -1
  112. package/dist/schema/procedure.js.map +1 -1
  113. package/dist/schema/query.d.ts +7 -8
  114. package/dist/schema/query.d.ts.map +1 -1
  115. package/dist/schema/query.js +0 -1
  116. package/dist/schema/query.js.map +1 -1
  117. package/dist/schema/record.d.ts +34 -28
  118. package/dist/schema/record.d.ts.map +1 -1
  119. package/dist/schema/record.js +1 -2
  120. package/dist/schema/record.js.map +1 -1
  121. package/dist/schema/ref.d.ts +2 -3
  122. package/dist/schema/ref.d.ts.map +1 -1
  123. package/dist/schema/ref.js +1 -2
  124. package/dist/schema/ref.js.map +1 -1
  125. package/dist/schema/refine.d.ts +18 -0
  126. package/dist/schema/refine.d.ts.map +1 -0
  127. package/dist/schema/refine.js +33 -0
  128. package/dist/schema/refine.js.map +1 -0
  129. package/dist/schema/regexp.d.ts +7 -0
  130. package/dist/schema/regexp.d.ts.map +1 -0
  131. package/dist/schema/regexp.js +22 -0
  132. package/dist/schema/regexp.js.map +1 -0
  133. package/dist/schema/string.d.ts +4 -8
  134. package/dist/schema/string.d.ts.map +1 -1
  135. package/dist/schema/string.js +6 -3
  136. package/dist/schema/string.js.map +1 -1
  137. package/dist/schema/subscription.d.ts +7 -6
  138. package/dist/schema/subscription.d.ts.map +1 -1
  139. package/dist/schema/subscription.js.map +1 -1
  140. package/dist/schema/token.d.ts +2 -3
  141. package/dist/schema/token.d.ts.map +1 -1
  142. package/dist/schema/token.js +1 -2
  143. package/dist/schema/token.js.map +1 -1
  144. package/dist/schema/typed-object.d.ts +29 -27
  145. package/dist/schema/typed-object.d.ts.map +1 -1
  146. package/dist/schema/typed-object.js +1 -2
  147. package/dist/schema/typed-object.js.map +1 -1
  148. package/dist/schema/typed-ref.d.ts +2 -2
  149. package/dist/schema/typed-ref.d.ts.map +1 -1
  150. package/dist/schema/typed-ref.js +1 -1
  151. package/dist/schema/typed-ref.js.map +1 -1
  152. package/dist/schema/typed-union.d.ts +3 -4
  153. package/dist/schema/typed-union.d.ts.map +1 -1
  154. package/dist/schema/typed-union.js +3 -10
  155. package/dist/schema/typed-union.js.map +1 -1
  156. package/dist/schema/union.d.ts +2 -2
  157. package/dist/schema/union.d.ts.map +1 -1
  158. package/dist/schema/union.js +1 -1
  159. package/dist/schema/union.js.map +1 -1
  160. package/dist/schema/unknown-object.d.ts +2 -3
  161. package/dist/schema/unknown-object.d.ts.map +1 -1
  162. package/dist/schema/unknown-object.js +1 -2
  163. package/dist/schema/unknown-object.js.map +1 -1
  164. package/dist/schema/unknown.d.ts +2 -2
  165. package/dist/schema/unknown.d.ts.map +1 -1
  166. package/dist/schema/unknown.js +1 -1
  167. package/dist/schema/unknown.js.map +1 -1
  168. package/dist/schema.d.ts +4 -0
  169. package/dist/schema.d.ts.map +1 -1
  170. package/dist/schema.js +6 -1
  171. package/dist/schema.js.map +1 -1
  172. package/dist/util/array-agg.d.ts.map +1 -1
  173. package/dist/util/array-agg.js +1 -0
  174. package/dist/util/array-agg.js.map +1 -1
  175. package/dist/util/lazy-property.d.ts +2 -0
  176. package/dist/util/lazy-property.d.ts.map +1 -0
  177. package/dist/util/lazy-property.js +14 -0
  178. package/dist/util/lazy-property.js.map +1 -0
  179. package/dist/validation/schema.d.ts +24 -0
  180. package/dist/validation/schema.d.ts.map +1 -0
  181. package/dist/validation/schema.js +57 -0
  182. package/dist/validation/schema.js.map +1 -0
  183. package/dist/validation/validation-error.d.ts +3 -3
  184. package/dist/validation/validation-error.d.ts.map +1 -1
  185. package/dist/validation/validation-error.js +32 -4
  186. package/dist/validation/validation-error.js.map +1 -1
  187. package/dist/validation/validation-issue.d.ts +32 -24
  188. package/dist/validation/validation-issue.d.ts.map +1 -1
  189. package/dist/validation/validation-issue.js +136 -92
  190. package/dist/validation/validation-issue.js.map +1 -1
  191. package/dist/validation/validator.d.ts +20 -50
  192. package/dist/validation/validator.d.ts.map +1 -1
  193. package/dist/validation/validator.js +40 -134
  194. package/dist/validation/validator.js.map +1 -1
  195. package/dist/validation.d.ts +1 -0
  196. package/dist/validation.d.ts.map +1 -1
  197. package/dist/validation.js +1 -0
  198. package/dist/validation.js.map +1 -1
  199. package/package.json +8 -4
  200. package/src/core/$type.ts +7 -4
  201. package/src/core/record-key.ts +12 -5
  202. package/src/core/result.ts +6 -0
  203. package/src/core/string-format.ts +97 -61
  204. package/src/core/types.ts +12 -6
  205. package/src/external.ts +92 -70
  206. package/src/schema/_parameters.test.ts +416 -0
  207. package/src/schema/array.test.ts +237 -0
  208. package/src/schema/array.ts +17 -11
  209. package/src/schema/blob.test.ts +506 -0
  210. package/src/schema/blob.ts +3 -5
  211. package/src/schema/boolean.test.ts +116 -0
  212. package/src/schema/boolean.ts +5 -7
  213. package/src/schema/bytes.test.ts +226 -0
  214. package/src/schema/bytes.ts +4 -6
  215. package/src/schema/cid.test.ts +155 -0
  216. package/src/schema/cid.ts +14 -8
  217. package/src/schema/custom.test.ts +413 -0
  218. package/src/schema/custom.ts +10 -8
  219. package/src/schema/dict.test.ts +198 -0
  220. package/src/schema/dict.ts +6 -8
  221. package/src/schema/discriminated-union.test.ts +675 -0
  222. package/src/schema/discriminated-union.ts +68 -95
  223. package/src/schema/enum.test.ts +396 -0
  224. package/src/schema/enum.ts +12 -5
  225. package/src/schema/integer.test.ts +312 -0
  226. package/src/schema/integer.ts +5 -7
  227. package/src/schema/intersection.test.ts +32 -0
  228. package/src/schema/intersection.ts +37 -40
  229. package/src/schema/literal.test.ts +531 -0
  230. package/src/schema/literal.ts +12 -5
  231. package/src/schema/never.test.ts +174 -0
  232. package/src/schema/never.ts +3 -10
  233. package/src/schema/null.test.ts +79 -0
  234. package/src/schema/null.ts +3 -5
  235. package/src/schema/nullable.test.ts +480 -0
  236. package/src/schema/nullable.ts +23 -0
  237. package/src/schema/object.test.ts +47 -115
  238. package/src/schema/object.ts +19 -123
  239. package/src/schema/optional.test.ts +485 -0
  240. package/src/schema/optional.ts +31 -0
  241. package/src/schema/params.test.ts +582 -0
  242. package/src/schema/params.ts +37 -55
  243. package/src/schema/payload.test.ts +345 -0
  244. package/src/schema/payload.ts +5 -5
  245. package/src/schema/permission-set.test.ts +679 -0
  246. package/src/schema/permission-set.ts +6 -8
  247. package/src/schema/permission.test.ts +536 -0
  248. package/src/schema/permission.ts +0 -2
  249. package/src/schema/procedure.test.ts +443 -0
  250. package/src/schema/procedure.ts +11 -13
  251. package/src/schema/query.test.ts +408 -0
  252. package/src/schema/query.ts +9 -11
  253. package/src/schema/record.test.ts +694 -0
  254. package/src/schema/record.ts +38 -36
  255. package/src/schema/ref.test.ts +365 -0
  256. package/src/schema/ref.ts +8 -5
  257. package/src/schema/refine.test.ts +578 -0
  258. package/src/schema/refine.ts +85 -0
  259. package/src/schema/regexp.test.ts +580 -0
  260. package/src/schema/regexp.ts +22 -0
  261. package/src/schema/string.test.ts +612 -0
  262. package/src/schema/string.ts +11 -17
  263. package/src/schema/subscription.test.ts +689 -0
  264. package/src/schema/subscription.ts +13 -8
  265. package/src/schema/token.test.ts +428 -0
  266. package/src/schema/token.ts +3 -5
  267. package/src/schema/typed-object.test.ts +612 -0
  268. package/src/schema/typed-object.ts +23 -20
  269. package/src/schema/typed-ref.test.ts +823 -0
  270. package/src/schema/typed-ref.ts +10 -5
  271. package/src/schema/typed-union.test.ts +378 -0
  272. package/src/schema/typed-union.ts +6 -15
  273. package/src/schema/union.test.ts +200 -0
  274. package/src/schema/union.ts +5 -4
  275. package/src/schema/unknown-object.test.ts +592 -0
  276. package/src/schema/unknown-object.ts +3 -5
  277. package/src/schema/unknown.test.ts +312 -0
  278. package/src/schema/unknown.ts +3 -3
  279. package/src/schema.ts +7 -1
  280. package/src/util/array-agg.ts +1 -0
  281. package/src/util/lazy-property.ts +14 -0
  282. package/src/validation/schema.ts +92 -0
  283. package/src/validation/validation-error.ts +60 -9
  284. package/src/validation/validation-issue.ts +141 -144
  285. package/src/validation/validator.ts +67 -206
  286. package/src/validation.ts +1 -0
  287. package/tsconfig.build.json +12 -0
  288. package/tsconfig.json +7 -0
  289. package/tsconfig.tests.json +9 -0
@@ -1,134 +1,51 @@
1
1
  import { isPlainObject } from '@atproto/lex-data'
2
- import { Simplify } from '../core.js'
2
+ import { WithOptionalProperties } from '../core.js'
3
+ import { lazyProperty } from '../util/lazy-property.js'
3
4
  import {
4
5
  Infer,
6
+ Schema,
5
7
  ValidationResult,
6
8
  Validator,
7
9
  ValidatorContext,
8
10
  } from '../validation.js'
9
- import { DictSchema } from './dict.js'
10
11
 
11
- export type ObjectSchemaProperties = { [_ in string]: Validator<any> }
12
- export type ObjectSchemaOptions = {
13
- required?: readonly string[]
14
- nullable?: readonly string[]
15
- unknownProperties?: 'strict' | DictSchema
16
- }
17
-
18
- export type ObjectSchemaNullValue<
19
- O extends ObjectSchemaOptions,
20
- K extends string,
21
- > = O extends { nullable: readonly (infer N extends string)[] }
22
- ? K extends N
23
- ? null
24
- : never
25
- : never
26
-
27
- export type ObjectSchemaPropertiesOutput<
28
- P extends ObjectSchemaProperties,
29
- O extends ObjectSchemaOptions,
30
- > = O extends { required: readonly (infer R extends string)[] }
31
- ? {
32
- -readonly [K in string & keyof P & R]-?:
33
- | Infer<P[K]>
34
- | ObjectSchemaNullValue<O, K>
35
- } & {
36
- -readonly [K in Exclude<string & keyof P, R>]?:
37
- | Infer<P[K]>
38
- | ObjectSchemaNullValue<O, K>
39
- }
40
- : {
41
- -readonly [K in string & keyof P]?:
42
- | Infer<P[K]>
43
- | ObjectSchemaNullValue<O, K>
44
- }
12
+ export type ObjectSchemaShape = Record<string, Validator>
45
13
 
46
- /**
47
- * Allows to more accurately represent the intersection of two object types
48
- * where both types may share some keys, and one of them uses an index
49
- * signature.
50
- *
51
- * @see {@link https://www.typescriptlang.org/play/?#code/C4TwDgpgBAglC8UDeUBmB7dAuKByARgIYBOuUAvlAGTJQDaA+lAJYB2UAzsMWwOYC6OVgFcAtvgjEKAKGkATCAGMANiWiL0rLlEI4YsjVuBQA1hBA4uPVrwRQARBnT2Dm7QDdCy4dESE6ZiD8UAD0IVAi4pJQABQcABbowspyUBIORMT2AJSyEAAeYOjExqCQUACSrMCSHErAzJoAPNJQsFAFNaxyHFAASkrFck1WfAA0UMKsJqzoAO6sAHxjrVAAQh35XT39g8TDozYTUzPzSyuLdqtwVKttMYHoqO00j88bnRDdvawQ7pJ3NpQAD860BbRwSHBQLadAA0ix2G91oJ1vDggAfWABcxPF5QOH8aFtci5aRlaAwVDMfIQVKIKo1Yh1RQNZq0Jw4AgkMjkCYoRiIzjcPioyISKTkRayBQqNRQQzaQgAMRpdL01NpclcRignm8EFVWrsKrVchxQVC4XF0SxmSAA Playground link}
52
- */
53
- type Intersect<
54
- A extends Record<string, unknown>,
55
- B extends Record<string, unknown>,
56
- > = B[keyof B] extends never
57
- ? A
58
- : keyof A & keyof B extends never
59
- ? // If A and B don't overlap, just return A & B
60
- A & B
61
- : // Otherwise, properly represent the fact that accessing using an
62
- // index signature could return a value from either A or B
63
- A & { [K in keyof B]: B[K] | A[keyof A & K] }
64
-
65
- export type ObjectSchemaOutput<
66
- P extends ObjectSchemaProperties,
67
- O extends ObjectSchemaOptions,
68
- > = O extends {
69
- unknownProperties: Validator<infer D extends Record<string, unknown>>
70
- }
71
- ? Simplify<Intersect<ObjectSchemaPropertiesOutput<P, O>, D>>
72
- : Simplify<ObjectSchemaPropertiesOutput<P, O>>
14
+ export type ObjectSchemaOutput<Shape extends ObjectSchemaShape> =
15
+ WithOptionalProperties<{
16
+ [K in keyof Shape]: Infer<Shape[K]>
17
+ }>
73
18
 
74
19
  export class ObjectSchema<
75
- const Validators extends ObjectSchemaProperties = any,
76
- const Options extends ObjectSchemaOptions = any,
77
- const Output extends ObjectSchemaOutput<
78
- Validators,
79
- Options
80
- > = ObjectSchemaOutput<Validators, Options>,
81
- > extends Validator<Output> {
82
- constructor(
83
- readonly validators: Validators,
84
- readonly options: Options,
85
- ) {
20
+ const Shape extends ObjectSchemaShape = any,
21
+ > extends Schema<ObjectSchemaOutput<Shape>> {
22
+ constructor(readonly shape: Shape) {
86
23
  super()
87
24
  }
88
25
 
89
26
  get validatorsMap(): Map<string, Validator> {
90
- const map = new Map(Object.entries(this.validators))
91
-
92
- // Cache the map on the instance (to avoid re-creating it)
93
- Object.defineProperty(this, 'validatorsMap', {
94
- value: map,
95
- writable: false,
96
- enumerable: false,
97
- configurable: true,
98
- })
27
+ const map = new Map(Object.entries(this.shape))
99
28
 
100
- return map
29
+ return lazyProperty(this, 'validatorsMap', map)
101
30
  }
102
31
 
103
- override validateInContext(
32
+ validateInContext(
104
33
  input: unknown,
105
34
  ctx: ValidatorContext,
106
- ): ValidationResult<Output> {
35
+ ): ValidationResult<ObjectSchemaOutput<Shape>> {
107
36
  if (!isPlainObject(input)) {
108
- return ctx.issueInvalidType(input, ['object'])
37
+ return ctx.issueInvalidType(input, 'object')
109
38
  }
110
39
 
111
40
  // Lazily copy value
112
41
  let copy: undefined | Record<string, unknown>
113
42
 
114
43
  for (const [key, propDef] of this.validatorsMap) {
115
- if (input[key] === null && this.options.nullable?.includes(key)) {
116
- continue
117
- }
118
-
119
44
  const result = ctx.validateChild(input, key, propDef)
120
45
  if (!result.success) {
121
- // Because default values are provided by child validators, we need to
122
- // run the validator to get the default value and, in case of failure,
123
- // ignore validation error that were caused by missing keys.
124
46
  if (!(key in input)) {
125
- if (this.options.required?.includes(key)) {
126
- // Transform into "required key" issue
127
- return ctx.issueRequiredKey(input, key)
128
- }
129
-
130
- // Ignore missing non-required key
131
- continue
47
+ // Transform into "required key" issue
48
+ return ctx.issueRequiredKey(input, key)
132
49
  }
133
50
 
134
51
  return result
@@ -136,11 +53,6 @@ export class ObjectSchema<
136
53
 
137
54
  // Skip copying if key is not present in input (and value is undefined)
138
55
  if (result.value === undefined && !(key in input)) {
139
- // Except if the key is required
140
- if (this.options.required?.includes(key)) {
141
- return ctx.issueRequiredKey(input, key)
142
- }
143
-
144
56
  continue
145
57
  }
146
58
 
@@ -150,23 +62,7 @@ export class ObjectSchema<
150
62
  }
151
63
  }
152
64
 
153
- if (this.options.unknownProperties === 'strict') {
154
- for (const key of Object.keys(input)) {
155
- if (!this.validatorsMap.has(key)) {
156
- return ctx.issueInvalidPropertyType(input, key, 'undefined')
157
- }
158
- }
159
- } else if (this.options.unknownProperties) {
160
- const result = this.options.unknownProperties.validateInContext(
161
- copy ?? input,
162
- ctx,
163
- { ignoredKeys: this.validatorsMap },
164
- )
165
- if (!result.success) return result
166
- if (result.value !== input) copy = result.value
167
- }
168
-
169
- const output = (copy ?? input) as Output
65
+ const output = (copy ?? input) as ObjectSchemaOutput<Shape>
170
66
 
171
67
  return ctx.success(output)
172
68
  }
@@ -0,0 +1,485 @@
1
+ import { BooleanSchema } from './boolean.js'
2
+ import { IntegerSchema } from './integer.js'
3
+ import { OptionalSchema } from './optional.js'
4
+ import { StringSchema } from './string.js'
5
+
6
+ describe('OptionalSchema', () => {
7
+ describe('basic validation with string schema', () => {
8
+ const schema = new OptionalSchema(new StringSchema({}))
9
+
10
+ it('validates defined string values', () => {
11
+ const result = schema.safeParse('hello')
12
+ expect(result.success).toBe(true)
13
+ if (result.success) {
14
+ expect(result.value).toBe('hello')
15
+ }
16
+ })
17
+
18
+ it('validates empty strings', () => {
19
+ const result = schema.safeParse('')
20
+ expect(result.success).toBe(true)
21
+ if (result.success) {
22
+ expect(result.value).toBe('')
23
+ }
24
+ })
25
+
26
+ it('validates undefined', () => {
27
+ const result = schema.safeParse(undefined)
28
+ expect(result.success).toBe(true)
29
+ if (result.success) {
30
+ expect(result.value).toBe(undefined)
31
+ }
32
+ })
33
+
34
+ it('rejects invalid types for the inner schema', () => {
35
+ const result = schema.safeParse(123)
36
+ expect(result.success).toBe(false)
37
+ })
38
+
39
+ it('rejects null', () => {
40
+ const result = schema.safeParse(null)
41
+ expect(result.success).toBe(false)
42
+ })
43
+
44
+ it('rejects booleans', () => {
45
+ const result = schema.safeParse(true)
46
+ expect(result.success).toBe(false)
47
+ })
48
+
49
+ it('rejects objects', () => {
50
+ const result = schema.safeParse({ value: 'hello' })
51
+ expect(result.success).toBe(false)
52
+ })
53
+
54
+ it('rejects arrays', () => {
55
+ const result = schema.safeParse(['hello'])
56
+ expect(result.success).toBe(false)
57
+ })
58
+ })
59
+
60
+ describe('basic validation with integer schema', () => {
61
+ const schema = new OptionalSchema(new IntegerSchema({}))
62
+
63
+ it('validates defined integer values', () => {
64
+ const result = schema.safeParse(42)
65
+ expect(result.success).toBe(true)
66
+ if (result.success) {
67
+ expect(result.value).toBe(42)
68
+ }
69
+ })
70
+
71
+ it('validates zero', () => {
72
+ const result = schema.safeParse(0)
73
+ expect(result.success).toBe(true)
74
+ if (result.success) {
75
+ expect(result.value).toBe(0)
76
+ }
77
+ })
78
+
79
+ it('validates negative integers', () => {
80
+ const result = schema.safeParse(-42)
81
+ expect(result.success).toBe(true)
82
+ if (result.success) {
83
+ expect(result.value).toBe(-42)
84
+ }
85
+ })
86
+
87
+ it('validates undefined', () => {
88
+ const result = schema.safeParse(undefined)
89
+ expect(result.success).toBe(true)
90
+ if (result.success) {
91
+ expect(result.value).toBe(undefined)
92
+ }
93
+ })
94
+
95
+ it('rejects invalid types for the inner schema', () => {
96
+ const result = schema.safeParse('not a number')
97
+ expect(result.success).toBe(false)
98
+ })
99
+
100
+ it('rejects floats', () => {
101
+ const result = schema.safeParse(3.14)
102
+ expect(result.success).toBe(false)
103
+ })
104
+
105
+ it('rejects null', () => {
106
+ const result = schema.safeParse(null)
107
+ expect(result.success).toBe(false)
108
+ })
109
+ })
110
+
111
+ describe('basic validation with boolean schema', () => {
112
+ const schema = new OptionalSchema(new BooleanSchema({}))
113
+
114
+ it('validates true', () => {
115
+ const result = schema.safeParse(true)
116
+ expect(result.success).toBe(true)
117
+ if (result.success) {
118
+ expect(result.value).toBe(true)
119
+ }
120
+ })
121
+
122
+ it('validates false', () => {
123
+ const result = schema.safeParse(false)
124
+ expect(result.success).toBe(true)
125
+ if (result.success) {
126
+ expect(result.value).toBe(false)
127
+ }
128
+ })
129
+
130
+ it('validates undefined', () => {
131
+ const result = schema.safeParse(undefined)
132
+ expect(result.success).toBe(true)
133
+ if (result.success) {
134
+ expect(result.value).toBe(undefined)
135
+ }
136
+ })
137
+
138
+ it('rejects strings', () => {
139
+ const result = schema.safeParse('true')
140
+ expect(result.success).toBe(false)
141
+ })
142
+
143
+ it('rejects numbers', () => {
144
+ const result = schema.safeParse(1)
145
+ expect(result.success).toBe(false)
146
+ })
147
+
148
+ it('rejects null', () => {
149
+ const result = schema.safeParse(null)
150
+ expect(result.success).toBe(false)
151
+ })
152
+ })
153
+
154
+ describe('inner schema with constraints', () => {
155
+ const schema = new OptionalSchema(
156
+ new StringSchema({ minLength: 5, maxLength: 10 }),
157
+ )
158
+
159
+ it('validates values meeting inner schema constraints', () => {
160
+ const result = schema.safeParse('hello')
161
+ expect(result.success).toBe(true)
162
+ })
163
+
164
+ it('validates values at minimum boundary', () => {
165
+ const result = schema.safeParse('abcde')
166
+ expect(result.success).toBe(true)
167
+ })
168
+
169
+ it('validates values at maximum boundary', () => {
170
+ const result = schema.safeParse('1234567890')
171
+ expect(result.success).toBe(true)
172
+ })
173
+
174
+ it('validates undefined', () => {
175
+ const result = schema.safeParse(undefined)
176
+ expect(result.success).toBe(true)
177
+ })
178
+
179
+ it('rejects values violating inner schema minimum constraint', () => {
180
+ const result = schema.safeParse('hi')
181
+ expect(result.success).toBe(false)
182
+ })
183
+
184
+ it('rejects values violating inner schema maximum constraint', () => {
185
+ const result = schema.safeParse('this is too long')
186
+ expect(result.success).toBe(false)
187
+ })
188
+
189
+ it('rejects empty strings when inner schema has minLength', () => {
190
+ const result = schema.safeParse('')
191
+ expect(result.success).toBe(false)
192
+ })
193
+ })
194
+
195
+ describe('inner schema with default value', () => {
196
+ const schema = new OptionalSchema(new StringSchema({ default: 'default' }))
197
+
198
+ it('applies default value when undefined is provided', () => {
199
+ const result = schema.safeParse(undefined)
200
+ expect(result.success).toBe(true)
201
+ if (result.success) {
202
+ expect(result.value).toBe('default')
203
+ }
204
+ })
205
+
206
+ it('does not apply default when explicit value is provided', () => {
207
+ const result = schema.safeParse('explicit')
208
+ expect(result.success).toBe(true)
209
+ if (result.success) {
210
+ expect(result.value).toBe('explicit')
211
+ }
212
+ })
213
+
214
+ it('does not apply default when empty string is provided', () => {
215
+ const result = schema.safeParse('')
216
+ expect(result.success).toBe(true)
217
+ if (result.success) {
218
+ expect(result.value).toBe('')
219
+ }
220
+ })
221
+ })
222
+
223
+ describe('inner schema with default value and constraints', () => {
224
+ const schema = new OptionalSchema(
225
+ new StringSchema({ default: 'default', minLength: 5 }),
226
+ )
227
+
228
+ it('applies default value when undefined is provided', () => {
229
+ const result = schema.safeParse(undefined)
230
+ expect(result.success).toBe(true)
231
+ if (result.success) {
232
+ expect(result.value).toBe('default')
233
+ }
234
+ })
235
+
236
+ it('validates explicit values against constraints', () => {
237
+ const result = schema.safeParse('hello')
238
+ expect(result.success).toBe(true)
239
+ })
240
+
241
+ it('rejects explicit values violating constraints', () => {
242
+ const result = schema.safeParse('hi')
243
+ expect(result.success).toBe(false)
244
+ })
245
+ })
246
+
247
+ describe('inner schema with invalid default value', () => {
248
+ const schema = new OptionalSchema(
249
+ new StringSchema({ default: 'bad', minLength: 5 }),
250
+ )
251
+
252
+ it('returns undefined when default value violates constraints', () => {
253
+ const result = schema.safeParse(undefined)
254
+ expect(result.success).toBe(true)
255
+ if (result.success) {
256
+ expect(result.value).toBe(undefined)
257
+ }
258
+ })
259
+
260
+ it('still validates conforming explicit values', () => {
261
+ const result = schema.safeParse('valid')
262
+ expect(result.success).toBe(true)
263
+ })
264
+ })
265
+
266
+ describe('inner schema with integer default', () => {
267
+ const schema = new OptionalSchema(new IntegerSchema({ default: 42 }))
268
+
269
+ it('applies default value when undefined is provided', () => {
270
+ const result = schema.safeParse(undefined)
271
+ expect(result.success).toBe(true)
272
+ if (result.success) {
273
+ expect(result.value).toBe(42)
274
+ }
275
+ })
276
+
277
+ it('does not apply default when explicit value is provided', () => {
278
+ const result = schema.safeParse(100)
279
+ expect(result.success).toBe(true)
280
+ if (result.success) {
281
+ expect(result.value).toBe(100)
282
+ }
283
+ })
284
+
285
+ it('does not apply default when zero is provided', () => {
286
+ const result = schema.safeParse(0)
287
+ expect(result.success).toBe(true)
288
+ if (result.success) {
289
+ expect(result.value).toBe(0)
290
+ }
291
+ })
292
+ })
293
+
294
+ describe('inner schema with boolean default', () => {
295
+ const schema = new OptionalSchema(new BooleanSchema({ default: true }))
296
+
297
+ it('applies default value when undefined is provided', () => {
298
+ const result = schema.safeParse(undefined)
299
+ expect(result.success).toBe(true)
300
+ if (result.success) {
301
+ expect(result.value).toBe(true)
302
+ }
303
+ })
304
+
305
+ it('does not apply default when explicit true is provided', () => {
306
+ const result = schema.safeParse(true)
307
+ expect(result.success).toBe(true)
308
+ if (result.success) {
309
+ expect(result.value).toBe(true)
310
+ }
311
+ })
312
+
313
+ it('does not apply default when explicit false is provided', () => {
314
+ const result = schema.safeParse(false)
315
+ expect(result.success).toBe(true)
316
+ if (result.success) {
317
+ expect(result.value).toBe(false)
318
+ }
319
+ })
320
+ })
321
+
322
+ describe('edge cases', () => {
323
+ const schema = new OptionalSchema(new StringSchema({}))
324
+
325
+ it('handles very long strings', () => {
326
+ const longString = 'a'.repeat(10000)
327
+ const result = schema.safeParse(longString)
328
+ expect(result.success).toBe(true)
329
+ })
330
+
331
+ it('handles strings with special characters', () => {
332
+ const result = schema.safeParse('hello\nworld\ttab')
333
+ expect(result.success).toBe(true)
334
+ })
335
+
336
+ it('handles strings with unicode characters', () => {
337
+ const result = schema.safeParse('Hello 世界 🌍')
338
+ expect(result.success).toBe(true)
339
+ })
340
+
341
+ it('handles empty string distinctly from undefined', () => {
342
+ const emptyResult = schema.safeParse('')
343
+ expect(emptyResult.success).toBe(true)
344
+ if (emptyResult.success) {
345
+ expect(emptyResult.value).toBe('')
346
+ }
347
+
348
+ const undefinedResult = schema.safeParse(undefined)
349
+ expect(undefinedResult.success).toBe(true)
350
+ if (undefinedResult.success) {
351
+ expect(undefinedResult.value).toBe(undefined)
352
+ }
353
+ })
354
+ })
355
+
356
+ describe('type distinctions', () => {
357
+ it('distinguishes between zero and undefined for integers', () => {
358
+ const schema = new OptionalSchema(new IntegerSchema({}))
359
+
360
+ const zeroResult = schema.safeParse(0)
361
+ expect(zeroResult.success).toBe(true)
362
+ if (zeroResult.success) {
363
+ expect(zeroResult.value).toBe(0)
364
+ }
365
+
366
+ const undefinedResult = schema.safeParse(undefined)
367
+ expect(undefinedResult.success).toBe(true)
368
+ if (undefinedResult.success) {
369
+ expect(undefinedResult.value).toBe(undefined)
370
+ }
371
+ })
372
+
373
+ it('distinguishes between false and undefined for booleans', () => {
374
+ const schema = new OptionalSchema(new BooleanSchema({}))
375
+
376
+ const falseResult = schema.safeParse(false)
377
+ expect(falseResult.success).toBe(true)
378
+ if (falseResult.success) {
379
+ expect(falseResult.value).toBe(false)
380
+ }
381
+
382
+ const undefinedResult = schema.safeParse(undefined)
383
+ expect(undefinedResult.success).toBe(true)
384
+ if (undefinedResult.success) {
385
+ expect(undefinedResult.value).toBe(undefined)
386
+ }
387
+ })
388
+
389
+ it('distinguishes between empty string and undefined for strings', () => {
390
+ const schema = new OptionalSchema(new StringSchema({}))
391
+
392
+ const emptyResult = schema.safeParse('')
393
+ expect(emptyResult.success).toBe(true)
394
+ if (emptyResult.success) {
395
+ expect(emptyResult.value).toBe('')
396
+ }
397
+
398
+ const undefinedResult = schema.safeParse(undefined)
399
+ expect(undefinedResult.success).toBe(true)
400
+ if (undefinedResult.success) {
401
+ expect(undefinedResult.value).toBe(undefined)
402
+ }
403
+ })
404
+ })
405
+
406
+ describe('nested optional schemas', () => {
407
+ const schema = new OptionalSchema(new OptionalSchema(new StringSchema({})))
408
+
409
+ it('validates defined values through nested optionals', () => {
410
+ const result = schema.safeParse('hello')
411
+ expect(result.success).toBe(true)
412
+ if (result.success) {
413
+ expect(result.value).toBe('hello')
414
+ }
415
+ })
416
+
417
+ it('validates undefined through nested optionals', () => {
418
+ const result = schema.safeParse(undefined)
419
+ expect(result.success).toBe(true)
420
+ if (result.success) {
421
+ expect(result.value).toBe(undefined)
422
+ }
423
+ })
424
+
425
+ it('rejects invalid types through nested optionals', () => {
426
+ const result = schema.safeParse(123)
427
+ expect(result.success).toBe(false)
428
+ })
429
+ })
430
+
431
+ describe('inner schema format constraints', () => {
432
+ const schema = new OptionalSchema(new StringSchema({ format: 'uri' }))
433
+
434
+ it('validates values meeting format constraint', () => {
435
+ const result = schema.safeParse('https://example.com')
436
+ expect(result.success).toBe(true)
437
+ })
438
+
439
+ it('validates undefined', () => {
440
+ const result = schema.safeParse(undefined)
441
+ expect(result.success).toBe(true)
442
+ })
443
+
444
+ it('rejects values violating format constraint', () => {
445
+ const result = schema.safeParse('not a uri')
446
+ expect(result.success).toBe(false)
447
+ })
448
+ })
449
+
450
+ describe('integer constraint validation', () => {
451
+ const schema = new OptionalSchema(
452
+ new IntegerSchema({ minimum: 0, maximum: 100 }),
453
+ )
454
+
455
+ it('validates values within range', () => {
456
+ const result = schema.safeParse(50)
457
+ expect(result.success).toBe(true)
458
+ })
459
+
460
+ it('validates values at minimum boundary', () => {
461
+ const result = schema.safeParse(0)
462
+ expect(result.success).toBe(true)
463
+ })
464
+
465
+ it('validates values at maximum boundary', () => {
466
+ const result = schema.safeParse(100)
467
+ expect(result.success).toBe(true)
468
+ })
469
+
470
+ it('validates undefined', () => {
471
+ const result = schema.safeParse(undefined)
472
+ expect(result.success).toBe(true)
473
+ })
474
+
475
+ it('rejects values below minimum', () => {
476
+ const result = schema.safeParse(-1)
477
+ expect(result.success).toBe(false)
478
+ })
479
+
480
+ it('rejects values above maximum', () => {
481
+ const result = schema.safeParse(101)
482
+ expect(result.success).toBe(false)
483
+ })
484
+ })
485
+ })
@@ -0,0 +1,31 @@
1
+ import {
2
+ Schema,
3
+ ValidationResult,
4
+ Validator,
5
+ ValidatorContext,
6
+ } from '../validation.js'
7
+
8
+ export class OptionalSchema<V> extends Schema<V | undefined> {
9
+ constructor(readonly schema: Validator<V>) {
10
+ super()
11
+ }
12
+
13
+ validateInContext(
14
+ input: unknown,
15
+ ctx: ValidatorContext,
16
+ ): ValidationResult<V | undefined> {
17
+ // @NOTE The inner schema might apply a default value so we need to run it
18
+ // first, even if input is undefined.
19
+ const result = ctx.validate(input, this.schema)
20
+
21
+ if (result.success) {
22
+ return result
23
+ }
24
+
25
+ if (input === undefined) {
26
+ return ctx.success(input)
27
+ }
28
+
29
+ return result
30
+ }
31
+ }