@atproto/lex-schema 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/dist/core/$type.d.ts +4 -0
  2. package/dist/core/$type.d.ts.map +1 -0
  3. package/dist/core/$type.js +7 -0
  4. package/dist/core/$type.js.map +1 -0
  5. package/dist/core/record-key.d.ts +4 -0
  6. package/dist/core/record-key.d.ts.map +1 -0
  7. package/dist/core/record-key.js +16 -0
  8. package/dist/core/record-key.js.map +1 -0
  9. package/dist/core/result.d.ts +57 -0
  10. package/dist/core/result.d.ts.map +1 -0
  11. package/dist/core/result.js +74 -0
  12. package/dist/core/result.js.map +1 -0
  13. package/dist/core/string-format.d.ts +31 -0
  14. package/dist/core/string-format.d.ts.map +1 -0
  15. package/dist/core/string-format.js +81 -0
  16. package/dist/core/string-format.js.map +1 -0
  17. package/dist/core/types.d.ts +19 -0
  18. package/dist/core/types.d.ts.map +1 -0
  19. package/dist/core/types.js +3 -0
  20. package/dist/core/types.js.map +1 -0
  21. package/dist/core.d.ts +6 -0
  22. package/dist/core.d.ts.map +1 -0
  23. package/dist/core.js +9 -0
  24. package/dist/core.js.map +1 -0
  25. package/dist/external.d.ts +86 -0
  26. package/dist/external.d.ts.map +1 -0
  27. package/dist/external.js +171 -0
  28. package/dist/external.js.map +1 -0
  29. package/dist/index.d.ts +4 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +8 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/schema/_parameters.d.ts +17 -0
  34. package/dist/schema/_parameters.d.ts.map +1 -0
  35. package/dist/schema/_parameters.js +20 -0
  36. package/dist/schema/_parameters.js.map +1 -0
  37. package/dist/schema/array.d.ts +13 -0
  38. package/dist/schema/array.d.ts.map +1 -0
  39. package/dist/schema/array.js +40 -0
  40. package/dist/schema/array.js.map +1 -0
  41. package/dist/schema/blob.d.ts +32 -0
  42. package/dist/schema/blob.d.ts.map +1 -0
  43. package/dist/schema/blob.js +40 -0
  44. package/dist/schema/blob.js.map +1 -0
  45. package/dist/schema/boolean.d.ts +11 -0
  46. package/dist/schema/boolean.d.ts.map +1 -0
  47. package/dist/schema/boolean.js +20 -0
  48. package/dist/schema/boolean.js.map +1 -0
  49. package/dist/schema/bytes.d.ts +12 -0
  50. package/dist/schema/bytes.d.ts.map +1 -0
  51. package/dist/schema/bytes.js +31 -0
  52. package/dist/schema/bytes.js.map +1 -0
  53. package/dist/schema/cid.d.ts +13 -0
  54. package/dist/schema/cid.d.ts.map +1 -0
  55. package/dist/schema/cid.js +22 -0
  56. package/dist/schema/cid.js.map +1 -0
  57. package/dist/schema/custom.d.ts +15 -0
  58. package/dist/schema/custom.d.ts.map +1 -0
  59. package/dist/schema/custom.js +22 -0
  60. package/dist/schema/custom.js.map +1 -0
  61. package/dist/schema/dict.d.ts +18 -0
  62. package/dist/schema/dict.d.ts.map +1 -0
  63. package/dist/schema/dict.js +48 -0
  64. package/dist/schema/dict.js.map +1 -0
  65. package/dist/schema/discriminated-union.d.ts +34 -0
  66. package/dist/schema/discriminated-union.d.ts.map +1 -0
  67. package/dist/schema/discriminated-union.js +93 -0
  68. package/dist/schema/discriminated-union.js.map +1 -0
  69. package/dist/schema/enum.d.ts +7 -0
  70. package/dist/schema/enum.d.ts.map +1 -0
  71. package/dist/schema/enum.js +19 -0
  72. package/dist/schema/enum.js.map +1 -0
  73. package/dist/schema/integer.d.ts +13 -0
  74. package/dist/schema/integer.d.ts.map +1 -0
  75. package/dist/schema/integer.js +32 -0
  76. package/dist/schema/integer.js.map +1 -0
  77. package/dist/schema/intersection.d.ts +16 -0
  78. package/dist/schema/intersection.d.ts.map +1 -0
  79. package/dist/schema/intersection.js +33 -0
  80. package/dist/schema/intersection.js.map +1 -0
  81. package/dist/schema/literal.d.ts +7 -0
  82. package/dist/schema/literal.d.ts.map +1 -0
  83. package/dist/schema/literal.js +19 -0
  84. package/dist/schema/literal.js.map +1 -0
  85. package/dist/schema/never.d.ts +5 -0
  86. package/dist/schema/never.d.ts.map +1 -0
  87. package/dist/schema/never.js +11 -0
  88. package/dist/schema/never.js.map +1 -0
  89. package/dist/schema/null.d.ts +7 -0
  90. package/dist/schema/null.d.ts.map +1 -0
  91. package/dist/schema/null.js +18 -0
  92. package/dist/schema/null.js.map +1 -0
  93. package/dist/schema/object.d.ts +47 -0
  94. package/dist/schema/object.d.ts.map +1 -0
  95. package/dist/schema/object.js +89 -0
  96. package/dist/schema/object.js.map +1 -0
  97. package/dist/schema/params.d.ts +22 -0
  98. package/dist/schema/params.d.ts.map +1 -0
  99. package/dist/schema/params.js +115 -0
  100. package/dist/schema/params.js.map +1 -0
  101. package/dist/schema/payload.d.ts +19 -0
  102. package/dist/schema/payload.d.ts.map +1 -0
  103. package/dist/schema/payload.js +16 -0
  104. package/dist/schema/payload.js.map +1 -0
  105. package/dist/schema/permission-set.d.ts +15 -0
  106. package/dist/schema/permission-set.d.ts.map +1 -0
  107. package/dist/schema/permission-set.js +16 -0
  108. package/dist/schema/permission-set.js.map +1 -0
  109. package/dist/schema/permission.d.ts +9 -0
  110. package/dist/schema/permission.d.ts.map +1 -0
  111. package/dist/schema/permission.js +14 -0
  112. package/dist/schema/permission.js.map +1 -0
  113. package/dist/schema/procedure.d.ts +17 -0
  114. package/dist/schema/procedure.d.ts.map +1 -0
  115. package/dist/schema/procedure.js +20 -0
  116. package/dist/schema/procedure.js.map +1 -0
  117. package/dist/schema/query.d.ts +15 -0
  118. package/dist/schema/query.d.ts.map +1 -0
  119. package/dist/schema/query.js +18 -0
  120. package/dist/schema/query.js.map +1 -0
  121. package/dist/schema/record.d.ts +37 -0
  122. package/dist/schema/record.d.ts.map +1 -0
  123. package/dist/schema/record.js +64 -0
  124. package/dist/schema/record.js.map +1 -0
  125. package/dist/schema/ref.d.ts +10 -0
  126. package/dist/schema/ref.d.ts.map +1 -0
  127. package/dist/schema/ref.js +36 -0
  128. package/dist/schema/ref.js.map +1 -0
  129. package/dist/schema/string.d.ts +24 -0
  130. package/dist/schema/string.d.ts.map +1 -0
  131. package/dist/schema/string.js +107 -0
  132. package/dist/schema/string.js.map +1 -0
  133. package/dist/schema/subscription.d.ts +16 -0
  134. package/dist/schema/subscription.d.ts.map +1 -0
  135. package/dist/schema/subscription.js +18 -0
  136. package/dist/schema/subscription.js.map +1 -0
  137. package/dist/schema/token.d.ts +10 -0
  138. package/dist/schema/token.d.ts.map +1 -0
  139. package/dist/schema/token.js +36 -0
  140. package/dist/schema/token.js.map +1 -0
  141. package/dist/schema/typed-object.d.ts +32 -0
  142. package/dist/schema/typed-object.d.ts.map +1 -0
  143. package/dist/schema/typed-object.js +40 -0
  144. package/dist/schema/typed-object.js.map +1 -0
  145. package/dist/schema/typed-ref.d.ts +30 -0
  146. package/dist/schema/typed-ref.d.ts.map +1 -0
  147. package/dist/schema/typed-ref.js +44 -0
  148. package/dist/schema/typed-ref.js.map +1 -0
  149. package/dist/schema/typed-union.d.ts +26 -0
  150. package/dist/schema/typed-union.d.ts.map +1 -0
  151. package/dist/schema/typed-union.js +54 -0
  152. package/dist/schema/typed-union.js.map +1 -0
  153. package/dist/schema/union.d.ts +9 -0
  154. package/dist/schema/union.d.ts.map +1 -0
  155. package/dist/schema/union.js +29 -0
  156. package/dist/schema/union.js.map +1 -0
  157. package/dist/schema/unknown-object.d.ts +9 -0
  158. package/dist/schema/unknown-object.d.ts.map +1 -0
  159. package/dist/schema/unknown-object.js +16 -0
  160. package/dist/schema/unknown-object.js.map +1 -0
  161. package/dist/schema/unknown.d.ts +5 -0
  162. package/dist/schema/unknown.d.ts.map +1 -0
  163. package/dist/schema/unknown.js +11 -0
  164. package/dist/schema/unknown.js.map +1 -0
  165. package/dist/schema.d.ts +34 -0
  166. package/dist/schema.d.ts.map +1 -0
  167. package/dist/schema.js +41 -0
  168. package/dist/schema.js.map +1 -0
  169. package/dist/util/array-agg.d.ts +20 -0
  170. package/dist/util/array-agg.d.ts.map +1 -0
  171. package/dist/util/array-agg.js +42 -0
  172. package/dist/util/array-agg.js.map +1 -0
  173. package/dist/validation/property-key.d.ts +2 -0
  174. package/dist/validation/property-key.d.ts.map +1 -0
  175. package/dist/validation/property-key.js +3 -0
  176. package/dist/validation/property-key.js.map +1 -0
  177. package/dist/validation/validation-error.d.ts +9 -0
  178. package/dist/validation/validation-error.d.ts.map +1 -0
  179. package/dist/validation/validation-error.js +27 -0
  180. package/dist/validation/validation-error.js.map +1 -0
  181. package/dist/validation/validation-issue.d.ts +45 -0
  182. package/dist/validation/validation-issue.d.ts.map +1 -0
  183. package/dist/validation/validation-issue.js +167 -0
  184. package/dist/validation/validation-issue.js.map +1 -0
  185. package/dist/validation/validator.d.ts +113 -0
  186. package/dist/validation/validator.d.ts.map +1 -0
  187. package/dist/validation/validator.js +209 -0
  188. package/dist/validation/validator.js.map +1 -0
  189. package/dist/validation.d.ts +5 -0
  190. package/dist/validation.d.ts.map +1 -0
  191. package/dist/validation.js +8 -0
  192. package/dist/validation.js.map +1 -0
  193. package/package.json +45 -0
  194. package/src/core/$type.ts +19 -0
  195. package/src/core/record-key.ts +15 -0
  196. package/src/core/result.ts +73 -0
  197. package/src/core/string-format.ts +124 -0
  198. package/src/core/types.ts +22 -0
  199. package/src/core.ts +5 -0
  200. package/src/external.ts +365 -0
  201. package/src/index.ts +3 -0
  202. package/src/schema/_parameters.ts +26 -0
  203. package/src/schema/array.ts +51 -0
  204. package/src/schema/blob.ts +82 -0
  205. package/src/schema/boolean.ts +24 -0
  206. package/src/schema/bytes.ts +38 -0
  207. package/src/schema/cid.ts +27 -0
  208. package/src/schema/custom.ts +36 -0
  209. package/src/schema/dict.ts +69 -0
  210. package/src/schema/discriminated-union.ts +144 -0
  211. package/src/schema/enum.ts +20 -0
  212. package/src/schema/integer.ts +41 -0
  213. package/src/schema/intersection.ts +57 -0
  214. package/src/schema/literal.ts +20 -0
  215. package/src/schema/never.ts +14 -0
  216. package/src/schema/null.ts +20 -0
  217. package/src/schema/object.test.ts +138 -0
  218. package/src/schema/object.ts +180 -0
  219. package/src/schema/params.ts +157 -0
  220. package/src/schema/payload.ts +53 -0
  221. package/src/schema/permission-set.ts +22 -0
  222. package/src/schema/permission.ts +15 -0
  223. package/src/schema/procedure.ts +35 -0
  224. package/src/schema/query.ts +28 -0
  225. package/src/schema/record.ts +106 -0
  226. package/src/schema/ref.ts +47 -0
  227. package/src/schema/string.ts +139 -0
  228. package/src/schema/subscription.ts +35 -0
  229. package/src/schema/token.ts +41 -0
  230. package/src/schema/typed-object.ts +64 -0
  231. package/src/schema/typed-ref.ts +68 -0
  232. package/src/schema/typed-union.ts +106 -0
  233. package/src/schema/union.ts +40 -0
  234. package/src/schema/unknown-object.ts +20 -0
  235. package/src/schema/unknown.ts +10 -0
  236. package/src/schema.ts +40 -0
  237. package/src/util/array-agg.test.ts +41 -0
  238. package/src/util/array-agg.ts +43 -0
  239. package/src/validation/property-key.ts +1 -0
  240. package/src/validation/validation-error.ts +32 -0
  241. package/src/validation/validation-issue.ts +231 -0
  242. package/src/validation/validator.ts +361 -0
  243. package/src/validation.ts +4 -0
@@ -0,0 +1,38 @@
1
+ import { asUint8Array } from '@atproto/lex-data'
2
+ import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
3
+
4
+ export type BytesSchemaOptions = {
5
+ minLength?: number
6
+ maxLength?: number
7
+ }
8
+
9
+ export class BytesSchema extends Validator<Uint8Array> {
10
+ readonly lexiconType = 'bytes' as const
11
+
12
+ constructor(readonly options: BytesSchemaOptions) {
13
+ super()
14
+ }
15
+
16
+ override validateInContext(
17
+ input: unknown,
18
+ ctx: ValidatorContext,
19
+ ): ValidationResult<Uint8Array> {
20
+ // Coerce different binary formats into Uint8Array
21
+ const bytes = asUint8Array(input)
22
+ if (!bytes) {
23
+ return ctx.issueInvalidType(input, 'bytes')
24
+ }
25
+
26
+ const { minLength } = this.options
27
+ if (minLength != null && bytes.length < minLength) {
28
+ return ctx.issueTooSmall(bytes, 'bytes', minLength, bytes.length)
29
+ }
30
+
31
+ const { maxLength } = this.options
32
+ if (maxLength != null && bytes.length > maxLength) {
33
+ return ctx.issueTooBig(bytes, 'bytes', maxLength, bytes.length)
34
+ }
35
+
36
+ return ctx.success(bytes)
37
+ }
38
+ }
@@ -0,0 +1,27 @@
1
+ import { CID, isCid } from '@atproto/lex-data'
2
+ import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
3
+
4
+ export { CID }
5
+
6
+ export type CidSchemaOptions = {
7
+ strict?: boolean
8
+ }
9
+
10
+ export class CidSchema extends Validator<CID> {
11
+ readonly lexiconType = 'cid-link' as const
12
+
13
+ constructor(readonly options: CidSchemaOptions = {}) {
14
+ super()
15
+ }
16
+
17
+ override validateInContext(
18
+ input: unknown,
19
+ ctx: ValidatorContext,
20
+ ): ValidationResult<CID> {
21
+ if (!isCid(input, this.options)) {
22
+ return ctx.issueInvalidType(input, 'cid')
23
+ }
24
+
25
+ return ctx.success(input)
26
+ }
27
+ }
@@ -0,0 +1,36 @@
1
+ import { PropertyKey } from '../validation/property-key.js'
2
+ import {
3
+ ContextualIssue,
4
+ ValidationResult,
5
+ Validator,
6
+ ValidatorContext,
7
+ } from '../validation/validator.js'
8
+
9
+ export type CustomAssertionContext = {
10
+ path: PropertyKey[]
11
+ addIssue(issue: ContextualIssue): void
12
+ }
13
+
14
+ export type CustomAssertion<T = any> = (
15
+ this: null,
16
+ input: unknown,
17
+ ctx: CustomAssertionContext,
18
+ ) => input is T
19
+
20
+ export class CustomSchema<T = unknown> extends Validator<T> {
21
+ constructor(
22
+ private readonly assertion: CustomAssertion<T>,
23
+ private readonly message: string,
24
+ private readonly path?: PropertyKey | readonly PropertyKey[],
25
+ ) {
26
+ super()
27
+ }
28
+
29
+ override validateInContext(
30
+ input: unknown,
31
+ ctx: ValidatorContext,
32
+ ): ValidationResult<T> {
33
+ if (this.assertion.call(null, input, ctx)) return ctx.success(input as T)
34
+ return ctx.custom(input, this.message, this.path)
35
+ }
36
+ }
@@ -0,0 +1,69 @@
1
+ import { isPlainObject } from '@atproto/lex-data'
2
+ import {
3
+ Infer,
4
+ ValidationResult,
5
+ Validator,
6
+ ValidatorContext,
7
+ } from '../validation.js'
8
+
9
+ export type DictSchemaOutput<
10
+ KeySchema extends Validator,
11
+ ValueSchema extends Validator,
12
+ > =
13
+ Infer<KeySchema> extends never
14
+ ? Record<string, never>
15
+ : Record<Infer<KeySchema> & string, Infer<ValueSchema>>
16
+
17
+ /**
18
+ * @note There is no dictionary in Lexicon schemas. This is a custom extension
19
+ * to allow map-like objects when using the lex library programmatically (i.e.
20
+ * not code generated from a lexicon schema).
21
+ */
22
+ export class DictSchema<
23
+ const KeySchema extends Validator = any,
24
+ const ValueSchema extends Validator = any,
25
+ > extends Validator<DictSchemaOutput<KeySchema, ValueSchema>> {
26
+ constructor(
27
+ readonly keySchema: KeySchema,
28
+ readonly valueSchema: ValueSchema,
29
+ ) {
30
+ super()
31
+ }
32
+
33
+ override validateInContext(
34
+ input: unknown,
35
+ ctx: ValidatorContext,
36
+ options?: { ignoredKeys?: { has(k: string): boolean } },
37
+ ): ValidationResult<DictSchemaOutput<KeySchema, ValueSchema>> {
38
+ if (!isPlainObject(input)) {
39
+ return ctx.issueInvalidType(input, 'dict')
40
+ }
41
+
42
+ let copy: undefined | Record<string, unknown>
43
+
44
+ for (const key in input) {
45
+ if (options?.ignoredKeys?.has(key)) continue
46
+
47
+ const keyResult = ctx.validate(key, this.keySchema)
48
+ if (!keyResult.success) return keyResult
49
+ if (keyResult.value !== key) {
50
+ // We can't safely "move" the key to a different name in the output
51
+ // object (because there may already be something there), so we issue a
52
+ // "required key" error if the key validation changes the key
53
+ return ctx.issueRequiredKey(input, key)
54
+ }
55
+
56
+ const valueResult = ctx.validateChild(input, key, this.valueSchema)
57
+ if (!valueResult.success) return valueResult
58
+
59
+ if (valueResult.value !== input[key]) {
60
+ copy ??= { ...input }
61
+ copy[key] = valueResult.value
62
+ }
63
+ }
64
+
65
+ return ctx.success(
66
+ (copy ?? input) as DictSchemaOutput<KeySchema, ValueSchema>,
67
+ )
68
+ }
69
+ }
@@ -0,0 +1,144 @@
1
+ import { isPlainObject } from '@atproto/lex-data'
2
+ import { ArrayContaining } from '../core.js'
3
+ import {
4
+ ValidationError,
5
+ ValidationFailure,
6
+ ValidationResult,
7
+ Validator,
8
+ ValidatorContext,
9
+ } from '../validation.js'
10
+ import { EnumSchema } from './enum.js'
11
+ import { LiteralSchema } from './literal.js'
12
+ import { ObjectSchema } from './object.js'
13
+
14
+ export type DiscriminatedUnionSchemaVariant<Discriminator extends string> =
15
+ ObjectSchema<
16
+ { [_ in Discriminator]: Validator },
17
+ { required: ArrayContaining<Discriminator, string> }
18
+ >
19
+
20
+ export type DiscriminatedUnionSchemaVariants<Discriminator extends string> =
21
+ readonly [
22
+ DiscriminatedUnionSchemaVariant<Discriminator>,
23
+ ...DiscriminatedUnionSchemaVariant<Discriminator>[],
24
+ ]
25
+
26
+ 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
36
+
37
+ /**
38
+ * @note There is no discriminated union in Lexicon schemas. This is a custom
39
+ * extension to allow optimized validation of union of objects when using the
40
+ * lex library programmatically (i.e. not code generated from a lexicon schema).
41
+ */
42
+ export class DiscriminatedUnionSchema<
43
+ const Discriminator extends string = any,
44
+ const Options extends DiscriminatedUnionSchemaVariants<Discriminator> = any,
45
+ > extends Validator<DiscriminatedUnionSchemaOutput<Options>> {
46
+ constructor(
47
+ readonly discriminator: Discriminator,
48
+ readonly variants: Options,
49
+ ) {
50
+ 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
+
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
87
+ }
88
+
89
+ override validateInContext(
90
+ input: unknown,
91
+ ctx: ValidatorContext,
92
+ ): ValidationResult<DiscriminatedUnionSchemaOutput<Options>> {
93
+ if (!isPlainObject(input)) {
94
+ return ctx.issueInvalidType(input, 'object')
95
+ }
96
+
97
+ if (!Object.hasOwn(input, this.discriminator)) {
98
+ return ctx.issueRequiredKey(input, this.discriminator)
99
+ }
100
+
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
+ }
111
+
112
+ return ctx.validate(input, variant) as ValidationResult<
113
+ DiscriminatedUnionSchemaOutput<Options>
114
+ >
115
+ }
116
+
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
+ )
128
+
129
+ if (!discResult.success) {
130
+ failures.push(discResult)
131
+ continue
132
+ }
133
+
134
+ return ctx.validate(input, variant) as ValidationResult<
135
+ DiscriminatedUnionSchemaOutput<Options>
136
+ >
137
+ }
138
+
139
+ return {
140
+ success: false,
141
+ error: ValidationError.fromFailures(failures),
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,20 @@
1
+ import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
2
+
3
+ export class EnumSchema<
4
+ Output extends null | string | number | boolean = any,
5
+ > extends Validator<Output> {
6
+ constructor(readonly values: readonly Output[]) {
7
+ super()
8
+ }
9
+
10
+ override validateInContext(
11
+ input: unknown,
12
+ ctx: ValidatorContext,
13
+ ): ValidationResult<Output> {
14
+ if (!(this.values as readonly unknown[]).includes(input)) {
15
+ return ctx.issueInvalidValue(input, this.values)
16
+ }
17
+
18
+ return ctx.success(input as Output)
19
+ }
20
+ }
@@ -0,0 +1,41 @@
1
+ import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
2
+
3
+ export type IntegerSchemaOptions = {
4
+ default?: number
5
+ minimum?: number
6
+ maximum?: number
7
+ }
8
+
9
+ export class IntegerSchema extends Validator<number> {
10
+ readonly lexiconType = 'integer' as const
11
+
12
+ constructor(readonly options: IntegerSchemaOptions) {
13
+ super()
14
+ }
15
+
16
+ override validateInContext(
17
+ input: unknown = this.options.default,
18
+ ctx: ValidatorContext,
19
+ ): ValidationResult<number> {
20
+ if (!isInteger(input)) {
21
+ return ctx.issueInvalidType(input, 'integer')
22
+ }
23
+
24
+ if (this.options.minimum !== undefined && input < this.options.minimum) {
25
+ return ctx.issueTooSmall(input, 'integer', this.options.minimum, input)
26
+ }
27
+
28
+ if (this.options.maximum !== undefined && input > this.options.maximum) {
29
+ return ctx.issueTooBig(input, 'integer', this.options.maximum, input)
30
+ }
31
+
32
+ return ctx.success(input)
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Simple wrapper around {@link Number.isInteger} that acts as a type guard.
38
+ */
39
+ function isInteger(input: unknown): input is number {
40
+ return Number.isInteger(input)
41
+ }
@@ -0,0 +1,57 @@
1
+ import {
2
+ Infer,
3
+ ValidationResult,
4
+ Validator,
5
+ ValidatorContext,
6
+ } from '../validation.js'
7
+
8
+ export type IntersectionSchemaValidators = readonly [
9
+ Validator,
10
+ Validator,
11
+ ...Validator[],
12
+ ]
13
+ export type IntersectionSchemaOutput<
14
+ V extends readonly Validator[],
15
+ Base = unknown,
16
+ > = V extends readonly [
17
+ infer First extends Validator,
18
+ ...infer Rest extends Validator[],
19
+ ]
20
+ ? IntersectionSchemaOutput<Rest, Base & Infer<First>>
21
+ : Base
22
+
23
+ export class IntersectionSchema<
24
+ V extends IntersectionSchemaValidators = any,
25
+ > extends Validator<IntersectionSchemaOutput<V>> {
26
+ constructor(protected readonly validators: V) {
27
+ super()
28
+ }
29
+
30
+ override validateInContext(
31
+ input: unknown,
32
+ ctx: ValidatorContext,
33
+ ): ValidationResult<IntersectionSchemaOutput<V>> {
34
+ for (let i = 0; i < this.validators.length; i++) {
35
+ const result = ctx.validate(input, this.validators[i])
36
+
37
+ if (!result.success) {
38
+ return result
39
+ }
40
+
41
+ // @NOTE because transforming the value could make it invalid for previous
42
+ // validators, we need to ensure the input remains unchanged only gets
43
+ // transformed by the first validator.
44
+ if (i !== 0 && input !== result.value) {
45
+ // The alternative would be to allow transforms on a first pass
46
+ // (ignoring errors) and then re-validate the final value against all
47
+ // validators (without allowing further transforms). This would be way
48
+ // less efficient (we could make this optional).
49
+ return ctx.issueInvalidValue(input, [result.value])
50
+ }
51
+
52
+ input = result.value
53
+ }
54
+
55
+ return ctx.success(input as IntersectionSchemaOutput<V>)
56
+ }
57
+ }
@@ -0,0 +1,20 @@
1
+ import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
2
+
3
+ export class LiteralSchema<
4
+ Output extends null | string | number | boolean = any,
5
+ > extends Validator<Output> {
6
+ constructor(readonly value: Output) {
7
+ super()
8
+ }
9
+
10
+ override validateInContext(
11
+ input: unknown,
12
+ ctx: ValidatorContext,
13
+ ): ValidationResult<Output> {
14
+ if (input !== this.value) {
15
+ return ctx.issueInvalidValue(input, [this.value])
16
+ }
17
+
18
+ return ctx.success(this.value)
19
+ }
20
+ }
@@ -0,0 +1,14 @@
1
+ import {
2
+ ValidationFailure,
3
+ Validator,
4
+ ValidatorContext,
5
+ } from '../validation.js'
6
+
7
+ export class NeverSchema extends Validator<never> {
8
+ override validateInContext(
9
+ input: unknown,
10
+ ctx: ValidatorContext,
11
+ ): ValidationFailure {
12
+ return ctx.issueInvalidType(input, 'never')
13
+ }
14
+ }
@@ -0,0 +1,20 @@
1
+ import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
2
+
3
+ export class NullSchema extends Validator<null> {
4
+ readonly lexiconType = 'null' as const
5
+
6
+ constructor() {
7
+ super()
8
+ }
9
+
10
+ override validateInContext(
11
+ input: unknown,
12
+ ctx: ValidatorContext,
13
+ ): ValidationResult<null> {
14
+ if (input !== null) {
15
+ return ctx.issueInvalidType(input, 'null')
16
+ }
17
+
18
+ return ctx.success(null)
19
+ }
20
+ }
@@ -0,0 +1,138 @@
1
+ import { BooleanSchema } from './boolean.js'
2
+ import { DictSchema } from './dict.js'
3
+ import { EnumSchema } from './enum.js'
4
+ import { IntegerSchema } from './integer.js'
5
+ import { ObjectSchema } from './object.js'
6
+ import { StringSchema } from './string.js'
7
+
8
+ describe('ObjectSchema', () => {
9
+ describe('simple schema', () => {
10
+ const schema = new ObjectSchema(
11
+ {
12
+ name: new StringSchema({}),
13
+ age: new IntegerSchema({}),
14
+ gender: new EnumSchema(['male', 'female']),
15
+ },
16
+ {
17
+ required: ['name'],
18
+ nullable: ['gender'],
19
+ },
20
+ )
21
+
22
+ it('validates plain objects', () => {
23
+ const result = schema.validate({
24
+ name: 'Alice',
25
+ age: 30,
26
+ gender: 'female',
27
+ })
28
+ expect(result.success).toBe(true)
29
+ })
30
+
31
+ it('rejects non-objects', () => {
32
+ const result = schema.validate('not an object')
33
+ expect(result.success).toBe(false)
34
+ })
35
+
36
+ it('rejects missing properties', () => {
37
+ const result = schema.validate({
38
+ age: 30,
39
+ gender: 'female',
40
+ })
41
+ expect(result.success).toBe(false)
42
+ })
43
+
44
+ it('validates optional properties', () => {
45
+ const result = schema.validate({
46
+ name: 'Alice',
47
+ })
48
+ expect(result.success).toBe(true)
49
+ })
50
+
51
+ it('validates nullable properties', () => {
52
+ const result = schema.validate({
53
+ name: 'Alice',
54
+ gender: null,
55
+ })
56
+ expect(result.success).toBe(true)
57
+ })
58
+
59
+ it('rejects invalid property types', () => {
60
+ const result = schema.validate({
61
+ name: 'Alice',
62
+ age: 'thirty',
63
+ })
64
+ expect(result.success).toBe(false)
65
+ })
66
+
67
+ it('ignores extra properties', () => {
68
+ const result = schema.validate({
69
+ name: 'Alice',
70
+ age: 30,
71
+ extra: 'value',
72
+ })
73
+ expect(result.success).toBe(true)
74
+ })
75
+ })
76
+
77
+ describe('strict schema', () => {
78
+ const schema = new ObjectSchema(
79
+ {
80
+ id: new StringSchema({}),
81
+ score: new IntegerSchema({}),
82
+ },
83
+ {
84
+ required: ['id', 'score'],
85
+ unknownProperties: 'strict',
86
+ },
87
+ )
88
+
89
+ it('rejects extra properties in strict mode', () => {
90
+ const result = schema.validate({
91
+ id: 'item1',
92
+ score: 100,
93
+ extra: 'not allowed',
94
+ })
95
+ expect(result.success).toBe(false)
96
+ })
97
+
98
+ it('accepts only defined properties in strict mode', () => {
99
+ const result = schema.validate({
100
+ id: 'item1',
101
+ score: 100,
102
+ })
103
+ expect(result.success).toBe(true)
104
+ })
105
+ })
106
+
107
+ describe('schema with unknownProperties validator', () => {
108
+ const schema = new ObjectSchema(
109
+ {
110
+ title: new StringSchema({}),
111
+ },
112
+ {
113
+ required: ['title'],
114
+ unknownProperties: new DictSchema(
115
+ new EnumSchema(['tag1', 'tag2']),
116
+ new BooleanSchema({}),
117
+ ),
118
+ },
119
+ )
120
+
121
+ it('validates extra properties with the provided validator', () => {
122
+ const result = schema.validate({
123
+ title: 'My Post',
124
+ tag1: true,
125
+ tag2: false,
126
+ })
127
+ expect(result.success).toBe(true)
128
+ })
129
+
130
+ it('rejects extra properties that fail the provided validator', () => {
131
+ const result = schema.validate({
132
+ title: 'My Post',
133
+ tag1: 'not a boolean',
134
+ })
135
+ expect(result.success).toBe(false)
136
+ })
137
+ })
138
+ })