@atproto/lex-schema 0.0.1 → 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 +8 -4
  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 +13 -56
  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 +23 -134
  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,154 +1,59 @@
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
135
- } else if (result.value === undefined) {
136
- // Special case for validators that output "undefined" values (typically
137
- // UnknownSchema) since they cannot differentiate between "missing key"
138
- // and "key with undefined value"
139
-
140
- if (!(key in input)) {
141
- // Input was missing the key (was "undefined")
142
- if (this.options.required?.includes(key)) {
143
- return ctx.issueRequiredKey(input, key)
144
- }
145
-
146
- // Ignore missing non-required key
147
- continue
148
- }
52
+ }
149
53
 
150
- // if "key" existed in input (would typically be "undefined"), we keep
151
- // it as-is by continuing processing as if it was any other value.
54
+ // Skip copying if key is not present in input (and value is undefined)
55
+ if (result.value === undefined && !(key in input)) {
56
+ continue
152
57
  }
153
58
 
154
59
  if (result.value !== input[key]) {
@@ -157,23 +62,7 @@ export class ObjectSchema<
157
62
  }
158
63
  }
159
64
 
160
- if (this.options.unknownProperties === 'strict') {
161
- for (const key of Object.keys(input)) {
162
- if (!this.validatorsMap.has(key)) {
163
- return ctx.issueInvalidPropertyType(input, key, 'undefined')
164
- }
165
- }
166
- } else if (this.options.unknownProperties) {
167
- const result = this.options.unknownProperties.validateInContext(
168
- copy ?? input,
169
- ctx,
170
- { ignoredKeys: this.validatorsMap },
171
- )
172
- if (!result.success) return result
173
- if (result.value !== input) copy = result.value
174
- }
175
-
176
- const output = (copy ?? input) as Output
65
+ const output = (copy ?? input) as ObjectSchemaOutput<Shape>
177
66
 
178
67
  return ctx.success(output)
179
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
+ })