@atproto/lex-schema 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/core/$type.d.ts +6 -3
  3. package/dist/core/$type.d.ts.map +1 -1
  4. package/dist/core/$type.js +1 -0
  5. package/dist/core/$type.js.map +1 -1
  6. package/dist/core/record-key.d.ts +3 -3
  7. package/dist/core/record-key.d.ts.map +1 -1
  8. package/dist/core/record-key.js +12 -6
  9. package/dist/core/record-key.js.map +1 -1
  10. package/dist/core/result.d.ts.map +1 -1
  11. package/dist/core/result.js +6 -0
  12. package/dist/core/result.js.map +1 -1
  13. package/dist/core/string-format.d.ts +30 -27
  14. package/dist/core/string-format.d.ts.map +1 -1
  15. package/dist/core/string-format.js +56 -42
  16. package/dist/core/string-format.js.map +1 -1
  17. package/dist/core/types.d.ts +9 -1
  18. package/dist/core/types.d.ts.map +1 -1
  19. package/dist/core/types.js.map +1 -1
  20. package/dist/external.d.ts +31 -28
  21. package/dist/external.d.ts.map +1 -1
  22. package/dist/external.js +33 -17
  23. package/dist/external.js.map +1 -1
  24. package/dist/schema/_parameters.d.ts +2 -2
  25. package/dist/schema/_parameters.d.ts.map +1 -1
  26. package/dist/schema/array.d.ts +5 -6
  27. package/dist/schema/array.d.ts.map +1 -1
  28. package/dist/schema/array.js +5 -6
  29. package/dist/schema/array.js.map +1 -1
  30. package/dist/schema/blob.d.ts +2 -3
  31. package/dist/schema/blob.d.ts.map +1 -1
  32. package/dist/schema/blob.js +1 -2
  33. package/dist/schema/blob.js.map +1 -1
  34. package/dist/schema/boolean.d.ts +4 -5
  35. package/dist/schema/boolean.d.ts.map +1 -1
  36. package/dist/schema/boolean.js +2 -3
  37. package/dist/schema/boolean.js.map +1 -1
  38. package/dist/schema/bytes.d.ts +3 -4
  39. package/dist/schema/bytes.d.ts.map +1 -1
  40. package/dist/schema/bytes.js +2 -3
  41. package/dist/schema/bytes.js.map +1 -1
  42. package/dist/schema/cid.d.ts +13 -6
  43. package/dist/schema/cid.d.ts.map +1 -1
  44. package/dist/schema/cid.js +2 -4
  45. package/dist/schema/cid.js.map +1 -1
  46. package/dist/schema/custom.d.ts +3 -4
  47. package/dist/schema/custom.d.ts.map +1 -1
  48. package/dist/schema/custom.js +4 -3
  49. package/dist/schema/custom.js.map +1 -1
  50. package/dist/schema/dict.d.ts +3 -3
  51. package/dist/schema/dict.d.ts.map +1 -1
  52. package/dist/schema/dict.js +1 -1
  53. package/dist/schema/dict.js.map +1 -1
  54. package/dist/schema/discriminated-union.d.ts +15 -24
  55. package/dist/schema/discriminated-union.d.ts.map +1 -1
  56. package/dist/schema/discriminated-union.js +40 -64
  57. package/dist/schema/discriminated-union.js.map +1 -1
  58. package/dist/schema/enum.d.ts +8 -4
  59. package/dist/schema/enum.d.ts.map +1 -1
  60. package/dist/schema/enum.js +5 -3
  61. package/dist/schema/enum.js.map +1 -1
  62. package/dist/schema/integer.d.ts +3 -4
  63. package/dist/schema/integer.d.ts.map +1 -1
  64. package/dist/schema/integer.js +3 -4
  65. package/dist/schema/integer.js.map +1 -1
  66. package/dist/schema/intersection.d.ts +22 -14
  67. package/dist/schema/intersection.d.ts.map +1 -1
  68. package/dist/schema/intersection.js +12 -22
  69. package/dist/schema/intersection.js.map +1 -1
  70. package/dist/schema/literal.d.ts +7 -3
  71. package/dist/schema/literal.d.ts.map +1 -1
  72. package/dist/schema/literal.js +5 -3
  73. package/dist/schema/literal.js.map +1 -1
  74. package/dist/schema/never.d.ts +2 -2
  75. package/dist/schema/never.d.ts.map +1 -1
  76. package/dist/schema/never.js +1 -1
  77. package/dist/schema/never.js.map +1 -1
  78. package/dist/schema/null.d.ts +2 -3
  79. package/dist/schema/null.d.ts.map +1 -1
  80. package/dist/schema/null.js +1 -2
  81. package/dist/schema/null.js.map +1 -1
  82. package/dist/schema/nullable.d.ts +7 -0
  83. package/dist/schema/nullable.d.ts.map +1 -0
  84. package/dist/schema/nullable.js +19 -0
  85. package/dist/schema/nullable.js.map +1 -0
  86. package/dist/schema/object.d.ts +10 -44
  87. package/dist/schema/object.d.ts.map +1 -1
  88. package/dist/schema/object.js +10 -46
  89. package/dist/schema/object.js.map +1 -1
  90. package/dist/schema/optional.d.ts +7 -0
  91. package/dist/schema/optional.d.ts.map +1 -0
  92. package/dist/schema/optional.js +25 -0
  93. package/dist/schema/optional.js.map +1 -0
  94. package/dist/schema/params.d.ts +14 -19
  95. package/dist/schema/params.d.ts.map +1 -1
  96. package/dist/schema/params.js +10 -24
  97. package/dist/schema/params.js.map +1 -1
  98. package/dist/schema/payload.d.ts +4 -4
  99. package/dist/schema/payload.d.ts.map +1 -1
  100. package/dist/schema/payload.js.map +1 -1
  101. package/dist/schema/permission-set.d.ts +6 -6
  102. package/dist/schema/permission-set.d.ts.map +1 -1
  103. package/dist/schema/permission-set.js +1 -2
  104. package/dist/schema/permission-set.js.map +1 -1
  105. package/dist/schema/permission.d.ts +0 -1
  106. package/dist/schema/permission.d.ts.map +1 -1
  107. package/dist/schema/permission.js +0 -1
  108. package/dist/schema/permission.js.map +1 -1
  109. package/dist/schema/procedure.d.ts +8 -9
  110. package/dist/schema/procedure.d.ts.map +1 -1
  111. package/dist/schema/procedure.js +0 -1
  112. package/dist/schema/procedure.js.map +1 -1
  113. package/dist/schema/query.d.ts +7 -8
  114. package/dist/schema/query.d.ts.map +1 -1
  115. package/dist/schema/query.js +0 -1
  116. package/dist/schema/query.js.map +1 -1
  117. package/dist/schema/record.d.ts +34 -28
  118. package/dist/schema/record.d.ts.map +1 -1
  119. package/dist/schema/record.js +1 -2
  120. package/dist/schema/record.js.map +1 -1
  121. package/dist/schema/ref.d.ts +2 -3
  122. package/dist/schema/ref.d.ts.map +1 -1
  123. package/dist/schema/ref.js +1 -2
  124. package/dist/schema/ref.js.map +1 -1
  125. package/dist/schema/refine.d.ts +18 -0
  126. package/dist/schema/refine.d.ts.map +1 -0
  127. package/dist/schema/refine.js +33 -0
  128. package/dist/schema/refine.js.map +1 -0
  129. package/dist/schema/regexp.d.ts +7 -0
  130. package/dist/schema/regexp.d.ts.map +1 -0
  131. package/dist/schema/regexp.js +22 -0
  132. package/dist/schema/regexp.js.map +1 -0
  133. package/dist/schema/string.d.ts +4 -8
  134. package/dist/schema/string.d.ts.map +1 -1
  135. package/dist/schema/string.js +6 -3
  136. package/dist/schema/string.js.map +1 -1
  137. package/dist/schema/subscription.d.ts +7 -6
  138. package/dist/schema/subscription.d.ts.map +1 -1
  139. package/dist/schema/subscription.js.map +1 -1
  140. package/dist/schema/token.d.ts +2 -3
  141. package/dist/schema/token.d.ts.map +1 -1
  142. package/dist/schema/token.js +1 -2
  143. package/dist/schema/token.js.map +1 -1
  144. package/dist/schema/typed-object.d.ts +29 -27
  145. package/dist/schema/typed-object.d.ts.map +1 -1
  146. package/dist/schema/typed-object.js +1 -2
  147. package/dist/schema/typed-object.js.map +1 -1
  148. package/dist/schema/typed-ref.d.ts +2 -2
  149. package/dist/schema/typed-ref.d.ts.map +1 -1
  150. package/dist/schema/typed-ref.js +1 -1
  151. package/dist/schema/typed-ref.js.map +1 -1
  152. package/dist/schema/typed-union.d.ts +3 -4
  153. package/dist/schema/typed-union.d.ts.map +1 -1
  154. package/dist/schema/typed-union.js +3 -10
  155. package/dist/schema/typed-union.js.map +1 -1
  156. package/dist/schema/union.d.ts +2 -2
  157. package/dist/schema/union.d.ts.map +1 -1
  158. package/dist/schema/union.js +1 -1
  159. package/dist/schema/union.js.map +1 -1
  160. package/dist/schema/unknown-object.d.ts +2 -3
  161. package/dist/schema/unknown-object.d.ts.map +1 -1
  162. package/dist/schema/unknown-object.js +1 -2
  163. package/dist/schema/unknown-object.js.map +1 -1
  164. package/dist/schema/unknown.d.ts +2 -2
  165. package/dist/schema/unknown.d.ts.map +1 -1
  166. package/dist/schema/unknown.js +1 -1
  167. package/dist/schema/unknown.js.map +1 -1
  168. package/dist/schema.d.ts +4 -0
  169. package/dist/schema.d.ts.map +1 -1
  170. package/dist/schema.js +6 -1
  171. package/dist/schema.js.map +1 -1
  172. package/dist/util/array-agg.d.ts.map +1 -1
  173. package/dist/util/array-agg.js +1 -0
  174. package/dist/util/array-agg.js.map +1 -1
  175. package/dist/util/lazy-property.d.ts +2 -0
  176. package/dist/util/lazy-property.d.ts.map +1 -0
  177. package/dist/util/lazy-property.js +14 -0
  178. package/dist/util/lazy-property.js.map +1 -0
  179. package/dist/validation/schema.d.ts +24 -0
  180. package/dist/validation/schema.d.ts.map +1 -0
  181. package/dist/validation/schema.js +57 -0
  182. package/dist/validation/schema.js.map +1 -0
  183. package/dist/validation/validation-error.d.ts +3 -3
  184. package/dist/validation/validation-error.d.ts.map +1 -1
  185. package/dist/validation/validation-error.js +32 -4
  186. package/dist/validation/validation-error.js.map +1 -1
  187. package/dist/validation/validation-issue.d.ts +32 -24
  188. package/dist/validation/validation-issue.d.ts.map +1 -1
  189. package/dist/validation/validation-issue.js +136 -92
  190. package/dist/validation/validation-issue.js.map +1 -1
  191. package/dist/validation/validator.d.ts +20 -50
  192. package/dist/validation/validator.d.ts.map +1 -1
  193. package/dist/validation/validator.js +40 -134
  194. package/dist/validation/validator.js.map +1 -1
  195. package/dist/validation.d.ts +1 -0
  196. package/dist/validation.d.ts.map +1 -1
  197. package/dist/validation.js +1 -0
  198. package/dist/validation.js.map +1 -1
  199. package/package.json +8 -4
  200. package/src/core/$type.ts +7 -4
  201. package/src/core/record-key.ts +12 -5
  202. package/src/core/result.ts +6 -0
  203. package/src/core/string-format.ts +97 -61
  204. package/src/core/types.ts +12 -6
  205. package/src/external.ts +92 -70
  206. package/src/schema/_parameters.test.ts +416 -0
  207. package/src/schema/array.test.ts +237 -0
  208. package/src/schema/array.ts +17 -11
  209. package/src/schema/blob.test.ts +506 -0
  210. package/src/schema/blob.ts +3 -5
  211. package/src/schema/boolean.test.ts +116 -0
  212. package/src/schema/boolean.ts +5 -7
  213. package/src/schema/bytes.test.ts +226 -0
  214. package/src/schema/bytes.ts +4 -6
  215. package/src/schema/cid.test.ts +155 -0
  216. package/src/schema/cid.ts +14 -8
  217. package/src/schema/custom.test.ts +413 -0
  218. package/src/schema/custom.ts +10 -8
  219. package/src/schema/dict.test.ts +198 -0
  220. package/src/schema/dict.ts +6 -8
  221. package/src/schema/discriminated-union.test.ts +675 -0
  222. package/src/schema/discriminated-union.ts +68 -95
  223. package/src/schema/enum.test.ts +396 -0
  224. package/src/schema/enum.ts +12 -5
  225. package/src/schema/integer.test.ts +312 -0
  226. package/src/schema/integer.ts +5 -7
  227. package/src/schema/intersection.test.ts +32 -0
  228. package/src/schema/intersection.ts +37 -40
  229. package/src/schema/literal.test.ts +531 -0
  230. package/src/schema/literal.ts +12 -5
  231. package/src/schema/never.test.ts +174 -0
  232. package/src/schema/never.ts +3 -10
  233. package/src/schema/null.test.ts +79 -0
  234. package/src/schema/null.ts +3 -5
  235. package/src/schema/nullable.test.ts +480 -0
  236. package/src/schema/nullable.ts +23 -0
  237. package/src/schema/object.test.ts +47 -115
  238. package/src/schema/object.ts +19 -123
  239. package/src/schema/optional.test.ts +485 -0
  240. package/src/schema/optional.ts +31 -0
  241. package/src/schema/params.test.ts +582 -0
  242. package/src/schema/params.ts +37 -55
  243. package/src/schema/payload.test.ts +345 -0
  244. package/src/schema/payload.ts +5 -5
  245. package/src/schema/permission-set.test.ts +679 -0
  246. package/src/schema/permission-set.ts +6 -8
  247. package/src/schema/permission.test.ts +536 -0
  248. package/src/schema/permission.ts +0 -2
  249. package/src/schema/procedure.test.ts +443 -0
  250. package/src/schema/procedure.ts +11 -13
  251. package/src/schema/query.test.ts +408 -0
  252. package/src/schema/query.ts +9 -11
  253. package/src/schema/record.test.ts +694 -0
  254. package/src/schema/record.ts +38 -36
  255. package/src/schema/ref.test.ts +365 -0
  256. package/src/schema/ref.ts +8 -5
  257. package/src/schema/refine.test.ts +578 -0
  258. package/src/schema/refine.ts +85 -0
  259. package/src/schema/regexp.test.ts +580 -0
  260. package/src/schema/regexp.ts +22 -0
  261. package/src/schema/string.test.ts +612 -0
  262. package/src/schema/string.ts +11 -17
  263. package/src/schema/subscription.test.ts +689 -0
  264. package/src/schema/subscription.ts +13 -8
  265. package/src/schema/token.test.ts +428 -0
  266. package/src/schema/token.ts +3 -5
  267. package/src/schema/typed-object.test.ts +612 -0
  268. package/src/schema/typed-object.ts +23 -20
  269. package/src/schema/typed-ref.test.ts +823 -0
  270. package/src/schema/typed-ref.ts +10 -5
  271. package/src/schema/typed-union.test.ts +378 -0
  272. package/src/schema/typed-union.ts +6 -15
  273. package/src/schema/union.test.ts +200 -0
  274. package/src/schema/union.ts +5 -4
  275. package/src/schema/unknown-object.test.ts +592 -0
  276. package/src/schema/unknown-object.ts +3 -5
  277. package/src/schema/unknown.test.ts +312 -0
  278. package/src/schema/unknown.ts +3 -3
  279. package/src/schema.ts +7 -1
  280. package/src/util/array-agg.ts +1 -0
  281. package/src/util/lazy-property.ts +14 -0
  282. package/src/validation/schema.ts +92 -0
  283. package/src/validation/validation-error.ts +60 -9
  284. package/src/validation/validation-issue.ts +141 -144
  285. package/src/validation/validator.ts +67 -206
  286. package/src/validation.ts +1 -0
  287. package/tsconfig.build.json +12 -0
  288. package/tsconfig.json +7 -0
  289. package/tsconfig.tests.json +9 -0
@@ -0,0 +1,612 @@
1
+ import { EnumSchema } from './enum.js'
2
+ import { IntegerSchema } from './integer.js'
3
+ import { NullableSchema } from './nullable.js'
4
+ import { ObjectSchema } from './object.js'
5
+ import { OptionalSchema } from './optional.js'
6
+ import { StringSchema } from './string.js'
7
+ import { TypedObjectSchema } from './typed-object.js'
8
+
9
+ describe('TypedObjectSchema', () => {
10
+ const schema = new TypedObjectSchema(
11
+ 'app.bsky.feed.post#main',
12
+ new ObjectSchema({
13
+ text: new StringSchema({}),
14
+ likes: new OptionalSchema(new IntegerSchema({})),
15
+ }),
16
+ )
17
+
18
+ describe('basic validation', () => {
19
+ it('validates plain objects without $type', () => {
20
+ const result = schema.safeParse({
21
+ text: 'Hello world',
22
+ likes: 5,
23
+ })
24
+ expect(result.success).toBe(true)
25
+ })
26
+
27
+ it('validates plain objects with matching $type', () => {
28
+ const result = schema.safeParse({
29
+ $type: 'app.bsky.feed.post#main',
30
+ text: 'Hello world',
31
+ likes: 5,
32
+ })
33
+ expect(result.success).toBe(true)
34
+ })
35
+
36
+ it('rejects objects with non-matching $type', () => {
37
+ const result = schema.safeParse({
38
+ $type: 'app.bsky.feed.like#main',
39
+ text: 'Hello world',
40
+ likes: 5,
41
+ })
42
+ expect(result.success).toBe(false)
43
+ })
44
+
45
+ it('rejects non-objects', () => {
46
+ const result = schema.safeParse('not an object')
47
+ expect(result.success).toBe(false)
48
+ })
49
+
50
+ it('rejects null', () => {
51
+ const result = schema.safeParse(null)
52
+ expect(result.success).toBe(false)
53
+ })
54
+
55
+ it('rejects undefined', () => {
56
+ const result = schema.safeParse(undefined)
57
+ expect(result.success).toBe(false)
58
+ })
59
+
60
+ it('rejects arrays', () => {
61
+ const result = schema.safeParse(['text', 5])
62
+ expect(result.success).toBe(false)
63
+ })
64
+
65
+ it('rejects numbers', () => {
66
+ const result = schema.safeParse(123)
67
+ expect(result.success).toBe(false)
68
+ })
69
+
70
+ it('rejects booleans', () => {
71
+ const result = schema.safeParse(true)
72
+ expect(result.success).toBe(false)
73
+ })
74
+ })
75
+
76
+ describe('property validation', () => {
77
+ it('rejects missing required properties', () => {
78
+ const result = schema.safeParse({
79
+ likes: 5,
80
+ })
81
+ expect(result.success).toBe(false)
82
+ })
83
+
84
+ it('validates optional properties', () => {
85
+ const result = schema.safeParse({
86
+ text: 'Hello world',
87
+ })
88
+ expect(result.success).toBe(true)
89
+ })
90
+
91
+ it('rejects invalid property types', () => {
92
+ const result = schema.safeParse({
93
+ text: 'Hello world',
94
+ likes: 'five',
95
+ })
96
+ expect(result.success).toBe(false)
97
+ })
98
+
99
+ it('rejects invalid required property types', () => {
100
+ const result = schema.safeParse({
101
+ text: 123,
102
+ likes: 5,
103
+ })
104
+ expect(result.success).toBe(false)
105
+ })
106
+
107
+ it('ignores extra properties', () => {
108
+ const result = schema.safeParse({
109
+ text: 'Hello world',
110
+ likes: 5,
111
+ extra: 'value',
112
+ })
113
+ expect(result.success).toBe(true)
114
+ })
115
+ })
116
+
117
+ describe('$type validation', () => {
118
+ it('treats undefined $type as valid', () => {
119
+ const result = schema.safeParse({
120
+ $type: undefined,
121
+ text: 'Hello world',
122
+ })
123
+ expect(result.success).toBe(true)
124
+ })
125
+
126
+ it('rejects empty string $type', () => {
127
+ const result = schema.safeParse({
128
+ $type: '',
129
+ text: 'Hello world',
130
+ })
131
+ expect(result.success).toBe(false)
132
+ })
133
+
134
+ it('rejects numeric $type', () => {
135
+ const result = schema.safeParse({
136
+ $type: 123,
137
+ text: 'Hello world',
138
+ })
139
+ expect(result.success).toBe(false)
140
+ })
141
+
142
+ it('rejects object $type', () => {
143
+ const result = schema.safeParse({
144
+ $type: { type: 'app.bsky.feed.post#main' },
145
+ text: 'Hello world',
146
+ })
147
+ expect(result.success).toBe(false)
148
+ })
149
+
150
+ it('rejects partial match $type', () => {
151
+ const result = schema.safeParse({
152
+ $type: 'app.bsky.feed.post',
153
+ text: 'Hello world',
154
+ })
155
+ expect(result.success).toBe(false)
156
+ })
157
+
158
+ it('rejects $type with extra characters', () => {
159
+ const result = schema.safeParse({
160
+ $type: 'app.bsky.feed.post#main-extra',
161
+ text: 'Hello world',
162
+ })
163
+ expect(result.success).toBe(false)
164
+ })
165
+
166
+ it('rejects case-mismatched $type', () => {
167
+ const result = schema.safeParse({
168
+ $type: 'APP.BSKY.FEED.POST#MAIN',
169
+ text: 'Hello world',
170
+ })
171
+ expect(result.success).toBe(false)
172
+ })
173
+ })
174
+
175
+ describe('isTypeOf method', () => {
176
+ it('returns true for objects without $type', () => {
177
+ const obj = { text: 'Hello' }
178
+ expect(schema.isTypeOf(obj)).toBe(true)
179
+ })
180
+
181
+ it('returns true for objects with undefined $type', () => {
182
+ const obj = { $type: undefined, text: 'Hello' }
183
+ expect(schema.isTypeOf(obj)).toBe(true)
184
+ })
185
+
186
+ it('returns true for objects with matching $type', () => {
187
+ const obj = { $type: 'app.bsky.feed.post#main', text: 'Hello' }
188
+ expect(schema.isTypeOf(obj)).toBe(true)
189
+ })
190
+
191
+ it('returns false for objects with non-matching $type', () => {
192
+ const obj = { $type: 'app.bsky.feed.like#main', text: 'Hello' }
193
+ expect(schema.isTypeOf(obj)).toBe(false)
194
+ })
195
+
196
+ it('returns false for objects with empty $type', () => {
197
+ const obj = { $type: '', text: 'Hello' }
198
+ expect(schema.isTypeOf(obj)).toBe(false)
199
+ })
200
+
201
+ it('returns false for objects with numeric $type', () => {
202
+ const obj = { $type: 123, text: 'Hello' }
203
+ expect(schema.isTypeOf(obj)).toBe(false)
204
+ })
205
+ })
206
+
207
+ describe('$isTypeOf method', () => {
208
+ it('returns true for objects without $type', () => {
209
+ const obj = { text: 'Hello' }
210
+ expect(schema.$isTypeOf(obj)).toBe(true)
211
+ })
212
+
213
+ it('returns true for objects with matching $type', () => {
214
+ const obj = { $type: 'app.bsky.feed.post#main', text: 'Hello' }
215
+ expect(schema.$isTypeOf(obj)).toBe(true)
216
+ })
217
+
218
+ it('returns false for objects with non-matching $type', () => {
219
+ const obj = { $type: 'app.bsky.feed.like#main', text: 'Hello' }
220
+ expect(schema.$isTypeOf(obj)).toBe(false)
221
+ })
222
+
223
+ it('behaves identically to isTypeOf', () => {
224
+ const obj1 = { text: 'Hello' }
225
+ const obj2 = { $type: 'app.bsky.feed.post#main', text: 'Hello' }
226
+ const obj3 = { $type: 'app.bsky.feed.like#main', text: 'Hello' }
227
+
228
+ expect(schema.$isTypeOf(obj1)).toBe(schema.isTypeOf(obj1))
229
+ expect(schema.$isTypeOf(obj2)).toBe(schema.isTypeOf(obj2))
230
+ expect(schema.$isTypeOf(obj3)).toBe(schema.isTypeOf(obj3))
231
+ })
232
+ })
233
+
234
+ describe('build method', () => {
235
+ it('adds $type to object without $type', () => {
236
+ const input = { text: 'Hello world', likes: 5 }
237
+ const result = schema.build(input)
238
+ expect(result).toEqual({
239
+ text: 'Hello world',
240
+ likes: 5,
241
+ $type: 'app.bsky.feed.post#main',
242
+ })
243
+ })
244
+
245
+ it('adds $type to object with only required properties', () => {
246
+ const input = { text: 'Hello world' }
247
+ const result = schema.build(input)
248
+ expect(result).toEqual({
249
+ text: 'Hello world',
250
+ $type: 'app.bsky.feed.post#main',
251
+ })
252
+ })
253
+
254
+ it('preserves existing properties', () => {
255
+ const input = { text: 'Hello', likes: 10, extra: 'value' } as any
256
+ const result = schema.build(input)
257
+ expect(result).toEqual({
258
+ text: 'Hello',
259
+ likes: 10,
260
+ extra: 'value',
261
+ $type: 'app.bsky.feed.post#main',
262
+ })
263
+ })
264
+
265
+ it('does not mutate the input object', () => {
266
+ const input = { text: 'Hello world', likes: 5 }
267
+ const inputCopy = { ...input }
268
+ schema.build(input)
269
+ expect(input).toEqual(inputCopy)
270
+ })
271
+
272
+ it('adds $type to empty object', () => {
273
+ const emptySchema = new TypedObjectSchema(
274
+ 'app.bsky.test#main',
275
+ new ObjectSchema({}),
276
+ )
277
+ const input = {}
278
+ const result = emptySchema.build(input)
279
+ expect(result).toEqual({ $type: 'app.bsky.test#main' })
280
+ })
281
+ })
282
+
283
+ describe('$build method', () => {
284
+ it('adds $type to object without $type', () => {
285
+ const input = { text: 'Hello world', likes: 5 }
286
+ const result = schema.$build(input)
287
+ expect(result).toEqual({
288
+ text: 'Hello world',
289
+ likes: 5,
290
+ $type: 'app.bsky.feed.post#main',
291
+ })
292
+ })
293
+
294
+ it('behaves identically to build', () => {
295
+ const input1 = { text: 'Hello world', likes: 5 }
296
+ const input2 = { text: 'Another post' }
297
+
298
+ expect(schema.$build(input1)).toEqual(schema.build(input1))
299
+ expect(schema.$build(input2)).toEqual(schema.build(input2))
300
+ })
301
+
302
+ it('does not mutate the input object', () => {
303
+ const input = { text: 'Hello world' }
304
+ const inputCopy = { ...input }
305
+ schema.$build(input)
306
+ expect(input).toEqual(inputCopy)
307
+ })
308
+ })
309
+
310
+ describe('with complex nested schemas', () => {
311
+ const complexSchema = new TypedObjectSchema(
312
+ 'app.bsky.actor.profile#main',
313
+ new ObjectSchema({
314
+ displayName: new StringSchema({}),
315
+ bio: new OptionalSchema(new StringSchema({ maxLength: 256 })),
316
+ followerCount: new OptionalSchema(new IntegerSchema({ minimum: 0 })),
317
+ verified: new OptionalSchema(
318
+ new NullableSchema(new EnumSchema([true, false])),
319
+ ),
320
+ }),
321
+ )
322
+
323
+ it('validates complex nested structure', () => {
324
+ const result = complexSchema.safeParse({
325
+ displayName: 'John Doe',
326
+ bio: 'Software developer',
327
+ followerCount: 1000,
328
+ verified: true,
329
+ })
330
+ expect(result.success).toBe(true)
331
+ })
332
+
333
+ it('validates with nullable property set to null', () => {
334
+ const result = complexSchema.safeParse({
335
+ displayName: 'John Doe',
336
+ verified: null,
337
+ })
338
+ expect(result.success).toBe(true)
339
+ })
340
+
341
+ it('rejects when nested constraint is violated', () => {
342
+ const result = complexSchema.safeParse({
343
+ displayName: 'John Doe',
344
+ followerCount: -1,
345
+ })
346
+ expect(result.success).toBe(false)
347
+ })
348
+
349
+ it('rejects when string exceeds maxLength', () => {
350
+ const result = complexSchema.safeParse({
351
+ displayName: 'John Doe',
352
+ bio: 'x'.repeat(257),
353
+ })
354
+ expect(result.success).toBe(false)
355
+ })
356
+
357
+ it('validates with matching $type', () => {
358
+ const result = complexSchema.safeParse({
359
+ $type: 'app.bsky.actor.profile#main',
360
+ displayName: 'John Doe',
361
+ })
362
+ expect(result.success).toBe(true)
363
+ })
364
+
365
+ it('rejects with non-matching $type', () => {
366
+ const result = complexSchema.safeParse({
367
+ $type: 'app.bsky.feed.post#main',
368
+ displayName: 'John Doe',
369
+ })
370
+ expect(result.success).toBe(false)
371
+ })
372
+ })
373
+
374
+ describe('with different $type formats', () => {
375
+ it('validates with main type', () => {
376
+ const mainSchema = new TypedObjectSchema(
377
+ 'app.bsky.feed.post#main',
378
+ new ObjectSchema({ text: new StringSchema({}) }),
379
+ )
380
+ const result = mainSchema.safeParse({ text: 'Hello' })
381
+ expect(result.success).toBe(true)
382
+ })
383
+
384
+ it('validates with custom fragment', () => {
385
+ const fragmentSchema = new TypedObjectSchema(
386
+ 'app.bsky.feed.post#reply',
387
+ new ObjectSchema({ text: new StringSchema({}) }),
388
+ )
389
+ const result = fragmentSchema.safeParse({
390
+ $type: 'app.bsky.feed.post#reply',
391
+ text: 'Hello',
392
+ })
393
+ expect(result.success).toBe(true)
394
+ })
395
+
396
+ it('distinguishes between different fragments', () => {
397
+ const replySchema = new TypedObjectSchema(
398
+ 'app.bsky.feed.post#reply',
399
+ new ObjectSchema({ text: new StringSchema({}) }),
400
+ )
401
+ const result = replySchema.safeParse({
402
+ $type: 'app.bsky.feed.post#quote',
403
+ text: 'Hello',
404
+ })
405
+ expect(result.success).toBe(false)
406
+ })
407
+
408
+ it('validates with long NSID', () => {
409
+ const longSchema = new TypedObjectSchema(
410
+ 'com.example.app.feature.action.detail#variant',
411
+ new ObjectSchema({ value: new StringSchema({}) }),
412
+ )
413
+ const result = longSchema.safeParse({
414
+ $type: 'com.example.app.feature.action.detail#variant',
415
+ value: 'test',
416
+ })
417
+ expect(result.success).toBe(true)
418
+ })
419
+ })
420
+
421
+ describe('edge cases', () => {
422
+ it('validates object with only extra properties', () => {
423
+ const minimalSchema = new TypedObjectSchema(
424
+ 'app.bsky.test#main',
425
+ new ObjectSchema({}),
426
+ )
427
+ const result = minimalSchema.safeParse({
428
+ extra1: 'value1',
429
+ extra2: 'value2',
430
+ })
431
+ expect(result.success).toBe(true)
432
+ })
433
+
434
+ it('validates empty object with no required properties', () => {
435
+ const minimalSchema = new TypedObjectSchema(
436
+ 'app.bsky.test#main',
437
+ new ObjectSchema({}),
438
+ )
439
+ const result = minimalSchema.safeParse({})
440
+ expect(result.success).toBe(true)
441
+ })
442
+
443
+ it('validates with $type as only property', () => {
444
+ const minimalSchema = new TypedObjectSchema(
445
+ 'app.bsky.test#main',
446
+ new ObjectSchema({}),
447
+ )
448
+ const result = minimalSchema.safeParse({
449
+ $type: 'app.bsky.test#main',
450
+ })
451
+ expect(result.success).toBe(true)
452
+ })
453
+
454
+ it('rejects object with prototype properties', () => {
455
+ const obj = Object.create({ inherited: 'value' })
456
+ obj.text = 'Hello world'
457
+ const result = schema.safeParse(obj)
458
+ expect(result.success).toBe(false)
459
+ })
460
+
461
+ it('rejects Date objects', () => {
462
+ const result = schema.safeParse(new Date())
463
+ expect(result.success).toBe(false)
464
+ })
465
+
466
+ it('rejects RegExp objects', () => {
467
+ const result = schema.safeParse(/pattern/)
468
+ expect(result.success).toBe(false)
469
+ })
470
+
471
+ it('rejects Error objects', () => {
472
+ const result = schema.safeParse(new Error('test'))
473
+ expect(result.success).toBe(false)
474
+ })
475
+
476
+ it('rejects Map objects', () => {
477
+ const result = schema.safeParse(new Map())
478
+ expect(result.success).toBe(false)
479
+ })
480
+
481
+ it('rejects Set objects', () => {
482
+ const result = schema.safeParse(new Set())
483
+ expect(result.success).toBe(false)
484
+ })
485
+
486
+ it('rejects class instances', () => {
487
+ class CustomClass {
488
+ text = 'Hello'
489
+ }
490
+ const result = schema.safeParse(new CustomClass())
491
+ expect(result.success).toBe(false)
492
+ })
493
+ })
494
+
495
+ describe('integration with all property types', () => {
496
+ const fullSchema = new TypedObjectSchema(
497
+ 'app.bsky.test#full',
498
+ new ObjectSchema({
499
+ required: new StringSchema({}),
500
+ optional: new OptionalSchema(new StringSchema({})),
501
+ nullable: new NullableSchema(new StringSchema({})),
502
+ optionalNullable: new OptionalSchema(
503
+ new NullableSchema(new StringSchema({})),
504
+ ),
505
+ }),
506
+ )
507
+
508
+ it('validates with all properties present', () => {
509
+ const result = fullSchema.safeParse({
510
+ required: 'value',
511
+ optional: 'value',
512
+ nullable: 'value',
513
+ optionalNullable: 'value',
514
+ })
515
+ expect(result.success).toBe(true)
516
+ })
517
+
518
+ it('validates with only required property and nullable', () => {
519
+ const result = fullSchema.safeParse({
520
+ required: 'value',
521
+ nullable: 'value',
522
+ })
523
+ expect(result.success).toBe(true)
524
+ })
525
+
526
+ it('validates with nullable property set to null', () => {
527
+ const result = fullSchema.safeParse({
528
+ required: 'value',
529
+ nullable: null,
530
+ })
531
+ expect(result.success).toBe(true)
532
+ })
533
+
534
+ it('validates with required nullable and optional nullable set to null', () => {
535
+ const result = fullSchema.safeParse({
536
+ required: 'value',
537
+ nullable: 'value',
538
+ optionalNullable: null,
539
+ })
540
+ expect(result.success).toBe(true)
541
+ })
542
+
543
+ it('rejects when required property is missing', () => {
544
+ const result = fullSchema.safeParse({
545
+ optional: 'value',
546
+ nullable: 'value',
547
+ })
548
+ expect(result.success).toBe(false)
549
+ })
550
+
551
+ it('rejects when required property is null', () => {
552
+ const result = fullSchema.safeParse({
553
+ required: null,
554
+ })
555
+ expect(result.success).toBe(false)
556
+ })
557
+
558
+ it('rejects when required property is undefined', () => {
559
+ const result = fullSchema.safeParse({
560
+ required: undefined,
561
+ })
562
+ expect(result.success).toBe(false)
563
+ })
564
+
565
+ it('validates with $type and all properties', () => {
566
+ const result = fullSchema.safeParse({
567
+ $type: 'app.bsky.test#full',
568
+ required: 'value',
569
+ optional: 'value',
570
+ nullable: null,
571
+ optionalNullable: 'value',
572
+ })
573
+ expect(result.success).toBe(true)
574
+ })
575
+ })
576
+
577
+ describe('comparison with plain ObjectSchema', () => {
578
+ const plainSchema = new ObjectSchema({
579
+ text: new StringSchema({}),
580
+ likes: new OptionalSchema(new IntegerSchema({})),
581
+ })
582
+
583
+ it('typed schema accepts same input as plain schema', () => {
584
+ const input = { text: 'Hello', likes: 5 }
585
+ const typedResult = schema.safeParse(input)
586
+ const plainResult = plainSchema.safeParse(input)
587
+ expect(typedResult.success).toBe(plainResult.success)
588
+ })
589
+
590
+ it('typed schema adds $type enforcement', () => {
591
+ const input = { $type: 'wrong.type', text: 'Hello' }
592
+ const typedResult = schema.safeParse(input)
593
+ const plainResult = plainSchema.safeParse(input)
594
+ expect(typedResult.success).toBe(false)
595
+ expect(plainResult.success).toBe(true)
596
+ })
597
+
598
+ it('both schemas reject invalid types', () => {
599
+ const input = { text: 123 }
600
+ const typedResult = schema.safeParse(input)
601
+ const plainResult = plainSchema.safeParse(input)
602
+ expect(typedResult.success).toBe(false)
603
+ expect(plainResult.success).toBe(false)
604
+ })
605
+
606
+ it('typed schema accepts matching $type', () => {
607
+ const input = { $type: 'app.bsky.feed.post#main', text: 'Hello' }
608
+ const typedResult = schema.safeParse(input)
609
+ expect(typedResult.success).toBe(true)
610
+ })
611
+ })
612
+ })
@@ -2,51 +2,52 @@ import { isPlainObject } from '@atproto/lex-data'
2
2
  import { $Type, Simplify } from '../core.js'
3
3
  import {
4
4
  Infer,
5
+ Schema,
5
6
  ValidationResult,
6
7
  Validator,
7
8
  ValidatorContext,
8
9
  } from '../validation.js'
9
10
 
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
11
+ export type TypedObjectSchemaOutput<
12
+ T extends $Type,
13
+ S extends Validator<{ [_ in string]?: unknown }>,
14
+ > = Simplify<Infer<S> & { $type?: T }>
18
15
 
16
+ export class TypedObjectSchema<
17
+ const T extends $Type = any,
18
+ const S extends Validator<{ [_ in string]?: unknown }> = any,
19
+ > extends Schema<TypedObjectSchemaOutput<T, S>> {
19
20
  constructor(
20
- readonly $type: Type,
21
- readonly schema: Schema,
21
+ readonly $type: T,
22
+ readonly schema: S,
22
23
  ) {
23
24
  super()
24
25
  }
25
26
 
26
- isTypeOf<X extends { $type?: unknown }>(
27
+ isTypeOf<X extends Record<string, unknown>>(
27
28
  value: X,
28
- ): value is X extends { $type?: Type } ? X : never {
29
+ ): value is X extends { $type?: T } ? X : X & { $type?: T } {
29
30
  return value.$type === undefined || value.$type === this.$type
30
31
  }
31
32
 
32
- build<X extends Omit<Output, '$type'>>(
33
+ build<X extends Omit<Infer<S>, '$type'>>(
33
34
  input: X,
34
- ): Simplify<Omit<X, '$type'> & { $type: Type }> {
35
+ ): Simplify<Omit<X, '$type'> & { $type: T }> {
35
36
  return { ...input, $type: this.$type }
36
37
  }
37
38
 
38
- $isTypeOf<X extends { $type?: unknown }>(value: X) {
39
- return this.isTypeOf<X>(value)
39
+ $isTypeOf<X extends Record<string, unknown>>(value: X) {
40
+ return this.isTypeOf(value)
40
41
  }
41
42
 
42
- $build<X extends Omit<Output, '$type'>>(input: X) {
43
+ $build<X extends Omit<Infer<S>, '$type'>>(input: X) {
43
44
  return this.build<X>(input)
44
45
  }
45
46
 
46
- override validateInContext(
47
+ validateInContext(
47
48
  input: unknown,
48
49
  ctx: ValidatorContext,
49
- ): ValidationResult<Output> {
50
+ ): ValidationResult<TypedObjectSchemaOutput<T, S>> {
50
51
  if (!isPlainObject(input)) {
51
52
  return ctx.issueInvalidType(input, 'object')
52
53
  }
@@ -59,6 +60,8 @@ export class TypedObjectSchema<
59
60
  return ctx.issueInvalidPropertyValue(input, '$type', [this.$type])
60
61
  }
61
62
 
62
- return ctx.validate(input, this.schema as Validator<Output>)
63
+ return ctx.validate(input, this.schema) as ValidationResult<
64
+ TypedObjectSchemaOutput<T, S>
65
+ >
63
66
  }
64
67
  }