@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,139 @@
1
+ import { CID, graphemeLen, utf8Len } from '@atproto/lex-data'
2
+ import {
3
+ InferStringFormat,
4
+ StringFormat,
5
+ UnknownString,
6
+ assertStringFormat,
7
+ } from '../core.js'
8
+ import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
9
+ import { TokenSchema } from './token.js'
10
+
11
+ export type StringSchemaOptions = {
12
+ default?: string
13
+ knownValues?: readonly string[]
14
+ format?: StringFormat
15
+ minLength?: number
16
+ maxLength?: number
17
+ minGraphemes?: number
18
+ maxGraphemes?: number
19
+ }
20
+
21
+ export type StringSchemaOutput<Options> =
22
+ //
23
+ Options extends { format: infer F extends StringFormat }
24
+ ? InferStringFormat<F>
25
+ : Options extends { knownValues: readonly (infer K extends string)[] }
26
+ ? K | UnknownString
27
+ : string
28
+
29
+ export class StringSchema<
30
+ const Options extends StringSchemaOptions = any,
31
+ > extends Validator<StringSchemaOutput<Options>> {
32
+ readonly lexiconType = 'string' as const
33
+
34
+ constructor(readonly options: Options) {
35
+ super()
36
+ }
37
+
38
+ override validateInContext(
39
+ // @NOTE validation will be applied on the default value as well
40
+ input: unknown = this.options.default,
41
+ ctx: ValidatorContext,
42
+ ): ValidationResult<StringSchemaOutput<Options>> {
43
+ const { options } = this
44
+
45
+ const str = coerceToString(input)
46
+ if (str == null) {
47
+ return ctx.issueInvalidType(input, 'string')
48
+ }
49
+
50
+ let lazyUtf8Len: number
51
+
52
+ const { minLength } = options
53
+ if (minLength != null) {
54
+ if ((lazyUtf8Len ??= utf8Len(str)) < minLength) {
55
+ return ctx.issueTooSmall(str, 'string', minLength, lazyUtf8Len)
56
+ }
57
+ }
58
+
59
+ const { maxLength } = options
60
+ if (maxLength != null) {
61
+ // Optimization: we can avoid computing the UTF-8 length if the maximum
62
+ // possible length, in bytes, of the input JS string is smaller than the
63
+ // maxLength (in UTF-8 string bytes).
64
+ if (str.length * 3 <= maxLength) {
65
+ // Input string so small it can't possibly exceed maxLength
66
+ } else if ((lazyUtf8Len ??= utf8Len(str)) > maxLength) {
67
+ return ctx.issueTooBig(str, 'string', maxLength, lazyUtf8Len)
68
+ }
69
+ }
70
+
71
+ let lazyGraphLen: number
72
+
73
+ const { minGraphemes } = options
74
+ if (minGraphemes != null) {
75
+ // Optimization: avoid counting graphemes if the length check already fails
76
+ if (str.length < minGraphemes) {
77
+ return ctx.issueTooSmall(str, 'grapheme', minGraphemes, str.length)
78
+ } else if ((lazyGraphLen ??= graphemeLen(str)) < minGraphemes) {
79
+ return ctx.issueTooSmall(str, 'grapheme', minGraphemes, lazyGraphLen)
80
+ }
81
+ }
82
+
83
+ const { maxGraphemes } = options
84
+ if (maxGraphemes != null) {
85
+ if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) {
86
+ return ctx.issueTooBig(str, 'grapheme', maxGraphemes, lazyGraphLen)
87
+ }
88
+ }
89
+
90
+ if (options.format !== undefined) {
91
+ try {
92
+ // @TODO optimize to avoid throw cost (requires re-writing utilities
93
+ // from @atproto/syntax)
94
+ assertStringFormat(str, options.format)
95
+ } catch (err) {
96
+ const message = err instanceof Error ? err.message : undefined
97
+ return ctx.issueInvalidFormat(str, options.format, message)
98
+ }
99
+ }
100
+
101
+ return ctx.success(str as StringSchemaOutput<Options>)
102
+ }
103
+ }
104
+
105
+ export function coerceToString(input: unknown): string | null {
106
+ switch (typeof input) {
107
+ case 'string':
108
+ return input
109
+ case 'object': {
110
+ if (input == null) return null
111
+
112
+ // @NOTE Allow using TokenSchema instances in places expecting strings,
113
+ // converting them to their string value.
114
+ if (input instanceof TokenSchema) {
115
+ return input.toString()
116
+ }
117
+
118
+ if (input instanceof Date) {
119
+ if (Number.isNaN(input.getTime())) return null
120
+ return input.toISOString()
121
+ }
122
+
123
+ if (input instanceof URL) {
124
+ return input.toString()
125
+ }
126
+
127
+ const cid = CID.asCID(input)
128
+ if (cid) return cid.toString()
129
+
130
+ if (input instanceof String) {
131
+ return input.valueOf()
132
+ }
133
+ }
134
+
135
+ // falls through
136
+ default:
137
+ return null
138
+ }
139
+ }
@@ -0,0 +1,35 @@
1
+ import { Infer } from '../validation.js'
2
+ import { ObjectSchema } from './object.js'
3
+ import { ParamsSchema } from './params.js'
4
+ import { RefSchema } from './ref.js'
5
+ import { TypedUnionSchema } from './typed-union.js'
6
+
7
+ export type InferSubscriptionParameters<S extends Subscription> =
8
+ S extends Subscription<any, infer P extends ParamsSchema, any>
9
+ ? Infer<P>
10
+ : never
11
+
12
+ export type InferSubscriptionMessage<S extends Subscription> =
13
+ S extends Subscription<
14
+ any,
15
+ any,
16
+ infer M extends RefSchema | TypedUnionSchema | ObjectSchema
17
+ >
18
+ ? Infer<M>
19
+ : unknown
20
+
21
+ export class Subscription<
22
+ N extends string = any,
23
+ P extends ParamsSchema = any,
24
+ M extends undefined | RefSchema | TypedUnionSchema | ObjectSchema = any,
25
+ E extends undefined | readonly string[] = any,
26
+ > {
27
+ readonly type = 'subscription' as const
28
+
29
+ constructor(
30
+ readonly nsid: N,
31
+ readonly parameters: P,
32
+ readonly message: M,
33
+ readonly errors: E,
34
+ ) {}
35
+ }
@@ -0,0 +1,41 @@
1
+ import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
2
+
3
+ export class TokenSchema<V extends string = any> extends Validator<V> {
4
+ readonly lexiconType = 'token' as const
5
+
6
+ constructor(protected readonly value: V) {
7
+ super()
8
+ }
9
+
10
+ override validateInContext(
11
+ input: unknown,
12
+ ctx: ValidatorContext,
13
+ ): ValidationResult<V> {
14
+ if (input === this.value) {
15
+ return ctx.success(this.value)
16
+ }
17
+
18
+ // @NOTE: allow using the token instance itself (but convert to the actual
19
+ // token value)
20
+ if (input instanceof TokenSchema && input.value === this.value) {
21
+ return ctx.success(this.value)
22
+ }
23
+
24
+ if (typeof input !== 'string') {
25
+ return ctx.issueInvalidType(input, 'token')
26
+ }
27
+
28
+ return ctx.issueInvalidValue(input, [this.value])
29
+ }
30
+
31
+ // When using the TokenSchema instance as data, let's serialize it to the
32
+ // token value
33
+
34
+ toJSON(): string {
35
+ return this.value
36
+ }
37
+
38
+ toString(): string {
39
+ return this.value
40
+ }
41
+ }
@@ -0,0 +1,64 @@
1
+ import { isPlainObject } from '@atproto/lex-data'
2
+ import { $Type, Simplify } from '../core.js'
3
+ import {
4
+ Infer,
5
+ ValidationResult,
6
+ Validator,
7
+ ValidatorContext,
8
+ } from '../validation.js'
9
+
10
+ export class TypedObjectSchema<
11
+ Type extends $Type = any,
12
+ Schema extends Validator<Record<string, unknown>> = any,
13
+ Output extends Infer<Schema> & { $type?: Type } = Infer<Schema> & {
14
+ $type?: Type
15
+ },
16
+ > extends Validator<Output> {
17
+ readonly lexiconType = 'object' as const
18
+
19
+ constructor(
20
+ readonly $type: Type,
21
+ readonly schema: Schema,
22
+ ) {
23
+ super()
24
+ }
25
+
26
+ isTypeOf<X extends { $type?: unknown }>(
27
+ value: X,
28
+ ): value is X extends { $type?: Type } ? X : never {
29
+ return value.$type === undefined || value.$type === this.$type
30
+ }
31
+
32
+ build<X extends Omit<Output, '$type'>>(
33
+ input: X,
34
+ ): Simplify<Omit<X, '$type'> & { $type: Type }> {
35
+ return { ...input, $type: this.$type }
36
+ }
37
+
38
+ $isTypeOf<X extends { $type?: unknown }>(value: X) {
39
+ return this.isTypeOf<X>(value)
40
+ }
41
+
42
+ $build<X extends Omit<Output, '$type'>>(input: X) {
43
+ return this.build<X>(input)
44
+ }
45
+
46
+ override validateInContext(
47
+ input: unknown,
48
+ ctx: ValidatorContext,
49
+ ): ValidationResult<Output> {
50
+ if (!isPlainObject(input)) {
51
+ return ctx.issueInvalidType(input, 'object')
52
+ }
53
+
54
+ if (
55
+ '$type' in input &&
56
+ input.$type !== undefined &&
57
+ input.$type !== this.$type
58
+ ) {
59
+ return ctx.issueInvalidPropertyValue(input, '$type', [this.$type])
60
+ }
61
+
62
+ return ctx.validate(input, this.schema as Validator<Output>)
63
+ }
64
+ }
@@ -0,0 +1,68 @@
1
+ import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
2
+
3
+ // Basically a RecordSchema or TypedObjectSchema
4
+ export type TypedRefSchemaValidator<V extends { $type?: string } = any> =
5
+ V extends { $type?: infer T extends string }
6
+ ? { $type: T } & Validator<V & { $type?: T }>
7
+ : never
8
+
9
+ export type TypedRefGetter<V extends { $type?: string } = any> =
10
+ () => TypedRefSchemaValidator<V>
11
+
12
+ export type TypedRefSchemaOutput<V extends { $type?: string } = any> =
13
+ V extends { $type?: infer T extends string } ? V & { $type: T } : never
14
+
15
+ export class TypedRefSchema<
16
+ V extends { $type?: string } = any,
17
+ > extends Validator<TypedRefSchemaOutput<V>> {
18
+ #getter: TypedRefGetter<V>
19
+
20
+ constructor(getter: TypedRefGetter<V>) {
21
+ // @NOTE In order to avoid circular dependency issues, we don't resolve
22
+ // the schema here. Instead, we resolve it lazily when first accessed.
23
+
24
+ super()
25
+
26
+ this.#getter = getter
27
+ }
28
+
29
+ get schema(): TypedRefSchemaValidator<V> {
30
+ const value = this.#getter.call(null)
31
+
32
+ // Prevents a getter from depending on itself recursively, also allows GC to
33
+ // clean up the getter function.
34
+ this.#getter = throwAlreadyCalled
35
+
36
+ // Cache the resolved schema on the instance
37
+ Object.defineProperty(this, 'schema', {
38
+ value,
39
+ writable: false,
40
+ enumerable: false,
41
+ configurable: true,
42
+ })
43
+
44
+ return value
45
+ }
46
+
47
+ get $type(): TypedRefSchemaOutput<V>['$type'] {
48
+ return this.schema.$type
49
+ }
50
+
51
+ override validateInContext(
52
+ input: unknown,
53
+ ctx: ValidatorContext,
54
+ ): ValidationResult<TypedRefSchemaOutput<V>> {
55
+ const result = ctx.validate(input, this.schema)
56
+ if (!result.success) return result
57
+
58
+ if (result.value.$type !== this.$type) {
59
+ return ctx.issueInvalidPropertyValue(result.value, '$type', [this.$type])
60
+ }
61
+
62
+ return result as ValidationResult<TypedRefSchemaOutput<V>>
63
+ }
64
+ }
65
+
66
+ function throwAlreadyCalled(): never {
67
+ throw new Error('TypedRefSchema getter called multiple times')
68
+ }
@@ -0,0 +1,106 @@
1
+ import { isPlainObject } from '@atproto/lex-data'
2
+ import { Restricted, UnknownString } from '../core.js'
3
+ import {
4
+ Infer,
5
+ ValidationResult,
6
+ Validator,
7
+ ValidatorContext,
8
+ } from '../validation.js'
9
+ import { TypedRefSchema, TypedRefSchemaOutput } from './typed-ref.js'
10
+
11
+ export type TypedRef<T extends { $type?: string }> = TypedRefSchemaOutput<T>
12
+
13
+ export type TypedObject = { $type: UnknownString } & {
14
+ // In order to prevent places that expect an open union from accepting an
15
+ // invalid version of the known typed objects, we need to prevent any other
16
+ // properties from being present.
17
+ //
18
+ // For example, if an open union expects:
19
+ // ```ts
20
+ // TypedObject | { $type: 'A'; a: number }
21
+ // ```
22
+ // we don't want it to accept:
23
+ // ```ts
24
+ // { $type: 'A' }
25
+ // ```
26
+ // Which would be the case as `{ $type: 'A' }` is a valid
27
+ // `TypedObject`. By adding an index signature that forbids any
28
+ // property, we ensure that only valid known typed objects can be used.
29
+ [K in string]: Restricted<'Unknown property'>
30
+ }
31
+
32
+ type TypedRefSchemasToUnion<T extends readonly TypedRefSchema[]> = {
33
+ [K in keyof T]: Infer<T[K]>
34
+ }[number]
35
+
36
+ export type TypedUnionSchemaOutput<
37
+ TypedRefs extends readonly TypedRefSchema[],
38
+ Closed extends boolean,
39
+ > = Closed extends true
40
+ ? TypedRefSchemasToUnion<TypedRefs>
41
+ : TypedRefSchemasToUnion<TypedRefs> | TypedObject
42
+
43
+ export class TypedUnionSchema<
44
+ TypedRefs extends readonly TypedRefSchema[] = any,
45
+ Closed extends boolean = any,
46
+ > extends Validator<TypedUnionSchemaOutput<TypedRefs, Closed>> {
47
+ readonly lexiconType = 'union' as const
48
+
49
+ constructor(
50
+ protected readonly refs: TypedRefs,
51
+ public readonly closed: Closed,
52
+ ) {
53
+ // @NOTE In order to avoid circular dependency issues, we don't access the
54
+ // refs's schema (or $type) here. Instead, we access them lazily when first
55
+ // needed.
56
+
57
+ super()
58
+ }
59
+
60
+ protected get refsMap() {
61
+ const map = new Map<unknown, TypedRefs[number]>()
62
+ for (const ref of this.refs) map.set(ref.$type, ref)
63
+
64
+ // Cache the map on the instance
65
+ Object.defineProperty(this, 'refsMap', {
66
+ value: map,
67
+ writable: false,
68
+ enumerable: false,
69
+ configurable: true,
70
+ })
71
+
72
+ return map
73
+ }
74
+
75
+ get $types() {
76
+ return Array.from(this.refsMap.keys())
77
+ }
78
+
79
+ override validateInContext(
80
+ input: unknown,
81
+ ctx: ValidatorContext,
82
+ ): ValidationResult<TypedUnionSchemaOutput<TypedRefs, Closed>> {
83
+ if (!isPlainObject(input) || !('$type' in input)) {
84
+ return ctx.issueInvalidType(input, '$typed')
85
+ }
86
+
87
+ const { $type } = input
88
+
89
+ const def = this.refsMap.get($type)
90
+ if (def) {
91
+ const result = ctx.validate(input, def)
92
+ return result as ValidationResult<
93
+ TypedUnionSchemaOutput<TypedRefs, Closed>
94
+ >
95
+ }
96
+
97
+ if (this.closed) {
98
+ return ctx.issueInvalidPropertyValue(input, '$type', this.$types)
99
+ }
100
+ if (typeof $type !== 'string') {
101
+ return ctx.issueInvalidPropertyType(input, '$type', 'string')
102
+ }
103
+
104
+ return ctx.success(input as TypedUnionSchemaOutput<TypedRefs, Closed>)
105
+ }
106
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ Infer,
3
+ ValidationError,
4
+ ValidationFailure,
5
+ ValidationResult,
6
+ Validator,
7
+ ValidatorContext,
8
+ } from '../validation.js'
9
+
10
+ export type UnionSchemaValidators = readonly [Validator, ...Validator[]]
11
+ export type UnionSchemaOutput<V extends readonly Validator[]> = Infer<V[number]>
12
+
13
+ export class UnionSchema<
14
+ V extends UnionSchemaValidators = any,
15
+ > extends Validator<UnionSchemaOutput<V>> {
16
+ constructor(protected readonly validators: V) {
17
+ super()
18
+ }
19
+
20
+ override validateInContext(
21
+ input: unknown,
22
+ ctx: ValidatorContext,
23
+ ): ValidationResult<UnionSchemaOutput<V>> {
24
+ const failures: ValidationFailure[] = []
25
+
26
+ for (const validator of this.validators) {
27
+ const result = ctx.validate(input, validator)
28
+ if (result.success) {
29
+ return result as ValidationResult<UnionSchemaOutput<V>>
30
+ } else {
31
+ failures.push(result)
32
+ }
33
+ }
34
+
35
+ return {
36
+ success: false,
37
+ error: ValidationError.fromFailures(failures),
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,20 @@
1
+ import { LexMap, isLexMap } from '@atproto/lex-data'
2
+ import { ValidationResult, Validator, ValidatorContext } from '../validation'
3
+
4
+ export type { LexMap }
5
+ export type UnknownObjectOutput = LexMap
6
+
7
+ export class UnknownObjectSchema extends Validator<UnknownObjectOutput> {
8
+ readonly lexiconType = 'unknown' as const
9
+
10
+ override validateInContext(
11
+ input: unknown,
12
+ ctx: ValidatorContext,
13
+ ): ValidationResult<UnknownObjectOutput> {
14
+ if (isLexMap(input)) {
15
+ return ctx.success(input)
16
+ }
17
+
18
+ return ctx.issueInvalidType(input, 'unknown')
19
+ }
20
+ }
@@ -0,0 +1,10 @@
1
+ import { ValidationResult, Validator, ValidatorContext } from '../validation'
2
+
3
+ export class UnknownSchema extends Validator<unknown> {
4
+ override validateInContext(
5
+ input: unknown,
6
+ ctx: ValidatorContext,
7
+ ): ValidationResult<unknown> {
8
+ return ctx.success(input)
9
+ }
10
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,40 @@
1
+ // Utilities (that depend on *and* are used by schemas)
2
+ export * from './schema/_parameters.js'
3
+
4
+ // Concrete Types
5
+ export * from './schema/array.js'
6
+ export * from './schema/blob.js'
7
+ export * from './schema/boolean.js'
8
+ export * from './schema/bytes.js'
9
+ export * from './schema/cid.js'
10
+ export * from './schema/dict.js'
11
+ export * from './schema/enum.js'
12
+ export * from './schema/integer.js'
13
+ export * from './schema/literal.js'
14
+ export * from './schema/never.js'
15
+ export * from './schema/null.js'
16
+ export * from './schema/object.js'
17
+ export * from './schema/string.js'
18
+ export * from './schema/unknown-object.js'
19
+ export * from './schema/unknown.js'
20
+
21
+ // Composite Types
22
+ export * from './schema/custom.js'
23
+ export * from './schema/discriminated-union.js'
24
+ export * from './schema/intersection.js'
25
+ export * from './schema/ref.js'
26
+ export * from './schema/union.js'
27
+
28
+ // Lexicon specific Types
29
+ export * from './schema/params.js'
30
+ export * from './schema/payload.js'
31
+ export * from './schema/permission-set.js'
32
+ export * from './schema/permission.js'
33
+ export * from './schema/procedure.js'
34
+ export * from './schema/query.js'
35
+ export * from './schema/record.js'
36
+ export * from './schema/subscription.js'
37
+ export * from './schema/token.js'
38
+ export * from './schema/typed-object.js'
39
+ export * from './schema/typed-ref.js'
40
+ export * from './schema/typed-union.js'
@@ -0,0 +1,41 @@
1
+ import { arrayAgg } from './array-agg.js'
2
+
3
+ describe('arrayAgg', () => {
4
+ it('aggregates items based on comparison and aggregation functions', () => {
5
+ const input = [1, 1, 2, 2, 3, 3, 3]
6
+ const result = arrayAgg(
7
+ input,
8
+ (a, b) => a === b,
9
+ (items) => ({ value: items[0], count: items.length }),
10
+ )
11
+ expect(result).toEqual([
12
+ { value: 1, count: 2 },
13
+ { value: 2, count: 2 },
14
+ { value: 3, count: 3 },
15
+ ])
16
+ })
17
+
18
+ it('returns an empty array when input is empty', () => {
19
+ const input: number[] = []
20
+ const result = arrayAgg(
21
+ input,
22
+ (a, b) => a === b,
23
+ (items) => ({ value: items[0], count: items.length }),
24
+ )
25
+ expect(result).toEqual([])
26
+ })
27
+
28
+ it('handles non-consecutive grouping', () => {
29
+ const input = [1, 2, 1, 2, 3, 1]
30
+ const result = arrayAgg(
31
+ input,
32
+ (a, b) => a === b,
33
+ (items) => ({ value: items[0], count: items.length }),
34
+ )
35
+ expect(result).toEqual([
36
+ { value: 1, count: 3 },
37
+ { value: 2, count: 2 },
38
+ { value: 3, count: 1 },
39
+ ])
40
+ })
41
+ })
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Aggregates items in an array based on a comparison function and an aggregation function.
3
+ *
4
+ * @param arr - The input array to aggregate.
5
+ * @param cmp - A comparison function that determines if two items belong to the same group.
6
+ * @param agg - An aggregation function that combines items in a group into a single item.
7
+ * @returns An array of aggregated items.
8
+ * @example
9
+ * ```ts
10
+ * const input = [1, 1, 2, 2, 3, 3, 3]
11
+ * const result = arrayAgg(
12
+ * input,
13
+ * (a, b) => a === b,
14
+ * (items) => { value: items[0], sum: items.reduce((sum, item) => sum + item, 0) },
15
+ * )
16
+ * // result is [{ value: 1, sum: 2 }, { value: 2, sum: 4 }, { value: 3, sum: 6 }]
17
+ * ```
18
+ */
19
+ export function arrayAgg<T, O>(
20
+ arr: readonly T[],
21
+ cmp: (a: T, b: T) => boolean,
22
+ agg: (items: [T, ...T[]]) => O,
23
+ ): O[] {
24
+ if (arr.length === 0) return []
25
+
26
+ const groups: [T, ...T[]][] = [[arr[0]]]
27
+ const skipped = Array<undefined | boolean>(arr.length)
28
+
29
+ outer: for (let i = 1; i < arr.length; i++) {
30
+ if (skipped[i]) continue
31
+ const item = arr[i]
32
+ for (let j = 0; j < groups.length; j++) {
33
+ if (cmp(item, groups[j][0])) {
34
+ groups[j].push(item)
35
+ skipped[i] = true
36
+ continue outer
37
+ }
38
+ }
39
+ groups.push([item])
40
+ }
41
+
42
+ return groups.map(agg)
43
+ }
@@ -0,0 +1 @@
1
+ export type PropertyKey = string | number
@@ -0,0 +1,32 @@
1
+ import { ResultFailure, failureError } from '../core.js'
2
+ import {
3
+ ValidationIssue,
4
+ aggregateIssues,
5
+ stringifyIssue,
6
+ } from './validation-issue.js'
7
+
8
+ export class ValidationError extends Error {
9
+ name = 'ValidationError'
10
+
11
+ constructor(
12
+ readonly issues: ValidationIssue[],
13
+ options?: ErrorOptions,
14
+ ) {
15
+ super(issues.map(stringifyIssue).join(', '), options)
16
+ }
17
+
18
+ static fromFailures(
19
+ failures: ResultFailure<ValidationError>[],
20
+ ): ValidationError {
21
+ if (failures.length === 1) return failures[0].error
22
+ const issues = failures.flatMap(extractFailureIssues)
23
+ return new ValidationError(aggregateIssues(issues), {
24
+ // Keep the original errors as the cause chain
25
+ cause: failures.map(failureError),
26
+ })
27
+ }
28
+ }
29
+
30
+ function extractFailureIssues(result: ResultFailure<ValidationError>) {
31
+ return result.error.issues
32
+ }