@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,582 @@
1
+ import { ArraySchema } from './array.js'
2
+ import { BooleanSchema } from './boolean.js'
3
+ import { IntegerSchema } from './integer.js'
4
+ import { OptionalSchema } from './optional.js'
5
+ import { ParamsSchema } from './params.js'
6
+ import { StringSchema } from './string.js'
7
+
8
+ describe('ParamsSchema', () => {
9
+ describe('basic validation', () => {
10
+ const schema = new ParamsSchema({
11
+ name: new StringSchema({}),
12
+ age: new IntegerSchema({}),
13
+ })
14
+
15
+ it('validates plain objects with required params', () => {
16
+ const result = schema.safeParse({
17
+ name: 'Alice',
18
+ age: 30,
19
+ })
20
+ expect(result.success).toBe(true)
21
+ })
22
+
23
+ it('rejects non-objects', () => {
24
+ const result = schema.safeParse('not an object')
25
+ expect(result.success).toBe(false)
26
+ })
27
+
28
+ it('rejects null values', () => {
29
+ const result = schema.safeParse(null)
30
+ expect(result.success).toBe(false)
31
+ })
32
+
33
+ it('rejects undefined values', () => {
34
+ const result = schema.safeParse(undefined)
35
+ expect(result.success).toBe(false)
36
+ })
37
+
38
+ it('rejects arrays', () => {
39
+ const result = schema.safeParse(['Alice', 30])
40
+ expect(result.success).toBe(false)
41
+ })
42
+
43
+ it('rejects missing required properties', () => {
44
+ const result = schema.safeParse({
45
+ name: 'Alice',
46
+ })
47
+ expect(result.success).toBe(false)
48
+ })
49
+
50
+ it('rejects invalid property types', () => {
51
+ const result = schema.safeParse({
52
+ name: 'Alice',
53
+ age: 'thirty',
54
+ })
55
+ expect(result.success).toBe(false)
56
+ })
57
+ })
58
+
59
+ describe('optional parameters', () => {
60
+ const schema = new ParamsSchema({
61
+ name: new StringSchema({}),
62
+ age: new OptionalSchema(new IntegerSchema({})),
63
+ active: new OptionalSchema(new BooleanSchema({})),
64
+ })
65
+
66
+ it('validates with all parameters present', () => {
67
+ const result = schema.safeParse({
68
+ name: 'Alice',
69
+ age: 30,
70
+ active: true,
71
+ })
72
+ expect(result.success).toBe(true)
73
+ })
74
+
75
+ it('validates with only required parameters', () => {
76
+ const result = schema.safeParse({
77
+ name: 'Alice',
78
+ })
79
+ expect(result.success).toBe(true)
80
+ })
81
+
82
+ it('validates with some optional parameters', () => {
83
+ const result = schema.safeParse({
84
+ name: 'Alice',
85
+ age: 30,
86
+ })
87
+ expect(result.success).toBe(true)
88
+ })
89
+
90
+ it('rejects when required parameter is missing', () => {
91
+ const result = schema.safeParse({
92
+ age: 30,
93
+ active: true,
94
+ })
95
+ expect(result.success).toBe(false)
96
+ })
97
+ })
98
+
99
+ describe('additional unspecified parameters', () => {
100
+ const schema = new ParamsSchema({
101
+ name: new StringSchema({}),
102
+ })
103
+
104
+ it('accepts string values in additional params', () => {
105
+ const result = schema.safeParse({
106
+ name: 'Alice',
107
+ extra: 'value',
108
+ })
109
+ expect(result.success).toBe(true)
110
+ })
111
+
112
+ it('accepts boolean values in additional params', () => {
113
+ const result = schema.safeParse({
114
+ name: 'Alice',
115
+ flag: true,
116
+ })
117
+ expect(result.success).toBe(true)
118
+ })
119
+
120
+ it('accepts integer values in additional params', () => {
121
+ const result = schema.safeParse({
122
+ name: 'Alice',
123
+ count: 42,
124
+ })
125
+ expect(result.success).toBe(true)
126
+ })
127
+
128
+ it('accepts array values in additional params', () => {
129
+ const result = schema.safeParse({
130
+ name: 'Alice',
131
+ tags: ['tag1', 'tag2'],
132
+ })
133
+ expect(result.success).toBe(true)
134
+ })
135
+
136
+ it('accepts multiple additional params', () => {
137
+ const result = schema.safeParse({
138
+ name: 'Alice',
139
+ extra1: 'value',
140
+ extra2: 42,
141
+ extra3: true,
142
+ })
143
+ expect(result.success).toBe(true)
144
+ })
145
+
146
+ it('rejects null values in additional params', () => {
147
+ const result = schema.safeParse({
148
+ name: 'Alice',
149
+ extra: null,
150
+ })
151
+ expect(result.success).toBe(false)
152
+ })
153
+
154
+ it('rejects object values in additional params', () => {
155
+ const result = schema.safeParse({
156
+ name: 'Alice',
157
+ extra: { nested: 'object' },
158
+ })
159
+ expect(result.success).toBe(false)
160
+ })
161
+
162
+ it('rejects undefined values in additional params', () => {
163
+ const result = schema.safeParse({
164
+ name: 'Alice',
165
+ extra: undefined,
166
+ })
167
+ expect(result.success).toBe(false)
168
+ })
169
+ })
170
+
171
+ describe('parameter types', () => {
172
+ describe('string parameters', () => {
173
+ const schema = new ParamsSchema({
174
+ text: new StringSchema({}),
175
+ })
176
+
177
+ it('validates string values', () => {
178
+ const result = schema.safeParse({ text: 'hello' })
179
+ expect(result.success).toBe(true)
180
+ })
181
+
182
+ it('validates empty strings', () => {
183
+ const result = schema.safeParse({ text: '' })
184
+ expect(result.success).toBe(true)
185
+ })
186
+
187
+ it('rejects non-string values', () => {
188
+ const result = schema.safeParse({ text: 123 })
189
+ expect(result.success).toBe(false)
190
+ })
191
+ })
192
+
193
+ describe('integer parameters', () => {
194
+ const schema = new ParamsSchema({
195
+ count: new IntegerSchema({}),
196
+ })
197
+
198
+ it('validates integer values', () => {
199
+ const result = schema.safeParse({ count: 42 })
200
+ expect(result.success).toBe(true)
201
+ })
202
+
203
+ it('validates zero', () => {
204
+ const result = schema.safeParse({ count: 0 })
205
+ expect(result.success).toBe(true)
206
+ })
207
+
208
+ it('validates negative integers', () => {
209
+ const result = schema.safeParse({ count: -10 })
210
+ expect(result.success).toBe(true)
211
+ })
212
+
213
+ it('rejects non-integer values', () => {
214
+ const result = schema.safeParse({ count: 'not a number' })
215
+ expect(result.success).toBe(false)
216
+ })
217
+ })
218
+
219
+ describe('boolean parameters', () => {
220
+ const schema = new ParamsSchema({
221
+ flag: new BooleanSchema({}),
222
+ })
223
+
224
+ it('validates true', () => {
225
+ const result = schema.safeParse({ flag: true })
226
+ expect(result.success).toBe(true)
227
+ })
228
+
229
+ it('validates false', () => {
230
+ const result = schema.safeParse({ flag: false })
231
+ expect(result.success).toBe(true)
232
+ })
233
+
234
+ it('rejects non-boolean values', () => {
235
+ const result = schema.safeParse({ flag: 'true' })
236
+ expect(result.success).toBe(false)
237
+ })
238
+ })
239
+
240
+ describe('array parameters', () => {
241
+ const schema = new ParamsSchema({
242
+ tags: new ArraySchema(new StringSchema({})),
243
+ })
244
+
245
+ it('validates string arrays', () => {
246
+ const result = schema.safeParse({ tags: ['tag1', 'tag2'] })
247
+ expect(result.success).toBe(true)
248
+ })
249
+
250
+ it('validates empty arrays', () => {
251
+ const result = schema.safeParse({ tags: [] })
252
+ expect(result.success).toBe(true)
253
+ })
254
+
255
+ it('rejects arrays with invalid items', () => {
256
+ const result = schema.safeParse({ tags: ['tag1', 123] })
257
+ expect(result.success).toBe(false)
258
+ })
259
+ })
260
+ })
261
+
262
+ describe('fromURLSearchParams', () => {
263
+ const schema = new ParamsSchema({
264
+ name: new StringSchema({}),
265
+ age: new OptionalSchema(new IntegerSchema({})),
266
+ active: new OptionalSchema(new BooleanSchema({})),
267
+ })
268
+
269
+ it('parses string parameters', () => {
270
+ const urlParams = new URLSearchParams('name=Alice')
271
+ const result = schema.fromURLSearchParams(urlParams)
272
+ expect(result).toEqual({ name: 'Alice' })
273
+ })
274
+
275
+ it('parses and coerces boolean true', () => {
276
+ const urlParams = new URLSearchParams('name=Alice&active=true')
277
+ const result = schema.fromURLSearchParams(urlParams)
278
+ expect(result).toEqual({ name: 'Alice', active: true })
279
+ })
280
+
281
+ it('parses and coerces boolean false', () => {
282
+ const urlParams = new URLSearchParams('name=Alice&active=false')
283
+ const result = schema.fromURLSearchParams(urlParams)
284
+ expect(result).toEqual({ name: 'Alice', active: false })
285
+ })
286
+
287
+ it('parses and coerces integer values', () => {
288
+ const urlParams = new URLSearchParams('name=Alice&age=30')
289
+ const result = schema.fromURLSearchParams(urlParams)
290
+ expect(result).toEqual({ name: 'Alice', age: 30 })
291
+ })
292
+
293
+ it('parses and coerces negative integers', () => {
294
+ const urlParams = new URLSearchParams('name=Alice&age=-5')
295
+ const result = schema.fromURLSearchParams(urlParams)
296
+ expect(result).toEqual({ name: 'Alice', age: -5 })
297
+ })
298
+
299
+ it('does not coerce non-integer numbers', () => {
300
+ const urlParams = new URLSearchParams('name=Alice&extra=3.14')
301
+ const result = schema.fromURLSearchParams(urlParams)
302
+ expect(result).toEqual({ name: 'Alice', extra: '3.14' })
303
+ })
304
+
305
+ it('keeps string values for string schema even if they look like numbers', () => {
306
+ const urlParams = new URLSearchParams('name=123')
307
+ const result = schema.fromURLSearchParams(urlParams)
308
+ expect(result).toEqual({ name: '123' })
309
+ })
310
+
311
+ it('parses multiple values as array', () => {
312
+ const urlParams = new URLSearchParams('name=Alice&tag=one&tag=two')
313
+ const result = schema.fromURLSearchParams(urlParams)
314
+ expect(result).toEqual({ name: 'Alice', tag: ['one', 'two'] })
315
+ })
316
+
317
+ it('coerces array values correctly', () => {
318
+ const urlParams = new URLSearchParams('name=Alice&num=1&num=2&num=3')
319
+ const result = schema.fromURLSearchParams(urlParams)
320
+ expect(result).toEqual({ name: 'Alice', num: [1, 2, 3] })
321
+ })
322
+
323
+ it('handles mixed types in arrays', () => {
324
+ const urlParams = new URLSearchParams(
325
+ 'name=Alice&val=true&val=123&val=text',
326
+ )
327
+ const result = schema.fromURLSearchParams(urlParams)
328
+ expect(result).toEqual({ name: 'Alice', val: [true, 123, 'text'] })
329
+ })
330
+
331
+ it('handles empty URLSearchParams', () => {
332
+ const urlParams = new URLSearchParams()
333
+ expect(() => schema.fromURLSearchParams(urlParams)).toThrow()
334
+ })
335
+
336
+ it('handles multiple parameters', () => {
337
+ const urlParams = new URLSearchParams(
338
+ 'name=Alice&age=30&active=true&extra=value',
339
+ )
340
+ const result = schema.fromURLSearchParams(urlParams)
341
+ expect(result).toEqual({
342
+ name: 'Alice',
343
+ age: 30,
344
+ active: true,
345
+ extra: 'value',
346
+ })
347
+ })
348
+ })
349
+
350
+ describe('toURLSearchParams', () => {
351
+ const schema = new ParamsSchema({
352
+ name: new StringSchema({}),
353
+ age: new OptionalSchema(new IntegerSchema({})),
354
+ active: new OptionalSchema(new BooleanSchema({})),
355
+ })
356
+
357
+ it('converts string parameters', () => {
358
+ const result = schema.toURLSearchParams({ name: 'Alice' })
359
+ expect(result.toString()).toBe('name=Alice')
360
+ })
361
+
362
+ it('converts integer parameters', () => {
363
+ const result = schema.toURLSearchParams({ name: 'Alice', age: 30 })
364
+ expect(result.toString()).toBe('name=Alice&age=30')
365
+ })
366
+
367
+ it('converts boolean parameters', () => {
368
+ const result = schema.toURLSearchParams({ name: 'Alice', active: true })
369
+ expect(result.toString()).toBe('name=Alice&active=true')
370
+ })
371
+
372
+ it('converts multiple parameters', () => {
373
+ const result = schema.toURLSearchParams({
374
+ name: 'Alice',
375
+ age: 30,
376
+ active: true,
377
+ })
378
+ expect(result.toString()).toBe('name=Alice&age=30&active=true')
379
+ })
380
+
381
+ it('handles array values', () => {
382
+ const result = schema.toURLSearchParams({
383
+ name: 'Alice',
384
+ // @ts-expect-error
385
+ tags: ['tag1', 'tag2'],
386
+ })
387
+ expect(result.toString()).toBe('name=Alice&tags=tag1&tags=tag2')
388
+ })
389
+
390
+ it('skips undefined values', () => {
391
+ const result = schema.toURLSearchParams({
392
+ name: 'Alice',
393
+ age: undefined,
394
+ })
395
+ expect(result.toString()).toBe('name=Alice')
396
+ })
397
+
398
+ it('handles empty arrays', () => {
399
+ const result = schema.toURLSearchParams({
400
+ name: 'Alice',
401
+ // @ts-expect-error
402
+ tags: [],
403
+ })
404
+ expect(result.toString()).toBe('name=Alice')
405
+ })
406
+
407
+ it('handles arrays with multiple types', () => {
408
+ const result = schema.toURLSearchParams({
409
+ name: 'Alice',
410
+ // @ts-expect-error
411
+ values: [1, true, 'text'],
412
+ })
413
+ expect(result.toString()).toBe(
414
+ 'name=Alice&values=1&values=true&values=text',
415
+ )
416
+ })
417
+
418
+ it('handles undefined input', () => {
419
+ // @ts-expect-error
420
+ const result = schema.toURLSearchParams(undefined)
421
+ expect(result.toString()).toBe('')
422
+ })
423
+
424
+ it('converts negative integers', () => {
425
+ const result = schema.toURLSearchParams({ name: 'Alice', age: -5 })
426
+ expect(result.toString()).toBe('name=Alice&age=-5')
427
+ })
428
+
429
+ it('converts zero', () => {
430
+ const result = schema.toURLSearchParams({ name: 'Alice', age: 0 })
431
+ expect(result.toString()).toBe('name=Alice&age=0')
432
+ })
433
+
434
+ it('converts false boolean', () => {
435
+ const result = schema.toURLSearchParams({ name: 'Alice', active: false })
436
+ expect(result.toString()).toBe('name=Alice&active=false')
437
+ })
438
+ })
439
+
440
+ describe('roundtrip conversion', () => {
441
+ const schema = new ParamsSchema({
442
+ name: new StringSchema({}),
443
+ age: new OptionalSchema(new IntegerSchema({})),
444
+ active: new OptionalSchema(new BooleanSchema({})),
445
+ })
446
+
447
+ it('roundtrips simple params correctly', () => {
448
+ const original = { name: 'Alice', age: 30, active: true }
449
+ const urlParams = schema.toURLSearchParams(original)
450
+ const result = schema.fromURLSearchParams(urlParams)
451
+ expect(result).toEqual(original)
452
+ })
453
+
454
+ it('roundtrips params with arrays', () => {
455
+ const original = { name: 'Alice', tags: ['tag1', 'tag2'] }
456
+ const urlParams = schema.toURLSearchParams(original)
457
+ const result = schema.fromURLSearchParams(urlParams)
458
+ expect(result).toEqual(original)
459
+ })
460
+
461
+ it('roundtrips params with boolean false', () => {
462
+ const original = { name: 'Alice', active: false }
463
+ const urlParams = schema.toURLSearchParams(original)
464
+ const result = schema.fromURLSearchParams(urlParams)
465
+ expect(result).toEqual(original)
466
+ })
467
+
468
+ it('roundtrips params with zero', () => {
469
+ const original = { name: 'Alice', age: 0 }
470
+ const urlParams = schema.toURLSearchParams(original)
471
+ const result = schema.fromURLSearchParams(urlParams)
472
+ expect(result).toEqual(original)
473
+ })
474
+ })
475
+
476
+ describe('empty schema', () => {
477
+ const schema = new ParamsSchema({})
478
+
479
+ it('validates empty object', () => {
480
+ const result = schema.safeParse({})
481
+ expect(result.success).toBe(true)
482
+ })
483
+
484
+ it('accepts additional params', () => {
485
+ const result = schema.safeParse({ extra: 'value' })
486
+ expect(result.success).toBe(true)
487
+ })
488
+
489
+ it('rejects non-objects', () => {
490
+ const result = schema.safeParse('not an object')
491
+ expect(result.success).toBe(false)
492
+ })
493
+ })
494
+
495
+ describe('complex scenarios', () => {
496
+ const schema = new ParamsSchema({
497
+ query: new StringSchema({ minLength: 1 }),
498
+ limit: new OptionalSchema(
499
+ new IntegerSchema({ minimum: 1, maximum: 100 }),
500
+ ),
501
+ offset: new OptionalSchema(new IntegerSchema({ minimum: 0 })),
502
+ filters: new OptionalSchema(new ArraySchema(new StringSchema({}))),
503
+ })
504
+
505
+ it('validates typical query parameters', () => {
506
+ const result = schema.safeParse({
507
+ query: 'search term',
508
+ limit: 10,
509
+ offset: 0,
510
+ filters: ['filter1', 'filter2'],
511
+ })
512
+ expect(result.success).toBe(true)
513
+ })
514
+
515
+ it('validates minimal query', () => {
516
+ const result = schema.safeParse({
517
+ query: 'search',
518
+ })
519
+ expect(result.success).toBe(true)
520
+ })
521
+
522
+ it('rejects empty query string', () => {
523
+ const result = schema.safeParse({
524
+ query: '',
525
+ })
526
+ expect(result.success).toBe(false)
527
+ })
528
+
529
+ it('rejects limit out of range', () => {
530
+ const result = schema.safeParse({
531
+ query: 'search',
532
+ limit: 200,
533
+ })
534
+ expect(result.success).toBe(false)
535
+ })
536
+
537
+ it('rejects negative offset', () => {
538
+ const result = schema.safeParse({
539
+ query: 'search',
540
+ offset: -1,
541
+ })
542
+ expect(result.success).toBe(false)
543
+ })
544
+
545
+ it('accepts additional unspecified params', () => {
546
+ const result = schema.safeParse({
547
+ query: 'search',
548
+ extra: 'value',
549
+ })
550
+ expect(result.success).toBe(true)
551
+ })
552
+ })
553
+
554
+ describe('edge cases', () => {
555
+ const schema = new ParamsSchema({
556
+ name: new StringSchema({}),
557
+ })
558
+
559
+ it('rejects objects with custom prototypes', () => {
560
+ const obj = Object.create({ inherited: 'value' })
561
+ obj.name = 'Alice'
562
+ const result = schema.safeParse(obj)
563
+ expect(result.success).toBe(false)
564
+ })
565
+
566
+ it('validates with numeric string keys', () => {
567
+ const result = schema.safeParse({
568
+ name: 'Alice',
569
+ '123': 'numeric key',
570
+ })
571
+ expect(result.success).toBe(true)
572
+ })
573
+
574
+ it('handles empty string keys in additional params', () => {
575
+ const result = schema.safeParse({
576
+ name: 'Alice',
577
+ '': 'empty key',
578
+ })
579
+ expect(result.success).toBe(true)
580
+ })
581
+ })
582
+ })