@atproto/lex-schema 0.0.2 → 0.0.4

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 +75 -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,823 @@
1
+ import { IntegerSchema } from './integer.js'
2
+ import { ObjectSchema } from './object.js'
3
+ import { StringSchema } from './string.js'
4
+ import { TypedObjectSchema } from './typed-object.js'
5
+ import { TypedRefSchema } from './typed-ref.js'
6
+
7
+ describe('TypedRefSchema', () => {
8
+ describe('basic validation', () => {
9
+ it('validates through a typed object reference with explicit $type', () => {
10
+ const typedObject = new TypedObjectSchema(
11
+ 'com.example.user',
12
+ new ObjectSchema({
13
+ name: new StringSchema({}),
14
+ age: new IntegerSchema({}),
15
+ }),
16
+ )
17
+
18
+ const schema = new TypedRefSchema(() => typedObject)
19
+
20
+ const result = schema.safeParse({
21
+ $type: 'com.example.user',
22
+ name: 'Alice',
23
+ age: 30,
24
+ })
25
+
26
+ expect(result.success).toBe(true)
27
+ if (result.success) {
28
+ expect(result.value.$type).toBe('com.example.user')
29
+ }
30
+ })
31
+
32
+ it('validates through a typed object with explicit $type', () => {
33
+ const typedObject = new TypedObjectSchema(
34
+ 'com.example.user',
35
+ new ObjectSchema({
36
+ name: new StringSchema({}),
37
+ age: new IntegerSchema({}),
38
+ }),
39
+ )
40
+
41
+ const schema = new TypedRefSchema(() => typedObject)
42
+
43
+ const result = schema.safeParse({
44
+ $type: 'com.example.user',
45
+ name: 'Alice',
46
+ age: 30,
47
+ })
48
+
49
+ expect(result.success).toBe(true)
50
+ if (result.success) {
51
+ expect(result.value.$type).toBe('com.example.user')
52
+ }
53
+ })
54
+
55
+ it('rejects input with wrong $type', () => {
56
+ const typedObject = new TypedObjectSchema(
57
+ 'com.example.user',
58
+ new ObjectSchema({
59
+ name: new StringSchema({}),
60
+ }),
61
+ )
62
+
63
+ const schema = new TypedRefSchema(() => typedObject)
64
+
65
+ const result = schema.safeParse({
66
+ $type: 'com.example.wrong',
67
+ name: 'Alice',
68
+ })
69
+
70
+ expect(result.success).toBe(false)
71
+ })
72
+
73
+ it('rejects invalid input through reference', () => {
74
+ const typedObject = new TypedObjectSchema(
75
+ 'com.example.user',
76
+ new ObjectSchema({
77
+ name: new StringSchema({}),
78
+ age: new IntegerSchema({}),
79
+ }),
80
+ )
81
+
82
+ const schema = new TypedRefSchema(() => typedObject)
83
+
84
+ const result = schema.safeParse({
85
+ $type: 'com.example.user',
86
+ name: 'Alice',
87
+ age: 'thirty',
88
+ })
89
+
90
+ expect(result.success).toBe(false)
91
+ })
92
+
93
+ it('rejects non-objects through reference', () => {
94
+ const typedObject = new TypedObjectSchema(
95
+ 'com.example.value',
96
+ new ObjectSchema({
97
+ value: new StringSchema({}),
98
+ }),
99
+ )
100
+
101
+ const schema = new TypedRefSchema(() => typedObject)
102
+
103
+ const result = schema.safeParse('not an object')
104
+ expect(result.success).toBe(false)
105
+ })
106
+
107
+ it('rejects null through reference', () => {
108
+ const typedObject = new TypedObjectSchema(
109
+ 'com.example.user',
110
+ new ObjectSchema({
111
+ name: new StringSchema({}),
112
+ }),
113
+ )
114
+
115
+ const schema = new TypedRefSchema(() => typedObject)
116
+
117
+ const result = schema.safeParse(null)
118
+ expect(result.success).toBe(false)
119
+ })
120
+
121
+ it('rejects undefined through reference', () => {
122
+ const typedObject = new TypedObjectSchema(
123
+ 'com.example.user',
124
+ new ObjectSchema({
125
+ name: new StringSchema({}),
126
+ }),
127
+ )
128
+
129
+ const schema = new TypedRefSchema(() => typedObject)
130
+
131
+ const result = schema.safeParse(undefined)
132
+ expect(result.success).toBe(false)
133
+ })
134
+ })
135
+
136
+ describe('$type property', () => {
137
+ it('exposes the $type from the referenced schema', () => {
138
+ const typedObject = new TypedObjectSchema(
139
+ 'com.example.post',
140
+ new ObjectSchema({
141
+ text: new StringSchema({}),
142
+ }),
143
+ )
144
+
145
+ const schema = new TypedRefSchema(() => typedObject)
146
+
147
+ expect(schema.$type).toBe('com.example.post')
148
+ })
149
+
150
+ it('validates that output has correct $type', () => {
151
+ const typedObject = new TypedObjectSchema(
152
+ 'com.example.like',
153
+ new ObjectSchema({
154
+ subject: new StringSchema({}),
155
+ }),
156
+ )
157
+
158
+ const schema = new TypedRefSchema(() => typedObject)
159
+
160
+ const result = schema.safeParse({
161
+ $type: 'com.example.like',
162
+ subject: 'at://did:plc:abc/app.bsky.feed.post/123',
163
+ })
164
+
165
+ expect(result.success).toBe(true)
166
+ if (result.success) {
167
+ expect(result.value.$type).toBe('com.example.like')
168
+ }
169
+ })
170
+
171
+ it('ensures $type matches expected value', () => {
172
+ const typedObject = new TypedObjectSchema(
173
+ 'com.example.follow',
174
+ new ObjectSchema({
175
+ subject: new StringSchema({}),
176
+ }),
177
+ )
178
+
179
+ const schema = new TypedRefSchema(() => typedObject)
180
+
181
+ // Try to pass wrong $type
182
+ const result = schema.safeParse({
183
+ $type: 'com.example.block',
184
+ subject: 'did:plc:abc',
185
+ })
186
+
187
+ expect(result.success).toBe(false)
188
+ })
189
+ })
190
+
191
+ describe('lazy schema resolution', () => {
192
+ it('does not call getter until first validation', () => {
193
+ let getterCalled = false
194
+
195
+ const schema = new TypedRefSchema(() => {
196
+ getterCalled = true
197
+ return new TypedObjectSchema(
198
+ 'com.example.test',
199
+ new ObjectSchema({
200
+ value: new StringSchema({}),
201
+ }),
202
+ )
203
+ })
204
+
205
+ expect(getterCalled).toBe(false)
206
+
207
+ schema.safeParse({ value: 'test' })
208
+ expect(getterCalled).toBe(true)
209
+ })
210
+
211
+ it('does not call getter until $type is accessed', () => {
212
+ let getterCalled = false
213
+
214
+ const schema = new TypedRefSchema(() => {
215
+ getterCalled = true
216
+ return new TypedObjectSchema(
217
+ 'com.example.test',
218
+ new ObjectSchema({
219
+ value: new StringSchema({}),
220
+ }),
221
+ )
222
+ })
223
+
224
+ expect(getterCalled).toBe(false)
225
+
226
+ // Access $type should trigger getter
227
+ const type = schema.$type
228
+ expect(getterCalled).toBe(true)
229
+ expect(type).toBe('com.example.test')
230
+ })
231
+
232
+ it('caches the resolved schema', () => {
233
+ let callCount = 0
234
+
235
+ const schema = new TypedRefSchema(() => {
236
+ callCount++
237
+ return new TypedObjectSchema(
238
+ 'com.example.test',
239
+ new ObjectSchema({
240
+ value: new StringSchema({}),
241
+ }),
242
+ )
243
+ })
244
+
245
+ schema.safeParse({ value: 'first' })
246
+ schema.safeParse({ value: 'second' })
247
+ schema.safeParse({ value: 'third' })
248
+
249
+ expect(callCount).toBe(1)
250
+ })
251
+
252
+ it('caches schema after $type access', () => {
253
+ let callCount = 0
254
+
255
+ const schema = new TypedRefSchema(() => {
256
+ callCount++
257
+ return new TypedObjectSchema(
258
+ 'com.example.test',
259
+ new ObjectSchema({
260
+ value: new StringSchema({}),
261
+ }),
262
+ )
263
+ })
264
+
265
+ // Access $type first
266
+ schema.$type
267
+ expect(callCount).toBe(1)
268
+
269
+ // Then validate multiple times
270
+ schema.safeParse({ value: 'test1' })
271
+ schema.safeParse({ value: 'test2' })
272
+
273
+ expect(callCount).toBe(1)
274
+ })
275
+
276
+ it('throws error if getter is called recursively', () => {
277
+ // @ts-expect-error
278
+ const schema = new TypedRefSchema(() => {
279
+ // This would cause infinite recursion if not protected
280
+ return schema.schema
281
+ })
282
+
283
+ expect(() => {
284
+ schema.safeParse({ value: 'test' })
285
+ }).toThrow()
286
+ })
287
+ })
288
+
289
+ describe('with constrained schemas', () => {
290
+ it('validates typed object with string constraints', () => {
291
+ const typedObject = new TypedObjectSchema(
292
+ 'com.example.post',
293
+ new ObjectSchema({
294
+ text: new StringSchema({ minLength: 1, maxLength: 300 }),
295
+ }),
296
+ )
297
+
298
+ const schema = new TypedRefSchema(() => typedObject)
299
+
300
+ const result = schema.safeParse({
301
+ $type: 'com.example.post',
302
+ text: 'This is a valid post',
303
+ })
304
+
305
+ expect(result.success).toBe(true)
306
+ })
307
+
308
+ it('rejects typed object violating string constraints', () => {
309
+ const typedObject = new TypedObjectSchema(
310
+ 'com.example.post',
311
+ new ObjectSchema({
312
+ text: new StringSchema({ minLength: 1, maxLength: 300 }),
313
+ }),
314
+ )
315
+
316
+ const schema = new TypedRefSchema(() => typedObject)
317
+
318
+ const result = schema.safeParse({
319
+ $type: 'com.example.post',
320
+ text: '',
321
+ })
322
+
323
+ expect(result.success).toBe(false)
324
+ })
325
+
326
+ it('validates typed object with integer constraints', () => {
327
+ const typedObject = new TypedObjectSchema(
328
+ 'com.example.rating',
329
+ new ObjectSchema({
330
+ score: new IntegerSchema({ minimum: 1, maximum: 5 }),
331
+ }),
332
+ )
333
+
334
+ const schema = new TypedRefSchema(() => typedObject)
335
+
336
+ const result = schema.safeParse({
337
+ $type: 'com.example.rating',
338
+ score: 4,
339
+ })
340
+
341
+ expect(result.success).toBe(true)
342
+ })
343
+
344
+ it('rejects typed object violating integer constraints', () => {
345
+ const typedObject = new TypedObjectSchema(
346
+ 'com.example.rating',
347
+ new ObjectSchema({
348
+ score: new IntegerSchema({ minimum: 1, maximum: 5 }),
349
+ }),
350
+ )
351
+
352
+ const schema = new TypedRefSchema(() => typedObject)
353
+
354
+ const result = schema.safeParse({
355
+ $type: 'com.example.rating',
356
+ score: 10,
357
+ })
358
+
359
+ expect(result.success).toBe(false)
360
+ })
361
+ })
362
+
363
+ describe('multiple validations', () => {
364
+ it('validates multiple inputs correctly', () => {
365
+ const typedObject = new TypedObjectSchema(
366
+ 'com.example.user',
367
+ new ObjectSchema({
368
+ name: new StringSchema({ minLength: 2 }),
369
+ }),
370
+ )
371
+
372
+ const schema = new TypedRefSchema(() => typedObject)
373
+
374
+ const result1 = schema.safeParse({
375
+ $type: 'com.example.user',
376
+ name: 'Alice',
377
+ })
378
+ expect(result1.success).toBe(true)
379
+
380
+ const result2 = schema.safeParse({ $type: 'com.example.user', name: 'A' })
381
+ expect(result2.success).toBe(false)
382
+
383
+ const result3 = schema.safeParse({
384
+ $type: 'com.example.user',
385
+ name: 'Bob',
386
+ })
387
+ expect(result3.success).toBe(true)
388
+
389
+ const result4 = schema.safeParse({ $type: 'com.example.user', name: '' })
390
+ expect(result4.success).toBe(false)
391
+ })
392
+
393
+ it('handles different types of validation failures', () => {
394
+ const typedObject = new TypedObjectSchema(
395
+ 'com.example.user',
396
+ new ObjectSchema({
397
+ name: new StringSchema({ minLength: 2 }),
398
+ age: new IntegerSchema({ minimum: 0, maximum: 150 }),
399
+ }),
400
+ )
401
+
402
+ const schema = new TypedRefSchema(() => typedObject)
403
+
404
+ const result1 = schema.safeParse({
405
+ $type: 'com.example.user',
406
+ name: 'A',
407
+ age: 25,
408
+ })
409
+ expect(result1.success).toBe(false)
410
+
411
+ const result2 = schema.safeParse({
412
+ $type: 'com.example.user',
413
+ name: 'Alice',
414
+ age: 200,
415
+ })
416
+ expect(result2.success).toBe(false)
417
+
418
+ const result3 = schema.safeParse({
419
+ $type: 'com.example.user',
420
+ name: 'Alice',
421
+ age: 25,
422
+ })
423
+ expect(result3.success).toBe(true)
424
+
425
+ const result4 = schema.safeParse({
426
+ $type: 'com.example.user',
427
+ name: 'Alice',
428
+ })
429
+ expect(result4.success).toBe(false)
430
+ })
431
+
432
+ it('validates same input multiple times consistently', () => {
433
+ const typedObject = new TypedObjectSchema(
434
+ 'com.example.post',
435
+ new ObjectSchema({
436
+ text: new StringSchema({}),
437
+ }),
438
+ )
439
+
440
+ const schema = new TypedRefSchema(() => typedObject)
441
+
442
+ const input = { $type: 'com.example.post', text: 'Hello world' }
443
+
444
+ const result1 = schema.safeParse(input)
445
+ const result2 = schema.safeParse(input)
446
+ const result3 = schema.safeParse(input)
447
+
448
+ expect(result1.success).toBe(true)
449
+ expect(result2.success).toBe(true)
450
+ expect(result3.success).toBe(true)
451
+ })
452
+ })
453
+
454
+ describe('edge cases', () => {
455
+ it('handles empty object validation', () => {
456
+ const typedObject = new TypedObjectSchema(
457
+ 'com.example.empty',
458
+ new ObjectSchema({}),
459
+ )
460
+
461
+ const schema = new TypedRefSchema(() => typedObject)
462
+
463
+ const result = schema.safeParse({ $type: 'com.example.empty' })
464
+ expect(result.success).toBe(true)
465
+ if (result.success) {
466
+ expect(result.value.$type).toBe('com.example.empty')
467
+ }
468
+ })
469
+
470
+ it('rejects arrays', () => {
471
+ const typedObject = new TypedObjectSchema(
472
+ 'com.example.test',
473
+ new ObjectSchema({
474
+ value: new StringSchema({}),
475
+ }),
476
+ )
477
+
478
+ const schema = new TypedRefSchema(() => typedObject)
479
+
480
+ const result = schema.safeParse([{ value: 'test' }])
481
+ expect(result.success).toBe(false)
482
+ })
483
+
484
+ it('rejects primitive values', () => {
485
+ const typedObject = new TypedObjectSchema(
486
+ 'com.example.test',
487
+ new ObjectSchema({
488
+ value: new StringSchema({}),
489
+ }),
490
+ )
491
+
492
+ const schema = new TypedRefSchema(() => typedObject)
493
+
494
+ const result1 = schema.safeParse('string')
495
+ expect(result1.success).toBe(false)
496
+
497
+ const result2 = schema.safeParse(123)
498
+ expect(result2.success).toBe(false)
499
+
500
+ const result3 = schema.safeParse(true)
501
+ expect(result3.success).toBe(false)
502
+ })
503
+
504
+ it('handles objects with extra properties', () => {
505
+ const typedObject = new TypedObjectSchema(
506
+ 'com.example.user',
507
+ new ObjectSchema({
508
+ name: new StringSchema({}),
509
+ }),
510
+ )
511
+
512
+ const schema = new TypedRefSchema(() => typedObject)
513
+
514
+ const result = schema.safeParse({
515
+ $type: 'com.example.user',
516
+ name: 'Alice',
517
+ extra: 'property',
518
+ another: 'value',
519
+ })
520
+
521
+ expect(result.success).toBe(true)
522
+ })
523
+
524
+ it('validates with zero values', () => {
525
+ const typedObject = new TypedObjectSchema(
526
+ 'com.example.counter',
527
+ new ObjectSchema({
528
+ count: new IntegerSchema({}),
529
+ }),
530
+ )
531
+
532
+ const schema = new TypedRefSchema(() => typedObject)
533
+
534
+ const result = schema.safeParse({
535
+ $type: 'com.example.counter',
536
+ count: 0,
537
+ })
538
+ expect(result.success).toBe(true)
539
+ })
540
+
541
+ it('validates with empty strings', () => {
542
+ const typedObject = new TypedObjectSchema(
543
+ 'com.example.text',
544
+ new ObjectSchema({
545
+ content: new StringSchema({}),
546
+ }),
547
+ )
548
+
549
+ const schema = new TypedRefSchema(() => typedObject)
550
+
551
+ const result = schema.safeParse({
552
+ $type: 'com.example.text',
553
+ content: '',
554
+ })
555
+ expect(result.success).toBe(true)
556
+ })
557
+
558
+ it('rejects NaN in integer fields', () => {
559
+ const typedObject = new TypedObjectSchema(
560
+ 'com.example.number',
561
+ new ObjectSchema({
562
+ value: new IntegerSchema({}),
563
+ }),
564
+ )
565
+
566
+ const schema = new TypedRefSchema(() => typedObject)
567
+
568
+ const result = schema.safeParse({
569
+ $type: 'com.example.number',
570
+ value: NaN,
571
+ })
572
+ expect(result.success).toBe(false)
573
+ })
574
+
575
+ it('rejects Infinity in integer fields', () => {
576
+ const typedObject = new TypedObjectSchema(
577
+ 'com.example.number',
578
+ new ObjectSchema({
579
+ value: new IntegerSchema({}),
580
+ }),
581
+ )
582
+
583
+ const schema = new TypedRefSchema(() => typedObject)
584
+
585
+ const result = schema.safeParse({
586
+ $type: 'com.example.number',
587
+ value: Infinity,
588
+ })
589
+ expect(result.success).toBe(false)
590
+ })
591
+ })
592
+
593
+ describe('nested references', () => {
594
+ it('validates through nested TypedRefSchema', () => {
595
+ const typedObject = new TypedObjectSchema(
596
+ 'com.example.user',
597
+ new ObjectSchema({
598
+ name: new StringSchema({ minLength: 2 }),
599
+ }),
600
+ )
601
+
602
+ const innerRef = new TypedRefSchema(() => typedObject)
603
+ const outerRef = new TypedRefSchema(() => innerRef.schema)
604
+
605
+ const result = outerRef.safeParse({
606
+ $type: 'com.example.user',
607
+ name: 'Alice',
608
+ })
609
+ expect(result.success).toBe(true)
610
+ if (result.success) {
611
+ expect(result.value.$type).toBe('com.example.user')
612
+ }
613
+ })
614
+
615
+ it('validates with objects containing TypedRef fields', () => {
616
+ const innerTyped = new TypedObjectSchema(
617
+ 'com.example.profile',
618
+ new ObjectSchema({
619
+ bio: new StringSchema({}),
620
+ }),
621
+ )
622
+
623
+ const outerTyped = new TypedObjectSchema(
624
+ 'com.example.user',
625
+ new ObjectSchema({
626
+ name: new StringSchema({}),
627
+ profile: new TypedRefSchema(() => innerTyped),
628
+ }),
629
+ )
630
+
631
+ const schema = new TypedRefSchema(() => outerTyped)
632
+
633
+ const result = schema.safeParse({
634
+ $type: 'com.example.user',
635
+ name: 'Alice',
636
+ profile: {
637
+ $type: 'com.example.profile',
638
+ bio: 'Software developer',
639
+ },
640
+ })
641
+
642
+ expect(result.success).toBe(true)
643
+ if (result.success) {
644
+ expect(result.value.$type).toBe('com.example.user')
645
+ expect(result.value.profile.$type).toBe('com.example.profile')
646
+ }
647
+ })
648
+
649
+ it('rejects nested objects with wrong $type', () => {
650
+ const innerTyped = new TypedObjectSchema(
651
+ 'com.example.profile',
652
+ new ObjectSchema({
653
+ bio: new StringSchema({}),
654
+ }),
655
+ )
656
+
657
+ const outerTyped = new TypedObjectSchema(
658
+ 'com.example.user',
659
+ new ObjectSchema({
660
+ name: new StringSchema({}),
661
+ profile: new TypedRefSchema(() => innerTyped),
662
+ }),
663
+ )
664
+
665
+ const schema = new TypedRefSchema(() => outerTyped)
666
+
667
+ const result = schema.safeParse({
668
+ $type: 'com.example.user',
669
+ name: 'Alice',
670
+ profile: {
671
+ $type: 'com.example.wrongtype',
672
+ bio: 'Software developer',
673
+ },
674
+ })
675
+
676
+ expect(result.success).toBe(false)
677
+ })
678
+ })
679
+
680
+ describe('schema property access', () => {
681
+ it('allows direct access to resolved schema', () => {
682
+ const typedObject = new TypedObjectSchema(
683
+ 'com.example.test',
684
+ new ObjectSchema({
685
+ value: new StringSchema({}),
686
+ }),
687
+ )
688
+
689
+ const refSchema = new TypedRefSchema(() => typedObject)
690
+
691
+ const resolved = refSchema.schema
692
+ expect(resolved).toBe(typedObject)
693
+ expect(resolved.$type).toBe('com.example.test')
694
+ })
695
+
696
+ it('returns same instance on multiple schema property accesses', () => {
697
+ const typedObject = new TypedObjectSchema(
698
+ 'com.example.test',
699
+ new ObjectSchema({
700
+ value: new StringSchema({}),
701
+ }),
702
+ )
703
+
704
+ const refSchema = new TypedRefSchema(() => typedObject)
705
+
706
+ const first = refSchema.schema
707
+ const second = refSchema.schema
708
+ const third = refSchema.schema
709
+
710
+ expect(first).toBe(second)
711
+ expect(second).toBe(third)
712
+ expect(first.$type).toBe('com.example.test')
713
+ })
714
+
715
+ it('resolves schema before validation', () => {
716
+ let resolved = false
717
+
718
+ const refSchema = new TypedRefSchema(() => {
719
+ resolved = true
720
+ return new TypedObjectSchema(
721
+ 'com.example.test',
722
+ new ObjectSchema({
723
+ value: new StringSchema({}),
724
+ }),
725
+ )
726
+ })
727
+
728
+ expect(resolved).toBe(false)
729
+
730
+ const schemaValue = refSchema.schema
731
+ expect(resolved).toBe(true)
732
+ expect(schemaValue).toBeDefined()
733
+ expect(schemaValue.$type).toBe('com.example.test')
734
+ })
735
+ })
736
+
737
+ describe('complex object structures', () => {
738
+ it('validates complex nested structure', () => {
739
+ const typedObject = new TypedObjectSchema(
740
+ 'com.example.post',
741
+ new ObjectSchema({
742
+ text: new StringSchema({ minLength: 1, maxLength: 300 }),
743
+ createdAt: new StringSchema({ format: 'datetime' }),
744
+ likeCount: new IntegerSchema({ minimum: 0 }),
745
+ }),
746
+ )
747
+
748
+ const schema = new TypedRefSchema(() => typedObject)
749
+
750
+ const result = schema.safeParse({
751
+ $type: 'com.example.post',
752
+ text: 'Hello world!',
753
+ createdAt: '2023-01-01T00:00:00Z',
754
+ likeCount: 42,
755
+ })
756
+
757
+ expect(result.success).toBe(true)
758
+ if (result.success) {
759
+ expect(result.value.$type).toBe('com.example.post')
760
+ expect(result.value.text).toBe('Hello world!')
761
+ expect(result.value.likeCount).toBe(42)
762
+ }
763
+ })
764
+
765
+ it('validates structure with multiple property types', () => {
766
+ const typedObject = new TypedObjectSchema(
767
+ 'com.example.record',
768
+ new ObjectSchema({
769
+ id: new StringSchema({ format: 'nsid' }),
770
+ count: new IntegerSchema({ minimum: 0 }),
771
+ flag: new StringSchema({}),
772
+ }),
773
+ )
774
+
775
+ const schema = new TypedRefSchema(() => typedObject)
776
+
777
+ const result = schema.safeParse({
778
+ $type: 'com.example.record',
779
+ id: 'com.example.feed.post',
780
+ count: 100,
781
+ flag: 'active',
782
+ })
783
+
784
+ expect(result.success).toBe(true)
785
+ })
786
+
787
+ it('rejects structure with any invalid property', () => {
788
+ const typedObject = new TypedObjectSchema(
789
+ 'com.example.record',
790
+ new ObjectSchema({
791
+ name: new StringSchema({ minLength: 1 }),
792
+ count: new IntegerSchema({ minimum: 0 }),
793
+ }),
794
+ )
795
+
796
+ const schema = new TypedRefSchema(() => typedObject)
797
+
798
+ // Valid name, invalid count
799
+ const result1 = schema.safeParse({
800
+ $type: 'com.example.record',
801
+ name: 'test',
802
+ count: -5,
803
+ })
804
+ expect(result1.success).toBe(false)
805
+
806
+ // Invalid name, valid count
807
+ const result2 = schema.safeParse({
808
+ $type: 'com.example.record',
809
+ name: '',
810
+ count: 5,
811
+ })
812
+ expect(result2.success).toBe(false)
813
+
814
+ // Both invalid
815
+ const result3 = schema.safeParse({
816
+ $type: 'com.example.record',
817
+ name: '',
818
+ count: -5,
819
+ })
820
+ expect(result3.success).toBe(false)
821
+ })
822
+ })
823
+ })