@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,612 @@
1
+ import { StringSchema } from './string.js'
2
+ import { TokenSchema } from './token.js'
3
+
4
+ describe('StringSchema', () => {
5
+ describe('basic validation', () => {
6
+ const schema = new StringSchema({})
7
+
8
+ it('validates plain strings', () => {
9
+ const result = schema.safeParse('hello world')
10
+ expect(result.success).toBe(true)
11
+ })
12
+
13
+ it('validates empty strings', () => {
14
+ const result = schema.safeParse('')
15
+ expect(result.success).toBe(true)
16
+ })
17
+
18
+ it('rejects non-strings', () => {
19
+ const result = schema.safeParse(123)
20
+ expect(result.success).toBe(false)
21
+ })
22
+
23
+ it('rejects null', () => {
24
+ const result = schema.safeParse(null)
25
+ expect(result.success).toBe(false)
26
+ })
27
+
28
+ it('rejects undefined', () => {
29
+ const result = schema.safeParse(undefined)
30
+ expect(result.success).toBe(false)
31
+ })
32
+
33
+ it('rejects booleans', () => {
34
+ const result = schema.safeParse(true)
35
+ expect(result.success).toBe(false)
36
+ })
37
+
38
+ it('rejects arrays', () => {
39
+ const result = schema.safeParse(['hello'])
40
+ expect(result.success).toBe(false)
41
+ })
42
+
43
+ it('rejects plain objects', () => {
44
+ const result = schema.safeParse({ value: 'hello' })
45
+ expect(result.success).toBe(false)
46
+ })
47
+ })
48
+
49
+ describe('default values', () => {
50
+ it('uses default value when no input provided', () => {
51
+ const schema = new StringSchema({ default: 'default value' })
52
+ const result = schema.safeParse(undefined)
53
+ expect(result.success).toBe(true)
54
+ if (result.success) {
55
+ expect(result.value).toBe('default value')
56
+ }
57
+ })
58
+
59
+ it('validates default value against constraints', () => {
60
+ const schema = new StringSchema({ default: 'hi', minLength: 5 })
61
+ const result = schema.safeParse(undefined)
62
+ expect(result.success).toBe(false)
63
+ })
64
+ })
65
+
66
+ describe('minLength constraint', () => {
67
+ const schema = new StringSchema({ minLength: 5 })
68
+
69
+ it('accepts strings meeting minimum length', () => {
70
+ const result = schema.safeParse('hello')
71
+ expect(result.success).toBe(true)
72
+ })
73
+
74
+ it('accepts strings exceeding minimum length', () => {
75
+ const result = schema.safeParse('hello world')
76
+ expect(result.success).toBe(true)
77
+ })
78
+
79
+ it('rejects strings below minimum length', () => {
80
+ const result = schema.safeParse('hi')
81
+ expect(result.success).toBe(false)
82
+ })
83
+
84
+ it('rejects empty strings when minLength is set', () => {
85
+ const result = schema.safeParse('')
86
+ expect(result.success).toBe(false)
87
+ })
88
+ })
89
+
90
+ describe('maxLength constraint', () => {
91
+ const schema = new StringSchema({ maxLength: 10 })
92
+
93
+ it('accepts strings meeting maximum length', () => {
94
+ const result = schema.safeParse('1234567890')
95
+ expect(result.success).toBe(true)
96
+ })
97
+
98
+ it('accepts strings below maximum length', () => {
99
+ const result = schema.safeParse('hello')
100
+ expect(result.success).toBe(true)
101
+ })
102
+
103
+ it('rejects strings exceeding maximum length', () => {
104
+ const result = schema.safeParse('hello world!')
105
+ expect(result.success).toBe(false)
106
+ })
107
+
108
+ it('accepts empty strings', () => {
109
+ const result = schema.safeParse('')
110
+ expect(result.success).toBe(true)
111
+ })
112
+
113
+ it('correctly handles UTF-8 multi-byte characters', () => {
114
+ // Emoji takes 4 bytes in UTF-8
115
+ const schema = new StringSchema({ maxLength: 4 })
116
+ const result = schema.safeParse('😀')
117
+ expect(result.success).toBe(true)
118
+ })
119
+
120
+ it('rejects when multi-byte characters exceed maxLength', () => {
121
+ const schema = new StringSchema({ maxLength: 3 })
122
+ const result = schema.safeParse('😀')
123
+ expect(result.success).toBe(false)
124
+ })
125
+ })
126
+
127
+ describe('combined min and max length', () => {
128
+ const schema = new StringSchema({ minLength: 3, maxLength: 10 })
129
+
130
+ it('accepts strings within range', () => {
131
+ const result = schema.safeParse('hello')
132
+ expect(result.success).toBe(true)
133
+ })
134
+
135
+ it('accepts strings at minimum boundary', () => {
136
+ const result = schema.safeParse('abc')
137
+ expect(result.success).toBe(true)
138
+ })
139
+
140
+ it('accepts strings at maximum boundary', () => {
141
+ const result = schema.safeParse('1234567890')
142
+ expect(result.success).toBe(true)
143
+ })
144
+
145
+ it('rejects strings below minimum', () => {
146
+ const result = schema.safeParse('hi')
147
+ expect(result.success).toBe(false)
148
+ })
149
+
150
+ it('rejects strings above maximum', () => {
151
+ const result = schema.safeParse('hello world!')
152
+ expect(result.success).toBe(false)
153
+ })
154
+ })
155
+
156
+ describe('minGraphemes constraint', () => {
157
+ const schema = new StringSchema({ minGraphemes: 3 })
158
+
159
+ it('accepts strings meeting minimum graphemes', () => {
160
+ const result = schema.safeParse('abc')
161
+ expect(result.success).toBe(true)
162
+ })
163
+
164
+ it('accepts strings exceeding minimum graphemes', () => {
165
+ const result = schema.safeParse('hello')
166
+ expect(result.success).toBe(true)
167
+ })
168
+
169
+ it('rejects strings below minimum graphemes', () => {
170
+ const result = schema.safeParse('ab')
171
+ expect(result.success).toBe(false)
172
+ })
173
+
174
+ it('counts emoji as single graphemes', () => {
175
+ const result = schema.safeParse('😀😀😀')
176
+ expect(result.success).toBe(true)
177
+ })
178
+
179
+ it('rejects when emoji count is below minimum', () => {
180
+ const result = schema.safeParse('😀😀')
181
+ expect(result.success).toBe(false)
182
+ })
183
+ })
184
+
185
+ describe('maxGraphemes constraint', () => {
186
+ const schema = new StringSchema({ maxGraphemes: 5 })
187
+
188
+ it('accepts strings meeting maximum graphemes', () => {
189
+ const result = schema.safeParse('hello')
190
+ expect(result.success).toBe(true)
191
+ })
192
+
193
+ it('accepts strings below maximum graphemes', () => {
194
+ const result = schema.safeParse('hi')
195
+ expect(result.success).toBe(true)
196
+ })
197
+
198
+ it('rejects strings exceeding maximum graphemes', () => {
199
+ const result = schema.safeParse('hello world')
200
+ expect(result.success).toBe(false)
201
+ })
202
+
203
+ it('counts emoji as single graphemes', () => {
204
+ const result = schema.safeParse('😀😀😀😀😀')
205
+ expect(result.success).toBe(true)
206
+ })
207
+
208
+ it('rejects when emoji count exceeds maximum', () => {
209
+ const result = schema.safeParse('😀😀😀😀😀😀')
210
+ expect(result.success).toBe(false)
211
+ })
212
+ })
213
+
214
+ describe('combined grapheme constraints', () => {
215
+ const schema = new StringSchema({ minGraphemes: 2, maxGraphemes: 5 })
216
+
217
+ it('accepts strings within grapheme range', () => {
218
+ const result = schema.safeParse('hello')
219
+ expect(result.success).toBe(true)
220
+ })
221
+
222
+ it('accepts strings at minimum boundary', () => {
223
+ const result = schema.safeParse('hi')
224
+ expect(result.success).toBe(true)
225
+ })
226
+
227
+ it('accepts strings at maximum boundary', () => {
228
+ const result = schema.safeParse('world')
229
+ expect(result.success).toBe(true)
230
+ })
231
+
232
+ it('rejects strings below minimum graphemes', () => {
233
+ const result = schema.safeParse('a')
234
+ expect(result.success).toBe(false)
235
+ })
236
+
237
+ it('rejects strings above maximum graphemes', () => {
238
+ const result = schema.safeParse('hello!')
239
+ expect(result.success).toBe(false)
240
+ })
241
+ })
242
+
243
+ describe('format: datetime', () => {
244
+ const schema = new StringSchema({ format: 'datetime' })
245
+
246
+ it('accepts valid ISO datetime strings', () => {
247
+ const result = schema.safeParse('2023-12-25T12:00:00Z')
248
+ expect(result.success).toBe(true)
249
+ })
250
+
251
+ it('accepts datetime with milliseconds', () => {
252
+ const result = schema.safeParse('2023-12-25T12:00:00.123Z')
253
+ expect(result.success).toBe(true)
254
+ })
255
+
256
+ it('rejects invalid datetime strings', () => {
257
+ const result = schema.safeParse('not a date')
258
+ expect(result.success).toBe(false)
259
+ })
260
+
261
+ it('rejects invalid date format', () => {
262
+ const result = schema.safeParse('12/25/2023')
263
+ expect(result.success).toBe(false)
264
+ })
265
+ })
266
+
267
+ describe('format: uri', () => {
268
+ const schema = new StringSchema({ format: 'uri' })
269
+
270
+ it('accepts valid HTTP URIs', () => {
271
+ const result = schema.safeParse('https://example.com')
272
+ expect(result.success).toBe(true)
273
+ })
274
+
275
+ it('accepts valid URIs with paths', () => {
276
+ const result = schema.safeParse('https://example.com/path/to/resource')
277
+ expect(result.success).toBe(true)
278
+ })
279
+
280
+ it('accepts URIs with different schemes', () => {
281
+ const result = schema.safeParse('ftp://files.example.com')
282
+ expect(result.success).toBe(true)
283
+ })
284
+
285
+ it('rejects invalid URIs', () => {
286
+ const result = schema.safeParse('not a uri')
287
+ expect(result.success).toBe(false)
288
+ })
289
+
290
+ it('rejects URIs without scheme', () => {
291
+ const result = schema.safeParse('example.com')
292
+ expect(result.success).toBe(false)
293
+ })
294
+ })
295
+
296
+ describe('format: at-uri', () => {
297
+ const schema = new StringSchema({ format: 'at-uri' })
298
+
299
+ it('accepts valid AT URI', () => {
300
+ const result = schema.safeParse(
301
+ 'at://did:plc:abc123/app.bsky.feed.post/xyz',
302
+ )
303
+ expect(result.success).toBe(true)
304
+ })
305
+
306
+ it('rejects invalid AT URI', () => {
307
+ const result = schema.safeParse('https://example.com')
308
+ expect(result.success).toBe(false)
309
+ })
310
+
311
+ it('rejects plain strings', () => {
312
+ const result = schema.safeParse('not an at-uri')
313
+ expect(result.success).toBe(false)
314
+ })
315
+ })
316
+
317
+ describe('format: did', () => {
318
+ const schema = new StringSchema({ format: 'did' })
319
+
320
+ it('accepts valid DID with plc method', () => {
321
+ const result = schema.safeParse('did:plc:abc123')
322
+ expect(result.success).toBe(true)
323
+ })
324
+
325
+ it('accepts valid DID with web method', () => {
326
+ const result = schema.safeParse('did:web:example.com')
327
+ expect(result.success).toBe(true)
328
+ })
329
+
330
+ it('rejects invalid DID format', () => {
331
+ const result = schema.safeParse('not-a-did')
332
+ expect(result.success).toBe(false)
333
+ })
334
+
335
+ it('rejects DID without method', () => {
336
+ const result = schema.safeParse('did:')
337
+ expect(result.success).toBe(false)
338
+ })
339
+ })
340
+
341
+ describe('format: handle', () => {
342
+ const schema = new StringSchema({ format: 'handle' })
343
+
344
+ it('accepts valid handle', () => {
345
+ const result = schema.safeParse('user.bsky.social')
346
+ expect(result.success).toBe(true)
347
+ })
348
+
349
+ it('accepts handle with subdomain', () => {
350
+ const result = schema.safeParse('alice.test.example.com')
351
+ expect(result.success).toBe(true)
352
+ })
353
+
354
+ it('rejects invalid handle format', () => {
355
+ const result = schema.safeParse('invalid handle!')
356
+ expect(result.success).toBe(false)
357
+ })
358
+
359
+ it('rejects handle with spaces', () => {
360
+ const result = schema.safeParse('user name.bsky.social')
361
+ expect(result.success).toBe(false)
362
+ })
363
+ })
364
+
365
+ describe('format: at-identifier', () => {
366
+ const schema = new StringSchema({ format: 'at-identifier' })
367
+
368
+ it('accepts valid DID as at-identifier', () => {
369
+ const result = schema.safeParse('did:plc:abc123')
370
+ expect(result.success).toBe(true)
371
+ })
372
+
373
+ it('accepts valid handle as at-identifier', () => {
374
+ const result = schema.safeParse('user.bsky.social')
375
+ expect(result.success).toBe(true)
376
+ })
377
+
378
+ it('rejects invalid at-identifier', () => {
379
+ const result = schema.safeParse('invalid!')
380
+ expect(result.success).toBe(false)
381
+ })
382
+ })
383
+
384
+ describe('format: nsid', () => {
385
+ const schema = new StringSchema({ format: 'nsid' })
386
+
387
+ it('accepts valid NSID', () => {
388
+ const result = schema.safeParse('app.bsky.feed.post')
389
+ expect(result.success).toBe(true)
390
+ })
391
+
392
+ it('accepts NSID with multiple segments', () => {
393
+ const result = schema.safeParse('com.example.app.feature.action')
394
+ expect(result.success).toBe(true)
395
+ })
396
+
397
+ it('rejects invalid NSID format', () => {
398
+ const result = schema.safeParse('not-an-nsid')
399
+ expect(result.success).toBe(false)
400
+ })
401
+
402
+ it('rejects NSID with invalid characters', () => {
403
+ const result = schema.safeParse('app.bsky.feed!')
404
+ expect(result.success).toBe(false)
405
+ })
406
+ })
407
+
408
+ describe('format: cid', () => {
409
+ const schema = new StringSchema({ format: 'cid' })
410
+
411
+ it('accepts valid CID v1', () => {
412
+ const result = schema.safeParse(
413
+ 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',
414
+ )
415
+ expect(result.success).toBe(true)
416
+ })
417
+
418
+ it('rejects invalid CID format', () => {
419
+ const result = schema.safeParse('not-a-cid')
420
+ expect(result.success).toBe(false)
421
+ })
422
+
423
+ it('rejects plain strings', () => {
424
+ const result = schema.safeParse('abc123')
425
+ expect(result.success).toBe(false)
426
+ })
427
+ })
428
+
429
+ describe('format: language', () => {
430
+ const schema = new StringSchema({ format: 'language' })
431
+
432
+ it('accepts valid BCP 47 language code', () => {
433
+ const result = schema.safeParse('en')
434
+ expect(result.success).toBe(true)
435
+ })
436
+
437
+ it('accepts language code with region', () => {
438
+ const result = schema.safeParse('en-US')
439
+ expect(result.success).toBe(true)
440
+ })
441
+
442
+ it('accepts language code with script and region', () => {
443
+ const result = schema.safeParse('zh-Hans-CN')
444
+ expect(result.success).toBe(true)
445
+ })
446
+
447
+ it('rejects invalid language code', () => {
448
+ const result = schema.safeParse('not valid')
449
+ expect(result.success).toBe(false)
450
+ })
451
+ })
452
+
453
+ describe('format: tid', () => {
454
+ const schema = new StringSchema({ format: 'tid' })
455
+
456
+ it('accepts valid TID', () => {
457
+ const result = schema.safeParse('3jzfcijpj2z2a')
458
+ expect(result.success).toBe(true)
459
+ })
460
+
461
+ it('rejects invalid TID format', () => {
462
+ const result = schema.safeParse('not-a-tid')
463
+ expect(result.success).toBe(false)
464
+ })
465
+
466
+ it('rejects TID with invalid characters', () => {
467
+ const result = schema.safeParse('3jzfcijpj2z2!')
468
+ expect(result.success).toBe(false)
469
+ })
470
+ })
471
+
472
+ describe('format: record-key', () => {
473
+ const schema = new StringSchema({ format: 'record-key' })
474
+
475
+ it('accepts valid record key', () => {
476
+ const result = schema.safeParse('3jzfcijpj2z2a')
477
+ expect(result.success).toBe(true)
478
+ })
479
+
480
+ it('accepts alphanumeric record key', () => {
481
+ const result = schema.safeParse('myRecordKey123')
482
+ expect(result.success).toBe(true)
483
+ })
484
+
485
+ it('rejects record key with invalid characters', () => {
486
+ const result = schema.safeParse('invalid/key')
487
+ expect(result.success).toBe(false)
488
+ })
489
+
490
+ it('rejects record key with spaces', () => {
491
+ const result = schema.safeParse('invalid key')
492
+ expect(result.success).toBe(false)
493
+ })
494
+ })
495
+
496
+ describe('type coercion', () => {
497
+ const schema = new StringSchema({})
498
+
499
+ it('coerces Date objects to ISO strings', () => {
500
+ const date = new Date('2023-12-25T12:00:00Z')
501
+ const result = schema.safeParse(date)
502
+ expect(result.success).toBe(true)
503
+ if (result.success) {
504
+ expect(result.value).toBe('2023-12-25T12:00:00.000Z')
505
+ }
506
+ })
507
+
508
+ it('rejects invalid Date objects', () => {
509
+ const invalidDate = new Date('invalid')
510
+ const result = schema.safeParse(invalidDate)
511
+ expect(result.success).toBe(false)
512
+ })
513
+
514
+ it('coerces URL objects to strings', () => {
515
+ const url = new URL('https://example.com/path')
516
+ const result = schema.safeParse(url)
517
+ expect(result.success).toBe(true)
518
+ if (result.success) {
519
+ expect(result.value).toBe('https://example.com/path')
520
+ }
521
+ })
522
+
523
+ it('coerces String objects to primitive strings', () => {
524
+ const stringObj = new String('hello')
525
+ const result = schema.safeParse(stringObj)
526
+ expect(result.success).toBe(true)
527
+ if (result.success) {
528
+ expect(result.value).toBe('hello')
529
+ }
530
+ })
531
+
532
+ it('coerces TokenSchema instances to strings', () => {
533
+ const token = new TokenSchema('mytoken')
534
+ const result = schema.safeParse(token)
535
+ expect(result.success).toBe(true)
536
+ if (result.success) {
537
+ expect(result.value).toBe('mytoken')
538
+ }
539
+ })
540
+ })
541
+
542
+ describe('combined constraints and format', () => {
543
+ const schema = new StringSchema({
544
+ format: 'handle',
545
+ minLength: 5,
546
+ maxLength: 50,
547
+ })
548
+
549
+ it('validates both format and length constraints', () => {
550
+ const result = schema.safeParse('user.bsky.social')
551
+ expect(result.success).toBe(true)
552
+ })
553
+
554
+ it('rejects when length is valid but format is invalid', () => {
555
+ const result = schema.safeParse('invalid handle!')
556
+ expect(result.success).toBe(false)
557
+ })
558
+
559
+ it('rejects when format is valid but length is too short', () => {
560
+ const result = schema.safeParse('a.bc')
561
+ expect(result.success).toBe(false)
562
+ })
563
+
564
+ it('rejects when format is valid but length is too long', () => {
565
+ const longHandle =
566
+ 'very.long.subdomain.name.that.exceeds.maximum.length.example.com'
567
+ const result = schema.safeParse(longHandle)
568
+ expect(result.success).toBe(false)
569
+ })
570
+ })
571
+
572
+ describe('edge cases', () => {
573
+ it('handles strings with special characters', () => {
574
+ const schema = new StringSchema({})
575
+ const result = schema.safeParse('hello\nworld\ttab')
576
+ expect(result.success).toBe(true)
577
+ })
578
+
579
+ it('handles strings with unicode characters', () => {
580
+ const schema = new StringSchema({})
581
+ const result = schema.safeParse('Hello 世界 🌍')
582
+ expect(result.success).toBe(true)
583
+ })
584
+
585
+ it('handles very long strings', () => {
586
+ const schema = new StringSchema({ maxLength: 10000 })
587
+ const longString = 'a'.repeat(10000)
588
+ const result = schema.safeParse(longString)
589
+ expect(result.success).toBe(true)
590
+ })
591
+
592
+ it('rejects very long strings exceeding maxLength', () => {
593
+ const schema = new StringSchema({ maxLength: 100 })
594
+ const longString = 'a'.repeat(101)
595
+ const result = schema.safeParse(longString)
596
+ expect(result.success).toBe(false)
597
+ })
598
+
599
+ it('handles zero as minLength', () => {
600
+ const schema = new StringSchema({ minLength: 0 })
601
+ const result = schema.safeParse('')
602
+ expect(result.success).toBe(true)
603
+ })
604
+
605
+ it('handles complex emoji sequences', () => {
606
+ const schema = new StringSchema({ maxGraphemes: 5 })
607
+ // Family emoji is a single grapheme cluster
608
+ const result = schema.safeParse('👨‍👩‍👧‍👦')
609
+ expect(result.success).toBe(true)
610
+ })
611
+ })
612
+ })
@@ -1,16 +1,10 @@
1
- import { CID, graphemeLen, utf8Len } from '@atproto/lex-data'
2
- import {
3
- InferStringFormat,
4
- StringFormat,
5
- UnknownString,
6
- assertStringFormat,
7
- } from '../core.js'
8
- import { ValidationResult, Validator, ValidatorContext } from '../validation.js'
1
+ import { asCid, graphemeLen, utf8Len } from '@atproto/lex-data'
2
+ import { InferStringFormat, StringFormat, assertStringFormat } from '../core.js'
3
+ import { Schema, ValidationResult, ValidatorContext } from '../validation.js'
9
4
  import { TokenSchema } from './token.js'
10
5
 
11
6
  export type StringSchemaOptions = {
12
7
  default?: string
13
- knownValues?: readonly string[]
14
8
  format?: StringFormat
15
9
  minLength?: number
16
10
  maxLength?: number
@@ -22,20 +16,16 @@ export type StringSchemaOutput<Options> =
22
16
  //
23
17
  Options extends { format: infer F extends StringFormat }
24
18
  ? InferStringFormat<F>
25
- : Options extends { knownValues: readonly (infer K extends string)[] }
26
- ? K | UnknownString
27
- : string
19
+ : string
28
20
 
29
21
  export class StringSchema<
30
22
  const Options extends StringSchemaOptions = any,
31
- > extends Validator<StringSchemaOutput<Options>> {
32
- readonly lexiconType = 'string' as const
33
-
23
+ > extends Schema<StringSchemaOutput<Options>> {
34
24
  constructor(readonly options: Options) {
35
25
  super()
36
26
  }
37
27
 
38
- override validateInContext(
28
+ validateInContext(
39
29
  // @NOTE validation will be applied on the default value as well
40
30
  input: unknown = this.options.default,
41
31
  ctx: ValidatorContext,
@@ -104,6 +94,10 @@ export class StringSchema<
104
94
 
105
95
  export function coerceToString(input: unknown): string | null {
106
96
  switch (typeof input) {
97
+ // @NOTE We do *not* coerce numbers/booleans to strings because that can
98
+ // lead to them being accepted as string instead of being coerced to
99
+ // number/boolean when the input is a string and the expected result is
100
+ // number/boolean (e.g. in params).
107
101
  case 'string':
108
102
  return input
109
103
  case 'object': {
@@ -124,7 +118,7 @@ export function coerceToString(input: unknown): string | null {
124
118
  return input.toString()
125
119
  }
126
120
 
127
- const cid = CID.asCID(input)
121
+ const cid = asCid(input)
128
122
  if (cid) return cid.toString()
129
123
 
130
124
  if (input instanceof String) {