@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,231 @@
1
+ import { CID, isPlainObject } from '@atproto/lex-data'
2
+ import { arrayAgg } from '../util/array-agg.js'
3
+ import { PropertyKey } from './property-key.js'
4
+
5
+ export interface Issue<I = unknown> {
6
+ readonly input: I
7
+ readonly code: string
8
+ readonly message?: string
9
+ readonly path: readonly PropertyKey[]
10
+ }
11
+
12
+ export interface IssueCustom extends Issue {
13
+ readonly code: 'custom'
14
+ readonly message: string
15
+ }
16
+
17
+ export interface IssueInvalidFormat extends Issue {
18
+ readonly code: 'invalid_format'
19
+ readonly format: string
20
+ }
21
+
22
+ export interface IssueInvalidType extends Issue {
23
+ readonly code: 'invalid_type'
24
+ readonly expected: readonly string[]
25
+ }
26
+
27
+ export interface IssueInvalidValue extends Issue {
28
+ readonly code: 'invalid_value'
29
+ readonly values: readonly unknown[]
30
+ }
31
+
32
+ export interface IssueRequiredKey extends Issue {
33
+ readonly code: 'required_key'
34
+ readonly key: PropertyKey
35
+ }
36
+
37
+ export interface IssueTooBig extends Issue {
38
+ readonly code: 'too_big'
39
+ readonly maximum: number
40
+ readonly type: 'array' | 'string' | 'integer' | 'grapheme' | 'bytes' | 'blob'
41
+ readonly actual: number
42
+ }
43
+
44
+ export interface IssueTooSmall extends Issue {
45
+ readonly code: 'too_small'
46
+ readonly minimum: number
47
+ readonly type: 'array' | 'string' | 'integer' | 'grapheme' | 'bytes'
48
+ readonly actual: number
49
+ }
50
+
51
+ export type ValidationIssue =
52
+ | IssueCustom
53
+ | IssueInvalidFormat
54
+ | IssueInvalidType
55
+ | IssueInvalidValue
56
+ | IssueRequiredKey
57
+ | IssueTooBig
58
+ | IssueTooSmall
59
+
60
+ export function stringifyIssue(issue: ValidationIssue): string {
61
+ const pathStr = issue.path.length ? ` at ${buildJsonPath(issue.path)}` : ''
62
+
63
+ switch (issue.code) {
64
+ case 'invalid_format':
65
+ return `Invalid ${stringifyStringFormat(issue.format)} format${issue.message ? ` (${issue.message})` : ''}${pathStr} (got ${stringifyValue(issue.input)})`
66
+ case 'invalid_type':
67
+ return `Expected ${oneOf(issue.expected.map(stringifyExpectedType))} value type${pathStr} (got ${stringifyType(issue.input)})`
68
+ case 'invalid_value':
69
+ return `Expected ${oneOf(issue.values.map(stringifyValue))}${pathStr} (got ${stringifyValue(issue.input)})`
70
+ case 'required_key':
71
+ return `Missing required key "${String(issue.key)}"${pathStr}`
72
+ case 'too_big':
73
+ return `${issue.type} too big (maximum ${issue.maximum})${pathStr} (got ${issue.actual})`
74
+ case 'too_small':
75
+ return `${issue.type} too small (minimum ${issue.minimum})${pathStr} (got ${issue.actual})`
76
+ case 'custom':
77
+ return `${issue.message}${pathStr}`
78
+ default:
79
+ // @ts-expect-error fool-proofing
80
+ return `${issue.code} validation error${pathStr}`
81
+ }
82
+ }
83
+
84
+ function stringifyExpectedType(expected: string): string {
85
+ if (expected === '$typed') {
86
+ return 'an object or record which includes a "$type" property'
87
+ }
88
+
89
+ return expected
90
+ }
91
+
92
+ function buildJsonPath(path: readonly PropertyKey[]): string {
93
+ let jsonPath = '$'
94
+ for (const segment of path) {
95
+ if (typeof segment === 'number') {
96
+ jsonPath += `[${segment}]`
97
+ } else if (/^[a-zA-Z_$][a-zA-Z0-9_]*$/.test(segment as string)) {
98
+ jsonPath += `.${segment}`
99
+ } else {
100
+ jsonPath += `[${JSON.stringify(segment)}]`
101
+ }
102
+ }
103
+ return jsonPath
104
+ }
105
+
106
+ function oneOf(arr: readonly string[]): string {
107
+ if (arr.length === 0) return ''
108
+ if (arr.length === 1) return arr[0]
109
+ return `one of ${arr.slice(0, -1).join(', ')} or ${arr.at(-1)}`
110
+ }
111
+
112
+ function stringifyStringFormat(format: string): string {
113
+ switch (format) {
114
+ case 'datetime':
115
+ return 'datetime'
116
+ case 'language':
117
+ return 'language'
118
+ case 'at-identifier':
119
+ return `AT identifier`
120
+ case 'did':
121
+ return `DID`
122
+ case 'handle':
123
+ return `handle`
124
+ case 'nsid':
125
+ return `NSID`
126
+ case 'cid':
127
+ return `CID string`
128
+ case 'tid':
129
+ return `TID string`
130
+ case 'record-key':
131
+ return `record key`
132
+ default:
133
+ return format
134
+ }
135
+ }
136
+
137
+ export function stringifyType(value: unknown): string {
138
+ switch (typeof value) {
139
+ case 'object':
140
+ if (value === null) return 'null'
141
+ if (Array.isArray(value)) return 'array'
142
+ if (CID.asCID(value)) return 'cid'
143
+ if (value instanceof Date) return 'date'
144
+ if (value instanceof RegExp) return 'regexp'
145
+ if (value instanceof Map) return 'map'
146
+ if (value instanceof Set) return 'set'
147
+ return 'object'
148
+ case 'number':
149
+ if (Number.isInteger(value)) return 'integer'
150
+ if (Number.isNaN(value)) return 'NaN'
151
+ return 'float'
152
+ default:
153
+ return typeof value
154
+ }
155
+ }
156
+
157
+ export function stringifyValue(value: unknown): string {
158
+ switch (typeof value) {
159
+ case 'bigint':
160
+ return `${value}n`
161
+ case 'number':
162
+ case 'string':
163
+ case 'boolean':
164
+ return JSON.stringify(value)
165
+ case 'object':
166
+ if (Array.isArray(value)) {
167
+ return `[${stringifyArray(value, stringifyValue)}]`
168
+ }
169
+ if (isPlainObject(value)) {
170
+ return `{${stringifyArray(Object.entries(value), stringifyObjectEntry)}}`
171
+ }
172
+ // fallthrough
173
+ default:
174
+ return stringifyType(value)
175
+ }
176
+ }
177
+
178
+ function stringifyObjectEntry([key, _value]: [PropertyKey, unknown]): string {
179
+ return `${JSON.stringify(key)}: ...`
180
+ }
181
+
182
+ function stringifyArray<T>(
183
+ arr: readonly T[],
184
+ fn: (item: T) => string,
185
+ n = 2,
186
+ ): string {
187
+ return arr.slice(0, n).map(fn).join(', ') + (arr.length > n ? ', ...' : '')
188
+ }
189
+
190
+ export function aggregateIssues(issues: ValidationIssue[]): ValidationIssue[] {
191
+ // Quick path for common cases
192
+ if (issues.length <= 1) return issues
193
+ if (issues.length === 2 && issues[0].code !== issues[1].code) return issues
194
+
195
+ return [
196
+ // Aggregate invalid_type with identical paths
197
+ ...arrayAgg(
198
+ issues.filter((issue) => issue.code === 'invalid_type'),
199
+ (a, b) => comparePropertyPaths(a.path, b.path),
200
+ (issues) => ({
201
+ ...issues[0],
202
+ expected: Array.from(new Set(issues.flatMap((iss) => iss.expected))),
203
+ }),
204
+ ),
205
+ // Aggregate invalid_value with identical paths
206
+ ...arrayAgg(
207
+ issues.filter((issue) => issue.code === 'invalid_value'),
208
+ (a, b) => comparePropertyPaths(a.path, b.path),
209
+ (issues) => ({
210
+ ...issues[0],
211
+ values: Array.from(new Set(issues.flatMap((iss) => iss.values))),
212
+ }),
213
+ ),
214
+ // Pass through other issues
215
+ ...issues.filter(
216
+ (issue) =>
217
+ issue.code !== 'invalid_type' && issue.code !== 'invalid_value',
218
+ ),
219
+ ]
220
+ }
221
+
222
+ function comparePropertyPaths(
223
+ a: readonly PropertyKey[],
224
+ b: readonly PropertyKey[],
225
+ ) {
226
+ if (a.length !== b.length) return false
227
+ for (let i = 0; i < a.length; i++) {
228
+ if (a[i] !== b[i]) return false
229
+ }
230
+ return true
231
+ }
@@ -0,0 +1,361 @@
1
+ import { ResultFailure, ResultSuccess, failure, success } from '../core.js'
2
+ import { PropertyKey } from './property-key.js'
3
+ import { ValidationError } from './validation-error.js'
4
+ import {
5
+ IssueTooBig,
6
+ IssueTooSmall,
7
+ ValidationIssue,
8
+ } from './validation-issue.js'
9
+
10
+ export type ValidationSuccess<Value = any> = ResultSuccess<Value>
11
+ export type ValidationFailure = ResultFailure<ValidationError>
12
+ export type ValidationResult<Value = any> =
13
+ | ValidationSuccess<Value>
14
+ | ValidationFailure
15
+
16
+ type ValidationOptions = {
17
+ path?: PropertyKey[]
18
+
19
+ /** @default true */
20
+ allowTransform?: boolean
21
+ }
22
+
23
+ export type Infer<T extends Validator> = T['_lex']['output']
24
+
25
+ export abstract class Validator<Output = any> {
26
+ /**
27
+ * This property is used for type inference purposes and does not actually
28
+ * exist at runtime.
29
+ *
30
+ * @deprecated For internal use only (not actually deprecated)
31
+ */
32
+ _lex!: { output: Output }
33
+
34
+ readonly lexiconType?: string
35
+
36
+ /**
37
+ * @internal **INTERNAL API, DO NOT USE**.
38
+ *
39
+ * Use {@link Validator.assert assert}, {@link Validator.check check},
40
+ * {@link Validator.parse parse} or {@link Validator.validate validate}
41
+ * instead.
42
+ *
43
+ * This method is implemented by subclasses to perform transformation and
44
+ * validation of the input value. Do not call this method directly; as the
45
+ * {@link ValidatorContext.options.allowTransform} option will **not** be
46
+ * enforced. See {@link ValidatorContext.validate} for details. When
47
+ * delegating validation from one validator sub-class implementation to
48
+ * another schema, {@link ValidatorContext.validate} should be used instead
49
+ * of calling {@link Validator.validateInContext}. This will allow to stop the
50
+ * validation process if the value was transformed (by the other schema) but
51
+ * transformations are not allowed.
52
+ *
53
+ * By convention, the {@link ValidationResult} must return the original input
54
+ * value if validation was successful and no transformation was applied (i.e.
55
+ * the input already conformed to the schema). If a default value, or any
56
+ * other transformation was applied, the returned value c&an be different from
57
+ * the input.
58
+ *
59
+ * This convention allows the {@link Validator.check check} and
60
+ * {@link Validator.assert assert} methods to check whether the input value
61
+ * exactly matches the schema (without defaults or transformations), by
62
+ * checking if the returned value is strictly equal to the input.
63
+ *
64
+ * @see {@link ValidatorContext.validate}
65
+ */
66
+ abstract validateInContext(
67
+ input: unknown,
68
+ ctx: ValidatorContext,
69
+ ): ValidationResult<Output>
70
+
71
+ assert(input: unknown): asserts input is Output {
72
+ const result = this.validate(input, { allowTransform: false })
73
+ if (!result.success) throw result.error
74
+ }
75
+
76
+ check(input: unknown): input is Output {
77
+ const result = this.validate(input, { allowTransform: false })
78
+ return result.success
79
+ }
80
+
81
+ maybe<I>(input: I): (I & Output) | undefined {
82
+ return this.check(input) ? input : undefined
83
+ }
84
+
85
+ parse<I>(
86
+ input: I,
87
+ options: ValidationOptions & { allowTransform: false },
88
+ ): I & Output
89
+ parse(input: unknown, options?: ValidationOptions): Output
90
+ parse(input: unknown, options?: ValidationOptions): Output {
91
+ const result = ValidatorContext.validate(input, this, options)
92
+ if (!result.success) throw result.error
93
+ return result.value
94
+ }
95
+
96
+ validate<I>(
97
+ input: I,
98
+ options: ValidationOptions & { allowTransform: false },
99
+ ): ValidationResult<I & Output>
100
+ validate(
101
+ input: unknown,
102
+ options?: ValidationOptions,
103
+ ): ValidationResult<Output>
104
+ validate(
105
+ input: unknown,
106
+ options?: ValidationOptions,
107
+ ): ValidationResult<Output> {
108
+ return ValidatorContext.validate(input, this, options)
109
+ }
110
+
111
+ // @NOTE The built lexicons namespaces will export utility functions that
112
+ // allow accessing the schema's methods without the need to specify ".main."
113
+ // as part of the namespace. This way, a utility for a particular record type
114
+ // can be called like "app.bsky.feed.post.<utility>()" instead of
115
+ // "app.bsky.feed.post.main.<utility>()". Because those utilities could
116
+ // conflict with other schemas (e.g. if there is a lexicon definition at
117
+ // "#<utility>"), those exported utilities will be prefixed with "$". In order
118
+ // to be able to consistently call the utilities, when using the "main" and
119
+ // non "main" definitions, we also expose the same methods with a "$" prefix.
120
+ // Thanks to this, both of the following call will be possible:
121
+ //
122
+ // - "app.bsky.feed.post.$parse(...)" // calls a utility function created by "lex build"
123
+ // - "app.bsky.feed.defs.postView.$parse(...)" // uses the alias defined below on the schema instance
124
+
125
+ $assert(input: unknown): asserts input is Output {
126
+ return this.assert(input)
127
+ }
128
+
129
+ $check(input: unknown): input is Output {
130
+ return this.check(input)
131
+ }
132
+
133
+ $maybe<I>(input: I): (I & Output) | undefined {
134
+ return this.maybe(input)
135
+ }
136
+
137
+ $parse(input: unknown, options?: ValidationOptions): Output {
138
+ return this.parse(input, options)
139
+ }
140
+
141
+ $validate(
142
+ input: unknown,
143
+ options?: ValidationOptions,
144
+ ): ValidationResult<Output> {
145
+ return this.validate(input, options)
146
+ }
147
+ }
148
+
149
+ export type ContextualIssue = {
150
+ [Code in ValidationIssue['code']]: Omit<
151
+ Extract<ValidationIssue, { code: Code }>,
152
+ 'path'
153
+ > & { path?: PropertyKey | readonly PropertyKey[] }
154
+ }[ValidationIssue['code']]
155
+
156
+ const asIssue = (
157
+ { path, ...issue }: ContextualIssue,
158
+ currentPath: readonly PropertyKey[],
159
+ ): ValidationIssue & { path: PropertyKey[] } => ({
160
+ ...issue,
161
+ path: path != null ? currentPath.concat(path) : [...currentPath],
162
+ })
163
+
164
+ export class ValidatorContext {
165
+ /**
166
+ * Creates a new validation context and validates the input using the
167
+ * provided validator.
168
+ */
169
+ static validate<V>(
170
+ input: unknown,
171
+ validator: Validator<V>,
172
+ options: ValidationOptions = {},
173
+ ): ValidationResult<V> {
174
+ const context = new ValidatorContext(options)
175
+ return context.validate(input, validator)
176
+ }
177
+
178
+ private readonly currentPath: PropertyKey[]
179
+ private readonly issues: ValidationIssue[] = []
180
+
181
+ protected constructor(readonly options: ValidationOptions) {
182
+ // Create a copy because we will be mutating the array during validation.
183
+ this.currentPath = options?.path != null ? [...options.path] : []
184
+ }
185
+
186
+ get path() {
187
+ return [...this.currentPath]
188
+ }
189
+
190
+ get allowTransform() {
191
+ // Default to true
192
+ return this.options?.allowTransform !== false
193
+ }
194
+
195
+ /**
196
+ * This is basically the entry point for validation within a context. Use this
197
+ * method instead of {@link Validator.validateInContext} directly, because
198
+ * this method enforces the {@link ValidationOptions.allowTransform} option.
199
+ */
200
+ validate<V>(input: unknown, validator: Validator<V>): ValidationResult<V> {
201
+ const result = validator.validateInContext(input, this)
202
+
203
+ if (result.success) {
204
+ if (!this.allowTransform && !Object.is(result.value, input)) {
205
+ // If the value changed, it means that a default (or some other
206
+ // transformation) was applied, meaning that the original value did
207
+ // *not* match the (output) schema. When "allowTransform" is false, we
208
+ // consider this a failure.
209
+
210
+ // This check is the reason why Validator.validateInContext should not
211
+ // be used directly, and ValidatorContext.validate should be used
212
+ // instead, even when delegating validation from one validator to
213
+ // another.
214
+
215
+ // This if block comes before the next one because 'this.issues' will
216
+ // end-up being appended to the returned ValidationError (see the
217
+ // "failure" method below), resulting in a more complete error report.
218
+ return this.issueInvalidValue(input, [result.value])
219
+ }
220
+
221
+ if (this.issues.length > 0) {
222
+ // Validator returned a success but issues were added via the context.
223
+ // This means the overall validation failed.
224
+ return { success: false, error: new ValidationError(this.issues) }
225
+ }
226
+ }
227
+
228
+ return result
229
+ }
230
+
231
+ validateChild<I extends object, K extends PropertyKey & keyof I, V>(
232
+ input: I,
233
+ key: K,
234
+ validator: Validator<V>,
235
+ ): ValidationResult<V> {
236
+ // Instead of creating a new context, we just push/pop the path segment.
237
+ this.currentPath.push(key)
238
+ try {
239
+ return this.validate(input[key], validator)
240
+ } finally {
241
+ this.currentPath.length--
242
+ }
243
+ }
244
+
245
+ addIssue(issue: ContextualIssue): void {
246
+ this.issues.push(asIssue(issue, this.currentPath))
247
+ }
248
+
249
+ success<V>(value: V): ValidationResult<V> {
250
+ return success(value)
251
+ }
252
+
253
+ failure(issue: ContextualIssue): ValidationFailure {
254
+ return failure(
255
+ new ValidationError([...this.issues, asIssue(issue, this.currentPath)]),
256
+ )
257
+ }
258
+
259
+ issueInvalidValue(
260
+ input: unknown,
261
+ values: readonly unknown[],
262
+ path?: PropertyKey | readonly PropertyKey[],
263
+ ) {
264
+ return this.failure({
265
+ code: 'invalid_value',
266
+ input,
267
+ values,
268
+ path,
269
+ })
270
+ }
271
+
272
+ issueInvalidType(
273
+ input: unknown,
274
+ expected: string | readonly string[],
275
+ path?: PropertyKey | readonly PropertyKey[],
276
+ ) {
277
+ return this.failure({
278
+ code: 'invalid_type',
279
+ input,
280
+ expected: Array.isArray(expected) ? expected : [expected],
281
+ path,
282
+ })
283
+ }
284
+
285
+ issueInvalidPropertyValue<I>(
286
+ input: I,
287
+ property: keyof I & PropertyKey,
288
+ values: readonly unknown[],
289
+ ) {
290
+ return this.issueInvalidValue(input[property], values, property)
291
+ }
292
+
293
+ issueInvalidPropertyType<I>(
294
+ input: I,
295
+ property: keyof I & PropertyKey,
296
+ expected: string | readonly string[],
297
+ ) {
298
+ return this.issueInvalidType(input[property], expected, property)
299
+ }
300
+
301
+ issueRequiredKey(input: object, key: PropertyKey) {
302
+ return this.failure({
303
+ code: 'required_key',
304
+ key,
305
+ input,
306
+ path: key,
307
+ })
308
+ }
309
+
310
+ issueInvalidFormat(input: unknown, format: string, message?: string) {
311
+ return this.failure({
312
+ code: 'invalid_format',
313
+ message,
314
+ format,
315
+ input,
316
+ })
317
+ }
318
+
319
+ issueTooBig(
320
+ input: unknown,
321
+ type: IssueTooBig['type'],
322
+ maximum: number,
323
+ actual: number,
324
+ ) {
325
+ return this.failure({
326
+ code: 'too_big',
327
+ type,
328
+ maximum,
329
+ actual,
330
+ input,
331
+ })
332
+ }
333
+
334
+ issueTooSmall(
335
+ input: unknown,
336
+ type: IssueTooSmall['type'],
337
+ minimum: number,
338
+ actual: number,
339
+ ) {
340
+ return this.failure({
341
+ code: 'too_small',
342
+ type,
343
+ minimum,
344
+ actual,
345
+ input,
346
+ })
347
+ }
348
+
349
+ custom(
350
+ input: unknown,
351
+ message: string,
352
+ path?: PropertyKey | readonly PropertyKey[],
353
+ ) {
354
+ return this.failure({
355
+ code: 'custom',
356
+ input,
357
+ message,
358
+ path,
359
+ })
360
+ }
361
+ }
@@ -0,0 +1,4 @@
1
+ export * from './validation/property-key.js'
2
+ export * from './validation/validation-error.js'
3
+ export * from './validation/validation-issue.js'
4
+ export * from './validation/validator.js'