@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,8 +1,7 @@
1
1
  import { isPlainObject } from '@atproto/lex-data'
2
- import { ArrayContaining } from '../core.js'
3
2
  import {
4
- ValidationError,
5
- ValidationFailure,
3
+ Infer,
4
+ Schema,
6
5
  ValidationResult,
7
6
  Validator,
8
7
  ValidatorContext,
@@ -11,28 +10,23 @@ import { EnumSchema } from './enum.js'
11
10
  import { LiteralSchema } from './literal.js'
12
11
  import { ObjectSchema } from './object.js'
13
12
 
14
- export type DiscriminatedUnionSchemaVariant<Discriminator extends string> =
15
- ObjectSchema<
16
- { [_ in Discriminator]: Validator },
17
- { required: ArrayContaining<Discriminator, string> }
18
- >
13
+ export type DiscriminatedUnionVariant<Discriminator extends string> =
14
+ ObjectSchema<Record<Discriminator, EnumSchema | LiteralSchema>>
19
15
 
20
- export type DiscriminatedUnionSchemaVariants<Discriminator extends string> =
16
+ export type DiscriminatedUnionVariants<Discriminator extends string> =
21
17
  readonly [
22
- DiscriminatedUnionSchemaVariant<Discriminator>,
23
- ...DiscriminatedUnionSchemaVariant<Discriminator>[],
18
+ DiscriminatedUnionVariant<Discriminator>,
19
+ ...DiscriminatedUnionVariant<Discriminator>[],
24
20
  ]
25
21
 
26
22
  export type DiscriminatedUnionSchemaOutput<
27
- Options extends readonly Validator[],
28
- > = Options extends readonly [Validator<infer V>]
29
- ? V
30
- : Options extends readonly [
31
- Validator<infer V>,
32
- ...infer Rest extends Validator[],
33
- ]
34
- ? V | DiscriminatedUnionSchemaOutput<Rest>
35
- : never
23
+ Variants extends readonly Validator[],
24
+ > = Variants extends readonly [
25
+ infer V extends Validator,
26
+ ...infer Rest extends readonly Validator[],
27
+ ]
28
+ ? Infer<V> | DiscriminatedUnionSchemaOutput<Rest>
29
+ : never
36
30
 
37
31
  /**
38
32
  * @note There is no discriminated union in Lexicon schemas. This is a custom
@@ -41,104 +35,83 @@ export type DiscriminatedUnionSchemaOutput<
41
35
  */
42
36
  export class DiscriminatedUnionSchema<
43
37
  const Discriminator extends string = any,
44
- const Options extends DiscriminatedUnionSchemaVariants<Discriminator> = any,
45
- > extends Validator<DiscriminatedUnionSchemaOutput<Options>> {
38
+ const Variants extends DiscriminatedUnionVariants<Discriminator> = any,
39
+ > extends Schema<DiscriminatedUnionSchemaOutput<Variants>> {
40
+ readonly variantsMap: Map<unknown, DiscriminatedUnionVariant<Discriminator>>
41
+
46
42
  constructor(
47
43
  readonly discriminator: Discriminator,
48
- readonly variants: Options,
44
+ variants: Variants,
49
45
  ) {
50
46
  super()
51
- }
52
-
53
- /**
54
- * If all variants have a literal or enum for the discriminator property,
55
- * and there are no overlapping values, returns a map of discriminator values
56
- * to variants. Otherwise, returns null.
57
- */
58
- protected get variantsMap() {
59
- const map = new Map<
60
- unknown,
61
- DiscriminatedUnionSchemaVariant<Discriminator>
62
- >()
63
- for (const variant of this.variants) {
64
- const schema = variant.validators[this.discriminator]
65
- if (schema instanceof LiteralSchema) {
66
- if (map.has(schema.value)) return null // overlapping value
67
- map.set(schema.value, variant)
68
- } else if (schema instanceof EnumSchema) {
69
- for (const val of schema.values) {
70
- if (map.has(val)) return null // overlapping value
71
- map.set(val, variant)
72
- }
73
- } else {
74
- return null // not a literal or enum
75
- }
76
- }
77
47
 
78
- // Cache the map on the instance (to avoid re-computing)
79
- Object.defineProperty(this, 'variantsMap', {
80
- value: map,
81
- writable: false,
82
- enumerable: false,
83
- configurable: true,
84
- })
85
-
86
- return map
48
+ // Although we usually try to avoid initialization work in constructors,
49
+ // here it is necessary to ensure that invalid discriminated throw from the
50
+ // place of construction, rather than later during validation.
51
+ this.variantsMap = buildVariantsMap(discriminator, variants)
87
52
  }
88
53
 
89
- override validateInContext(
54
+ validateInContext(
90
55
  input: unknown,
91
56
  ctx: ValidatorContext,
92
- ): ValidationResult<DiscriminatedUnionSchemaOutput<Options>> {
57
+ ): ValidationResult<DiscriminatedUnionSchemaOutput<Variants>> {
93
58
  if (!isPlainObject(input)) {
94
59
  return ctx.issueInvalidType(input, 'object')
95
60
  }
96
61
 
97
- if (!Object.hasOwn(input, this.discriminator)) {
98
- return ctx.issueRequiredKey(input, this.discriminator)
62
+ const { discriminator } = this
63
+
64
+ if (!Object.hasOwn(input, discriminator)) {
65
+ return ctx.issueRequiredKey(input, discriminator)
99
66
  }
100
67
 
101
- // Fast path: if we have a mapping of discriminator values to variants,
102
- // we can directly select the correct variant to validate against. This also
103
- // outputs a better error (with a single failure issue) when the discriminator.
104
- if (this.variantsMap) {
105
- const variant = this.variantsMap.get(input[this.discriminator])
106
- if (!variant) {
107
- return ctx.issueInvalidPropertyValue(input, this.discriminator, [
108
- ...this.variantsMap.keys(),
109
- ])
110
- }
68
+ const discriminatorValue = input[discriminator]
111
69
 
70
+ const variant = this.variantsMap.get(discriminatorValue)
71
+ if (variant) {
112
72
  return ctx.validate(input, variant) as ValidationResult<
113
- DiscriminatedUnionSchemaOutput<Options>
73
+ DiscriminatedUnionSchemaOutput<Variants>
114
74
  >
115
75
  }
116
76
 
117
- // Slow path: try validating against each variant and return the first
118
- // successful one (or aggregate all failures if none match).
119
- const failures: ValidationFailure[] = []
120
-
121
- for (const variant of this.variants) {
122
- const discSchema = variant.validators[this.discriminator]
123
- const discResult = ctx.validateChild(
124
- input,
125
- this.discriminator,
126
- discSchema,
127
- )
77
+ return ctx.issueInvalidPropertyValue(input, discriminator, [
78
+ ...this.variantsMap.keys(),
79
+ ])
80
+ }
81
+ }
128
82
 
129
- if (!discResult.success) {
130
- failures.push(discResult)
131
- continue
83
+ function buildVariantsMap<Discriminator extends string>(
84
+ discriminator: Discriminator,
85
+ variants: DiscriminatedUnionVariants<Discriminator>,
86
+ ) {
87
+ const variantsMap = new Map<
88
+ unknown,
89
+ DiscriminatedUnionVariant<Discriminator>
90
+ >()
91
+
92
+ for (const variant of variants) {
93
+ const schema = variant.shape[discriminator]
94
+ if (schema instanceof LiteralSchema) {
95
+ if (variantsMap.has(schema.value)) {
96
+ throw new TypeError(`Overlapping discriminator value: ${schema.value}`)
132
97
  }
98
+ variantsMap.set(schema.value, variant)
99
+ } else if (schema instanceof EnumSchema) {
100
+ for (const val of schema.values) {
101
+ if (variantsMap.has(val)) {
102
+ throw new TypeError(`Overlapping discriminator value: ${val}`)
103
+ }
104
+ variantsMap.set(val, variant)
105
+ }
106
+ } else {
107
+ // Only enumerable discriminator schemas are supported
133
108
 
134
- return ctx.validate(input, variant) as ValidationResult<
135
- DiscriminatedUnionSchemaOutput<Options>
136
- >
137
- }
138
-
139
- return {
140
- success: false,
141
- error: ValidationError.fromFailures(failures),
109
+ // Should never happen if types are used correctly
110
+ throw new TypeError(
111
+ `Discriminator schema must be a LiteralSchema or EnumSchema`,
112
+ )
142
113
  }
143
114
  }
115
+
116
+ return variantsMap
144
117
  }
@@ -0,0 +1,396 @@
1
+ import { EnumSchema } from './enum.js'
2
+
3
+ describe('EnumSchema', () => {
4
+ describe('with string values', () => {
5
+ const schema = new EnumSchema(['male', 'female', 'other'])
6
+
7
+ it('validates matching string values', () => {
8
+ const result = schema.safeParse('male')
9
+ expect(result.success).toBe(true)
10
+ })
11
+
12
+ it('validates all enum values', () => {
13
+ expect(schema.safeParse('male').success).toBe(true)
14
+ expect(schema.safeParse('female').success).toBe(true)
15
+ expect(schema.safeParse('other').success).toBe(true)
16
+ })
17
+
18
+ it('rejects non-matching string values', () => {
19
+ const result = schema.safeParse('unknown')
20
+ expect(result.success).toBe(false)
21
+ })
22
+
23
+ it('rejects null', () => {
24
+ const result = schema.safeParse(null)
25
+ expect(result.success).toBe(false)
26
+ })
27
+
28
+ it('rejects undefined', () => {
29
+ const result = schema.safeParse(undefined)
30
+ expect(result.success).toBe(false)
31
+ })
32
+
33
+ it('rejects numbers', () => {
34
+ const result = schema.safeParse(123)
35
+ expect(result.success).toBe(false)
36
+ })
37
+
38
+ it('rejects booleans', () => {
39
+ const result = schema.safeParse(true)
40
+ expect(result.success).toBe(false)
41
+ })
42
+
43
+ it('rejects objects', () => {
44
+ const result = schema.safeParse({ value: 'male' })
45
+ expect(result.success).toBe(false)
46
+ })
47
+
48
+ it('rejects arrays', () => {
49
+ const result = schema.safeParse(['male'])
50
+ expect(result.success).toBe(false)
51
+ })
52
+
53
+ it('rejects empty string when not in enum', () => {
54
+ const result = schema.safeParse('')
55
+ expect(result.success).toBe(false)
56
+ })
57
+ })
58
+
59
+ describe('with number values', () => {
60
+ const schema = new EnumSchema([1, 2, 3])
61
+
62
+ it('validates matching number values', () => {
63
+ const result = schema.safeParse(1)
64
+ expect(result.success).toBe(true)
65
+ })
66
+
67
+ it('validates all enum values', () => {
68
+ expect(schema.safeParse(1).success).toBe(true)
69
+ expect(schema.safeParse(2).success).toBe(true)
70
+ expect(schema.safeParse(3).success).toBe(true)
71
+ })
72
+
73
+ it('rejects non-matching number values', () => {
74
+ const result = schema.safeParse(4)
75
+ expect(result.success).toBe(false)
76
+ })
77
+
78
+ it('rejects string numbers', () => {
79
+ const result = schema.safeParse('1')
80
+ expect(result.success).toBe(false)
81
+ })
82
+
83
+ it('rejects zero when not in enum', () => {
84
+ const result = schema.safeParse(0)
85
+ expect(result.success).toBe(false)
86
+ })
87
+
88
+ it('rejects negative numbers when not in enum', () => {
89
+ const result = schema.safeParse(-1)
90
+ expect(result.success).toBe(false)
91
+ })
92
+ })
93
+
94
+ describe('with boolean values', () => {
95
+ const schema = new EnumSchema([true, false])
96
+
97
+ it('validates true', () => {
98
+ const result = schema.safeParse(true)
99
+ expect(result.success).toBe(true)
100
+ })
101
+
102
+ it('validates false', () => {
103
+ const result = schema.safeParse(false)
104
+ expect(result.success).toBe(true)
105
+ })
106
+
107
+ it('rejects string booleans', () => {
108
+ expect(schema.safeParse('true').success).toBe(false)
109
+ expect(schema.safeParse('false').success).toBe(false)
110
+ })
111
+
112
+ it('rejects number booleans', () => {
113
+ expect(schema.safeParse(1).success).toBe(false)
114
+ expect(schema.safeParse(0).success).toBe(false)
115
+ })
116
+ })
117
+
118
+ describe('with single boolean value', () => {
119
+ const schema = new EnumSchema([true])
120
+
121
+ it('validates true', () => {
122
+ const result = schema.safeParse(true)
123
+ expect(result.success).toBe(true)
124
+ })
125
+
126
+ it('rejects false when not in enum', () => {
127
+ const result = schema.safeParse(false)
128
+ expect(result.success).toBe(false)
129
+ })
130
+ })
131
+
132
+ describe('with null value', () => {
133
+ const schema = new EnumSchema([null, 'value'])
134
+
135
+ it('validates null', () => {
136
+ const result = schema.safeParse(null)
137
+ expect(result.success).toBe(true)
138
+ })
139
+
140
+ it('validates other enum values', () => {
141
+ const result = schema.safeParse('value')
142
+ expect(result.success).toBe(true)
143
+ })
144
+
145
+ it('rejects undefined', () => {
146
+ const result = schema.safeParse(undefined)
147
+ expect(result.success).toBe(false)
148
+ })
149
+ })
150
+
151
+ describe('with mixed type values', () => {
152
+ const schema = new EnumSchema(['string', 123, true, null])
153
+
154
+ it('validates string value', () => {
155
+ const result = schema.safeParse('string')
156
+ expect(result.success).toBe(true)
157
+ })
158
+
159
+ it('validates number value', () => {
160
+ const result = schema.safeParse(123)
161
+ expect(result.success).toBe(true)
162
+ })
163
+
164
+ it('validates boolean value', () => {
165
+ const result = schema.safeParse(true)
166
+ expect(result.success).toBe(true)
167
+ })
168
+
169
+ it('validates null value', () => {
170
+ const result = schema.safeParse(null)
171
+ expect(result.success).toBe(true)
172
+ })
173
+
174
+ it('rejects non-matching values', () => {
175
+ expect(schema.safeParse('other').success).toBe(false)
176
+ expect(schema.safeParse(456).success).toBe(false)
177
+ expect(schema.safeParse(false).success).toBe(false)
178
+ expect(schema.safeParse(undefined).success).toBe(false)
179
+ })
180
+ })
181
+
182
+ describe('with default option', () => {
183
+ const schema = new EnumSchema(['red', 'green', 'blue'], { default: 'red' })
184
+
185
+ it('validates matching values', () => {
186
+ const result = schema.safeParse('green')
187
+ expect(result.success).toBe(true)
188
+ })
189
+
190
+ it('uses default when input is undefined', () => {
191
+ const result = schema.safeParse(undefined)
192
+ expect(result.success).toBe(true)
193
+ if (result.success) {
194
+ expect(result.value).toBe('red')
195
+ }
196
+ })
197
+
198
+ it('uses default when no argument is passed', () => {
199
+ const result = schema.safeParse(undefined)
200
+ expect(result.success).toBe(true)
201
+ if (result.success) {
202
+ expect(result.value).toBe('red')
203
+ }
204
+ })
205
+
206
+ it('does not use default for null', () => {
207
+ const result = schema.safeParse(null)
208
+ expect(result.success).toBe(false)
209
+ })
210
+
211
+ it('does not use default for invalid values', () => {
212
+ const result = schema.safeParse('yellow')
213
+ expect(result.success).toBe(false)
214
+ })
215
+
216
+ it('does not use default for empty string', () => {
217
+ const result = schema.safeParse('')
218
+ expect(result.success).toBe(false)
219
+ })
220
+ })
221
+
222
+ describe('with default option as number', () => {
223
+ const schema = new EnumSchema([1, 2, 3], { default: 1 })
224
+
225
+ it('uses default when input is undefined', () => {
226
+ const result = schema.safeParse(undefined)
227
+ expect(result.success).toBe(true)
228
+ if (result.success) {
229
+ expect(result.value).toBe(1)
230
+ }
231
+ })
232
+
233
+ it('does not use default for zero', () => {
234
+ const result = schema.safeParse(0)
235
+ expect(result.success).toBe(false)
236
+ })
237
+ })
238
+
239
+ describe('with default option as boolean', () => {
240
+ const schema = new EnumSchema([true, false], { default: false })
241
+
242
+ it('uses default when input is undefined', () => {
243
+ const result = schema.safeParse(undefined)
244
+ expect(result.success).toBe(true)
245
+ if (result.success) {
246
+ expect(result.value).toBe(false)
247
+ }
248
+ })
249
+
250
+ it('validates true even when default is false', () => {
251
+ const result = schema.safeParse(true)
252
+ expect(result.success).toBe(true)
253
+ if (result.success) {
254
+ expect(result.value).toBe(true)
255
+ }
256
+ })
257
+ })
258
+
259
+ describe('with default option as null', () => {
260
+ const schema = new EnumSchema([null, 'value'], { default: null })
261
+
262
+ it('uses default when input is undefined', () => {
263
+ const result = schema.safeParse(undefined)
264
+ expect(result.success).toBe(true)
265
+ if (result.success) {
266
+ expect(result.value).toBe(null)
267
+ }
268
+ })
269
+
270
+ it('validates explicit null', () => {
271
+ const result = schema.safeParse(null)
272
+ expect(result.success).toBe(true)
273
+ if (result.success) {
274
+ expect(result.value).toBe(null)
275
+ }
276
+ })
277
+ })
278
+
279
+ describe('with single value', () => {
280
+ const schema = new EnumSchema(['only'])
281
+
282
+ it('validates the single value', () => {
283
+ const result = schema.safeParse('only')
284
+ expect(result.success).toBe(true)
285
+ })
286
+
287
+ it('rejects any other value', () => {
288
+ expect(schema.safeParse('other').success).toBe(false)
289
+ expect(schema.safeParse('').success).toBe(false)
290
+ expect(schema.safeParse(null).success).toBe(false)
291
+ expect(schema.safeParse(undefined).success).toBe(false)
292
+ })
293
+ })
294
+
295
+ describe('with empty string value', () => {
296
+ const schema = new EnumSchema(['', 'value'])
297
+
298
+ it('validates empty string', () => {
299
+ const result = schema.safeParse('')
300
+ expect(result.success).toBe(true)
301
+ })
302
+
303
+ it('validates other values', () => {
304
+ const result = schema.safeParse('value')
305
+ expect(result.success).toBe(true)
306
+ })
307
+
308
+ it('rejects non-matching values', () => {
309
+ const result = schema.safeParse('other')
310
+ expect(result.success).toBe(false)
311
+ })
312
+ })
313
+
314
+ describe('with zero value', () => {
315
+ const schema = new EnumSchema([0, 1, 2])
316
+
317
+ it('validates zero', () => {
318
+ const result = schema.safeParse(0)
319
+ expect(result.success).toBe(true)
320
+ })
321
+
322
+ it('validates other values', () => {
323
+ expect(schema.safeParse(1).success).toBe(true)
324
+ expect(schema.safeParse(2).success).toBe(true)
325
+ })
326
+
327
+ it('rejects false even though zero is in enum', () => {
328
+ const result = schema.safeParse(false)
329
+ expect(result.success).toBe(false)
330
+ })
331
+ })
332
+
333
+ describe('case sensitivity', () => {
334
+ const schema = new EnumSchema(['Value', 'VALUE', 'value'])
335
+
336
+ it('validates exact case matches', () => {
337
+ expect(schema.safeParse('Value').success).toBe(true)
338
+ expect(schema.safeParse('VALUE').success).toBe(true)
339
+ expect(schema.safeParse('value').success).toBe(true)
340
+ })
341
+
342
+ it('rejects case mismatches', () => {
343
+ expect(schema.safeParse('vaLue').success).toBe(false)
344
+ expect(schema.safeParse('VaLuE').success).toBe(false)
345
+ })
346
+ })
347
+
348
+ describe('with special string values', () => {
349
+ const schema = new EnumSchema([
350
+ 'with space',
351
+ 'with\ttab',
352
+ 'with\nnewline',
353
+ '123',
354
+ 'true',
355
+ 'null',
356
+ 'undefined',
357
+ ])
358
+
359
+ it('validates strings with spaces', () => {
360
+ const result = schema.safeParse('with space')
361
+ expect(result.success).toBe(true)
362
+ })
363
+
364
+ it('validates strings with tabs', () => {
365
+ const result = schema.safeParse('with\ttab')
366
+ expect(result.success).toBe(true)
367
+ })
368
+
369
+ it('validates strings with newlines', () => {
370
+ const result = schema.safeParse('with\nnewline')
371
+ expect(result.success).toBe(true)
372
+ })
373
+
374
+ it('validates number-like strings', () => {
375
+ const result = schema.safeParse('123')
376
+ expect(result.success).toBe(true)
377
+ })
378
+
379
+ it('rejects actual numbers for number-like strings', () => {
380
+ const result = schema.safeParse(123)
381
+ expect(result.success).toBe(false)
382
+ })
383
+
384
+ it('validates keyword strings', () => {
385
+ expect(schema.safeParse('true').success).toBe(true)
386
+ expect(schema.safeParse('null').success).toBe(true)
387
+ expect(schema.safeParse('undefined').success).toBe(true)
388
+ })
389
+
390
+ it('rejects actual boolean/null/undefined for keyword strings', () => {
391
+ expect(schema.safeParse(true).success).toBe(false)
392
+ expect(schema.safeParse(null).success).toBe(false)
393
+ expect(schema.safeParse(undefined).success).toBe(false)
394
+ })
395
+ })
396
+ })
@@ -1,14 +1,21 @@
1
- import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
1
+ import { Schema, ValidationResult, ValidatorContext } from '../validation.js'
2
+
3
+ export type EnumSchemaOptions<T extends null | string | number | boolean> = {
4
+ default?: T
5
+ }
2
6
 
3
7
  export class EnumSchema<
4
8
  Output extends null | string | number | boolean = any,
5
- > extends Validator<Output> {
6
- constructor(readonly values: readonly Output[]) {
9
+ > extends Schema<Output> {
10
+ constructor(
11
+ readonly values: readonly Output[],
12
+ readonly options?: EnumSchemaOptions<Output>,
13
+ ) {
7
14
  super()
8
15
  }
9
16
 
10
- override validateInContext(
11
- input: unknown,
17
+ validateInContext(
18
+ input: unknown = this.options?.default,
12
19
  ctx: ValidatorContext,
13
20
  ): ValidationResult<Output> {
14
21
  if (!(this.values as readonly unknown[]).includes(input)) {