@atproto/lex-schema 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/core/$type.d.ts +6 -3
  3. package/dist/core/$type.d.ts.map +1 -1
  4. package/dist/core/$type.js +1 -0
  5. package/dist/core/$type.js.map +1 -1
  6. package/dist/core/record-key.d.ts +3 -3
  7. package/dist/core/record-key.d.ts.map +1 -1
  8. package/dist/core/record-key.js +12 -6
  9. package/dist/core/record-key.js.map +1 -1
  10. package/dist/core/result.d.ts.map +1 -1
  11. package/dist/core/result.js +6 -0
  12. package/dist/core/result.js.map +1 -1
  13. package/dist/core/string-format.d.ts +30 -27
  14. package/dist/core/string-format.d.ts.map +1 -1
  15. package/dist/core/string-format.js +56 -42
  16. package/dist/core/string-format.js.map +1 -1
  17. package/dist/core/types.d.ts +9 -1
  18. package/dist/core/types.d.ts.map +1 -1
  19. package/dist/core/types.js.map +1 -1
  20. package/dist/external.d.ts +31 -28
  21. package/dist/external.d.ts.map +1 -1
  22. package/dist/external.js +33 -17
  23. package/dist/external.js.map +1 -1
  24. package/dist/schema/_parameters.d.ts +2 -2
  25. package/dist/schema/_parameters.d.ts.map +1 -1
  26. package/dist/schema/array.d.ts +5 -6
  27. package/dist/schema/array.d.ts.map +1 -1
  28. package/dist/schema/array.js +5 -6
  29. package/dist/schema/array.js.map +1 -1
  30. package/dist/schema/blob.d.ts +2 -3
  31. package/dist/schema/blob.d.ts.map +1 -1
  32. package/dist/schema/blob.js +1 -2
  33. package/dist/schema/blob.js.map +1 -1
  34. package/dist/schema/boolean.d.ts +4 -5
  35. package/dist/schema/boolean.d.ts.map +1 -1
  36. package/dist/schema/boolean.js +2 -3
  37. package/dist/schema/boolean.js.map +1 -1
  38. package/dist/schema/bytes.d.ts +3 -4
  39. package/dist/schema/bytes.d.ts.map +1 -1
  40. package/dist/schema/bytes.js +2 -3
  41. package/dist/schema/bytes.js.map +1 -1
  42. package/dist/schema/cid.d.ts +13 -6
  43. package/dist/schema/cid.d.ts.map +1 -1
  44. package/dist/schema/cid.js +2 -4
  45. package/dist/schema/cid.js.map +1 -1
  46. package/dist/schema/custom.d.ts +3 -4
  47. package/dist/schema/custom.d.ts.map +1 -1
  48. package/dist/schema/custom.js +4 -3
  49. package/dist/schema/custom.js.map +1 -1
  50. package/dist/schema/dict.d.ts +3 -3
  51. package/dist/schema/dict.d.ts.map +1 -1
  52. package/dist/schema/dict.js +1 -1
  53. package/dist/schema/dict.js.map +1 -1
  54. package/dist/schema/discriminated-union.d.ts +15 -24
  55. package/dist/schema/discriminated-union.d.ts.map +1 -1
  56. package/dist/schema/discriminated-union.js +40 -64
  57. package/dist/schema/discriminated-union.js.map +1 -1
  58. package/dist/schema/enum.d.ts +8 -4
  59. package/dist/schema/enum.d.ts.map +1 -1
  60. package/dist/schema/enum.js +5 -3
  61. package/dist/schema/enum.js.map +1 -1
  62. package/dist/schema/integer.d.ts +3 -4
  63. package/dist/schema/integer.d.ts.map +1 -1
  64. package/dist/schema/integer.js +3 -4
  65. package/dist/schema/integer.js.map +1 -1
  66. package/dist/schema/intersection.d.ts +22 -14
  67. package/dist/schema/intersection.d.ts.map +1 -1
  68. package/dist/schema/intersection.js +12 -22
  69. package/dist/schema/intersection.js.map +1 -1
  70. package/dist/schema/literal.d.ts +7 -3
  71. package/dist/schema/literal.d.ts.map +1 -1
  72. package/dist/schema/literal.js +5 -3
  73. package/dist/schema/literal.js.map +1 -1
  74. package/dist/schema/never.d.ts +2 -2
  75. package/dist/schema/never.d.ts.map +1 -1
  76. package/dist/schema/never.js +1 -1
  77. package/dist/schema/never.js.map +1 -1
  78. package/dist/schema/null.d.ts +2 -3
  79. package/dist/schema/null.d.ts.map +1 -1
  80. package/dist/schema/null.js +1 -2
  81. package/dist/schema/null.js.map +1 -1
  82. package/dist/schema/nullable.d.ts +7 -0
  83. package/dist/schema/nullable.d.ts.map +1 -0
  84. package/dist/schema/nullable.js +19 -0
  85. package/dist/schema/nullable.js.map +1 -0
  86. package/dist/schema/object.d.ts +10 -44
  87. package/dist/schema/object.d.ts.map +1 -1
  88. package/dist/schema/object.js +10 -46
  89. package/dist/schema/object.js.map +1 -1
  90. package/dist/schema/optional.d.ts +7 -0
  91. package/dist/schema/optional.d.ts.map +1 -0
  92. package/dist/schema/optional.js +25 -0
  93. package/dist/schema/optional.js.map +1 -0
  94. package/dist/schema/params.d.ts +14 -19
  95. package/dist/schema/params.d.ts.map +1 -1
  96. package/dist/schema/params.js +10 -24
  97. package/dist/schema/params.js.map +1 -1
  98. package/dist/schema/payload.d.ts +4 -4
  99. package/dist/schema/payload.d.ts.map +1 -1
  100. package/dist/schema/payload.js.map +1 -1
  101. package/dist/schema/permission-set.d.ts +6 -6
  102. package/dist/schema/permission-set.d.ts.map +1 -1
  103. package/dist/schema/permission-set.js +1 -2
  104. package/dist/schema/permission-set.js.map +1 -1
  105. package/dist/schema/permission.d.ts +0 -1
  106. package/dist/schema/permission.d.ts.map +1 -1
  107. package/dist/schema/permission.js +0 -1
  108. package/dist/schema/permission.js.map +1 -1
  109. package/dist/schema/procedure.d.ts +8 -9
  110. package/dist/schema/procedure.d.ts.map +1 -1
  111. package/dist/schema/procedure.js +0 -1
  112. package/dist/schema/procedure.js.map +1 -1
  113. package/dist/schema/query.d.ts +7 -8
  114. package/dist/schema/query.d.ts.map +1 -1
  115. package/dist/schema/query.js +0 -1
  116. package/dist/schema/query.js.map +1 -1
  117. package/dist/schema/record.d.ts +34 -28
  118. package/dist/schema/record.d.ts.map +1 -1
  119. package/dist/schema/record.js +1 -2
  120. package/dist/schema/record.js.map +1 -1
  121. package/dist/schema/ref.d.ts +2 -3
  122. package/dist/schema/ref.d.ts.map +1 -1
  123. package/dist/schema/ref.js +1 -2
  124. package/dist/schema/ref.js.map +1 -1
  125. package/dist/schema/refine.d.ts +18 -0
  126. package/dist/schema/refine.d.ts.map +1 -0
  127. package/dist/schema/refine.js +33 -0
  128. package/dist/schema/refine.js.map +1 -0
  129. package/dist/schema/regexp.d.ts +7 -0
  130. package/dist/schema/regexp.d.ts.map +1 -0
  131. package/dist/schema/regexp.js +22 -0
  132. package/dist/schema/regexp.js.map +1 -0
  133. package/dist/schema/string.d.ts +4 -8
  134. package/dist/schema/string.d.ts.map +1 -1
  135. package/dist/schema/string.js +6 -3
  136. package/dist/schema/string.js.map +1 -1
  137. package/dist/schema/subscription.d.ts +7 -6
  138. package/dist/schema/subscription.d.ts.map +1 -1
  139. package/dist/schema/subscription.js.map +1 -1
  140. package/dist/schema/token.d.ts +2 -3
  141. package/dist/schema/token.d.ts.map +1 -1
  142. package/dist/schema/token.js +1 -2
  143. package/dist/schema/token.js.map +1 -1
  144. package/dist/schema/typed-object.d.ts +29 -27
  145. package/dist/schema/typed-object.d.ts.map +1 -1
  146. package/dist/schema/typed-object.js +1 -2
  147. package/dist/schema/typed-object.js.map +1 -1
  148. package/dist/schema/typed-ref.d.ts +2 -2
  149. package/dist/schema/typed-ref.d.ts.map +1 -1
  150. package/dist/schema/typed-ref.js +1 -1
  151. package/dist/schema/typed-ref.js.map +1 -1
  152. package/dist/schema/typed-union.d.ts +3 -4
  153. package/dist/schema/typed-union.d.ts.map +1 -1
  154. package/dist/schema/typed-union.js +3 -10
  155. package/dist/schema/typed-union.js.map +1 -1
  156. package/dist/schema/union.d.ts +2 -2
  157. package/dist/schema/union.d.ts.map +1 -1
  158. package/dist/schema/union.js +1 -1
  159. package/dist/schema/union.js.map +1 -1
  160. package/dist/schema/unknown-object.d.ts +2 -3
  161. package/dist/schema/unknown-object.d.ts.map +1 -1
  162. package/dist/schema/unknown-object.js +1 -2
  163. package/dist/schema/unknown-object.js.map +1 -1
  164. package/dist/schema/unknown.d.ts +2 -2
  165. package/dist/schema/unknown.d.ts.map +1 -1
  166. package/dist/schema/unknown.js +1 -1
  167. package/dist/schema/unknown.js.map +1 -1
  168. package/dist/schema.d.ts +4 -0
  169. package/dist/schema.d.ts.map +1 -1
  170. package/dist/schema.js +6 -1
  171. package/dist/schema.js.map +1 -1
  172. package/dist/util/array-agg.d.ts.map +1 -1
  173. package/dist/util/array-agg.js +1 -0
  174. package/dist/util/array-agg.js.map +1 -1
  175. package/dist/util/lazy-property.d.ts +2 -0
  176. package/dist/util/lazy-property.d.ts.map +1 -0
  177. package/dist/util/lazy-property.js +14 -0
  178. package/dist/util/lazy-property.js.map +1 -0
  179. package/dist/validation/schema.d.ts +24 -0
  180. package/dist/validation/schema.d.ts.map +1 -0
  181. package/dist/validation/schema.js +57 -0
  182. package/dist/validation/schema.js.map +1 -0
  183. package/dist/validation/validation-error.d.ts +3 -3
  184. package/dist/validation/validation-error.d.ts.map +1 -1
  185. package/dist/validation/validation-error.js +32 -4
  186. package/dist/validation/validation-error.js.map +1 -1
  187. package/dist/validation/validation-issue.d.ts +32 -24
  188. package/dist/validation/validation-issue.d.ts.map +1 -1
  189. package/dist/validation/validation-issue.js +136 -92
  190. package/dist/validation/validation-issue.js.map +1 -1
  191. package/dist/validation/validator.d.ts +20 -50
  192. package/dist/validation/validator.d.ts.map +1 -1
  193. package/dist/validation/validator.js +40 -134
  194. package/dist/validation/validator.js.map +1 -1
  195. package/dist/validation.d.ts +1 -0
  196. package/dist/validation.d.ts.map +1 -1
  197. package/dist/validation.js +1 -0
  198. package/dist/validation.js.map +1 -1
  199. package/package.json +8 -4
  200. package/src/core/$type.ts +7 -4
  201. package/src/core/record-key.ts +12 -5
  202. package/src/core/result.ts +6 -0
  203. package/src/core/string-format.ts +97 -61
  204. package/src/core/types.ts +12 -6
  205. package/src/external.ts +92 -70
  206. package/src/schema/_parameters.test.ts +416 -0
  207. package/src/schema/array.test.ts +237 -0
  208. package/src/schema/array.ts +17 -11
  209. package/src/schema/blob.test.ts +506 -0
  210. package/src/schema/blob.ts +3 -5
  211. package/src/schema/boolean.test.ts +116 -0
  212. package/src/schema/boolean.ts +5 -7
  213. package/src/schema/bytes.test.ts +226 -0
  214. package/src/schema/bytes.ts +4 -6
  215. package/src/schema/cid.test.ts +155 -0
  216. package/src/schema/cid.ts +14 -8
  217. package/src/schema/custom.test.ts +413 -0
  218. package/src/schema/custom.ts +10 -8
  219. package/src/schema/dict.test.ts +198 -0
  220. package/src/schema/dict.ts +6 -8
  221. package/src/schema/discriminated-union.test.ts +675 -0
  222. package/src/schema/discriminated-union.ts +68 -95
  223. package/src/schema/enum.test.ts +396 -0
  224. package/src/schema/enum.ts +12 -5
  225. package/src/schema/integer.test.ts +312 -0
  226. package/src/schema/integer.ts +5 -7
  227. package/src/schema/intersection.test.ts +32 -0
  228. package/src/schema/intersection.ts +37 -40
  229. package/src/schema/literal.test.ts +531 -0
  230. package/src/schema/literal.ts +12 -5
  231. package/src/schema/never.test.ts +174 -0
  232. package/src/schema/never.ts +3 -10
  233. package/src/schema/null.test.ts +79 -0
  234. package/src/schema/null.ts +3 -5
  235. package/src/schema/nullable.test.ts +480 -0
  236. package/src/schema/nullable.ts +23 -0
  237. package/src/schema/object.test.ts +47 -115
  238. package/src/schema/object.ts +19 -123
  239. package/src/schema/optional.test.ts +485 -0
  240. package/src/schema/optional.ts +31 -0
  241. package/src/schema/params.test.ts +582 -0
  242. package/src/schema/params.ts +37 -55
  243. package/src/schema/payload.test.ts +345 -0
  244. package/src/schema/payload.ts +5 -5
  245. package/src/schema/permission-set.test.ts +679 -0
  246. package/src/schema/permission-set.ts +6 -8
  247. package/src/schema/permission.test.ts +536 -0
  248. package/src/schema/permission.ts +0 -2
  249. package/src/schema/procedure.test.ts +443 -0
  250. package/src/schema/procedure.ts +11 -13
  251. package/src/schema/query.test.ts +408 -0
  252. package/src/schema/query.ts +9 -11
  253. package/src/schema/record.test.ts +694 -0
  254. package/src/schema/record.ts +38 -36
  255. package/src/schema/ref.test.ts +365 -0
  256. package/src/schema/ref.ts +8 -5
  257. package/src/schema/refine.test.ts +578 -0
  258. package/src/schema/refine.ts +85 -0
  259. package/src/schema/regexp.test.ts +580 -0
  260. package/src/schema/regexp.ts +22 -0
  261. package/src/schema/string.test.ts +612 -0
  262. package/src/schema/string.ts +11 -17
  263. package/src/schema/subscription.test.ts +689 -0
  264. package/src/schema/subscription.ts +13 -8
  265. package/src/schema/token.test.ts +428 -0
  266. package/src/schema/token.ts +3 -5
  267. package/src/schema/typed-object.test.ts +612 -0
  268. package/src/schema/typed-object.ts +23 -20
  269. package/src/schema/typed-ref.test.ts +823 -0
  270. package/src/schema/typed-ref.ts +10 -5
  271. package/src/schema/typed-union.test.ts +378 -0
  272. package/src/schema/typed-union.ts +6 -15
  273. package/src/schema/union.test.ts +200 -0
  274. package/src/schema/union.ts +5 -4
  275. package/src/schema/unknown-object.test.ts +592 -0
  276. package/src/schema/unknown-object.ts +3 -5
  277. package/src/schema/unknown.test.ts +312 -0
  278. package/src/schema/unknown.ts +3 -3
  279. package/src/schema.ts +7 -1
  280. package/src/util/array-agg.ts +1 -0
  281. package/src/util/lazy-property.ts +14 -0
  282. package/src/validation/schema.ts +92 -0
  283. package/src/validation/validation-error.ts +60 -9
  284. package/src/validation/validation-issue.ts +141 -144
  285. package/src/validation/validator.ts +67 -206
  286. package/src/validation.ts +1 -0
  287. package/tsconfig.build.json +12 -0
  288. package/tsconfig.json +7 -0
  289. package/tsconfig.tests.json +9 -0
@@ -0,0 +1,580 @@
1
+ import { RegexpSchema } from './regexp.js'
2
+
3
+ describe('RegexpSchema', () => {
4
+ describe('basic validation', () => {
5
+ const schema = new RegexpSchema(/^[a-z]+$/)
6
+
7
+ it('validates strings matching the pattern', () => {
8
+ const result = schema.safeParse('hello')
9
+ expect(result.success).toBe(true)
10
+ })
11
+
12
+ it('validates strings with different matching values', () => {
13
+ const result = schema.safeParse('world')
14
+ expect(result.success).toBe(true)
15
+ })
16
+
17
+ it('rejects strings not matching the pattern', () => {
18
+ const result = schema.safeParse('Hello')
19
+ expect(result.success).toBe(false)
20
+ })
21
+
22
+ it('rejects strings with numbers when pattern requires letters', () => {
23
+ const result = schema.safeParse('hello123')
24
+ expect(result.success).toBe(false)
25
+ })
26
+
27
+ it('rejects non-strings', () => {
28
+ const result = schema.safeParse(123)
29
+ expect(result.success).toBe(false)
30
+ })
31
+
32
+ it('rejects null', () => {
33
+ const result = schema.safeParse(null)
34
+ expect(result.success).toBe(false)
35
+ })
36
+
37
+ it('rejects undefined', () => {
38
+ const result = schema.safeParse(undefined)
39
+ expect(result.success).toBe(false)
40
+ })
41
+
42
+ it('rejects booleans', () => {
43
+ const result = schema.safeParse(true)
44
+ expect(result.success).toBe(false)
45
+ })
46
+
47
+ it('rejects arrays', () => {
48
+ const result = schema.safeParse(['hello'])
49
+ expect(result.success).toBe(false)
50
+ })
51
+
52
+ it('rejects plain objects', () => {
53
+ const result = schema.safeParse({ value: 'hello' })
54
+ expect(result.success).toBe(false)
55
+ })
56
+ })
57
+
58
+ describe('numeric patterns', () => {
59
+ const schema = new RegexpSchema(/^\d+$/)
60
+
61
+ it('validates numeric strings', () => {
62
+ const result = schema.safeParse('12345')
63
+ expect(result.success).toBe(true)
64
+ })
65
+
66
+ it('validates single digit strings', () => {
67
+ const result = schema.safeParse('0')
68
+ expect(result.success).toBe(true)
69
+ })
70
+
71
+ it('rejects strings with letters', () => {
72
+ const result = schema.safeParse('123abc')
73
+ expect(result.success).toBe(false)
74
+ })
75
+
76
+ it('rejects empty strings', () => {
77
+ const result = schema.safeParse('')
78
+ expect(result.success).toBe(false)
79
+ })
80
+ })
81
+
82
+ describe('email pattern', () => {
83
+ const schema = new RegexpSchema(
84
+ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
85
+ )
86
+
87
+ it('validates simple email addresses', () => {
88
+ const result = schema.safeParse('user@example.com')
89
+ expect(result.success).toBe(true)
90
+ })
91
+
92
+ it('validates email with subdomain', () => {
93
+ const result = schema.safeParse('user@mail.example.com')
94
+ expect(result.success).toBe(true)
95
+ })
96
+
97
+ it('validates email with numbers and special characters', () => {
98
+ const result = schema.safeParse('user.name+tag@example.com')
99
+ expect(result.success).toBe(true)
100
+ })
101
+
102
+ it('rejects email without @', () => {
103
+ const result = schema.safeParse('userexample.com')
104
+ expect(result.success).toBe(false)
105
+ })
106
+
107
+ it('rejects email without domain', () => {
108
+ const result = schema.safeParse('user@')
109
+ expect(result.success).toBe(false)
110
+ })
111
+
112
+ it('rejects email without TLD', () => {
113
+ const result = schema.safeParse('user@example')
114
+ expect(result.success).toBe(false)
115
+ })
116
+ })
117
+
118
+ describe('URL pattern', () => {
119
+ const schema = new RegexpSchema(/^https?:\/\/[^\s/$.?#].[^\s]*$/)
120
+
121
+ it('validates HTTP URLs', () => {
122
+ const result = schema.safeParse('http://example.com')
123
+ expect(result.success).toBe(true)
124
+ })
125
+
126
+ it('validates HTTPS URLs', () => {
127
+ const result = schema.safeParse('https://example.com')
128
+ expect(result.success).toBe(true)
129
+ })
130
+
131
+ it('validates URLs with paths', () => {
132
+ const result = schema.safeParse('https://example.com/path/to/resource')
133
+ expect(result.success).toBe(true)
134
+ })
135
+
136
+ it('validates URLs with query parameters', () => {
137
+ const result = schema.safeParse('https://example.com?param=value')
138
+ expect(result.success).toBe(true)
139
+ })
140
+
141
+ it('rejects URLs without protocol', () => {
142
+ const result = schema.safeParse('example.com')
143
+ expect(result.success).toBe(false)
144
+ })
145
+
146
+ it('rejects URLs with spaces', () => {
147
+ const result = schema.safeParse('https://example.com/path with spaces')
148
+ expect(result.success).toBe(false)
149
+ })
150
+ })
151
+
152
+ describe('phone number pattern', () => {
153
+ const schema = new RegexpSchema(/^\+?[1-9]\d{1,14}$/)
154
+
155
+ it('validates simple phone numbers', () => {
156
+ const result = schema.safeParse('1234567890')
157
+ expect(result.success).toBe(true)
158
+ })
159
+
160
+ it('validates phone numbers with plus prefix', () => {
161
+ const result = schema.safeParse('+1234567890')
162
+ expect(result.success).toBe(true)
163
+ })
164
+
165
+ it('validates international phone numbers', () => {
166
+ const result = schema.safeParse('+441234567890')
167
+ expect(result.success).toBe(true)
168
+ })
169
+
170
+ it('rejects phone numbers starting with zero', () => {
171
+ const result = schema.safeParse('0123456789')
172
+ expect(result.success).toBe(false)
173
+ })
174
+
175
+ it('rejects phone numbers with letters', () => {
176
+ const result = schema.safeParse('123-456-ABCD')
177
+ expect(result.success).toBe(false)
178
+ })
179
+
180
+ it('rejects phone numbers with special characters', () => {
181
+ const result = schema.safeParse('123-456-7890')
182
+ expect(result.success).toBe(false)
183
+ })
184
+ })
185
+
186
+ describe('hex color pattern', () => {
187
+ const schema = new RegexpSchema(/^#[0-9A-Fa-f]{6}$/)
188
+
189
+ it('validates 6-digit hex colors', () => {
190
+ const result = schema.safeParse('#FF5733')
191
+ expect(result.success).toBe(true)
192
+ })
193
+
194
+ it('validates lowercase hex colors', () => {
195
+ const result = schema.safeParse('#ff5733')
196
+ expect(result.success).toBe(true)
197
+ })
198
+
199
+ it('validates mixed case hex colors', () => {
200
+ const result = schema.safeParse('#Ff5733')
201
+ expect(result.success).toBe(true)
202
+ })
203
+
204
+ it('rejects hex colors without hash', () => {
205
+ const result = schema.safeParse('FF5733')
206
+ expect(result.success).toBe(false)
207
+ })
208
+
209
+ it('rejects 3-digit hex colors', () => {
210
+ const result = schema.safeParse('#FFF')
211
+ expect(result.success).toBe(false)
212
+ })
213
+
214
+ it('rejects hex colors with invalid characters', () => {
215
+ const result = schema.safeParse('#GG5733')
216
+ expect(result.success).toBe(false)
217
+ })
218
+ })
219
+
220
+ describe('alphanumeric pattern', () => {
221
+ const schema = new RegexpSchema(/^[a-zA-Z0-9]+$/)
222
+
223
+ it('validates alphanumeric strings', () => {
224
+ const result = schema.safeParse('abc123')
225
+ expect(result.success).toBe(true)
226
+ })
227
+
228
+ it('validates only letters', () => {
229
+ const result = schema.safeParse('abcdef')
230
+ expect(result.success).toBe(true)
231
+ })
232
+
233
+ it('validates only numbers', () => {
234
+ const result = schema.safeParse('123456')
235
+ expect(result.success).toBe(true)
236
+ })
237
+
238
+ it('rejects strings with spaces', () => {
239
+ const result = schema.safeParse('abc 123')
240
+ expect(result.success).toBe(false)
241
+ })
242
+
243
+ it('rejects strings with special characters', () => {
244
+ const result = schema.safeParse('abc_123')
245
+ expect(result.success).toBe(false)
246
+ })
247
+
248
+ it('rejects empty strings', () => {
249
+ const result = schema.safeParse('')
250
+ expect(result.success).toBe(false)
251
+ })
252
+ })
253
+
254
+ describe('case-insensitive pattern', () => {
255
+ const schema = new RegexpSchema(/^hello$/i)
256
+
257
+ it('validates lowercase match', () => {
258
+ const result = schema.safeParse('hello')
259
+ expect(result.success).toBe(true)
260
+ })
261
+
262
+ it('validates uppercase match', () => {
263
+ const result = schema.safeParse('HELLO')
264
+ expect(result.success).toBe(true)
265
+ })
266
+
267
+ it('validates mixed case match', () => {
268
+ const result = schema.safeParse('HeLLo')
269
+ expect(result.success).toBe(true)
270
+ })
271
+
272
+ it('rejects non-matching strings', () => {
273
+ const result = schema.safeParse('world')
274
+ expect(result.success).toBe(false)
275
+ })
276
+ })
277
+
278
+ describe('multiline pattern', () => {
279
+ const schema = new RegexpSchema(/^line\d+$/m)
280
+
281
+ it('validates single line matching pattern', () => {
282
+ const result = schema.safeParse('line1')
283
+ expect(result.success).toBe(true)
284
+ })
285
+
286
+ it('validates multiline string with matching line', () => {
287
+ const result = schema.safeParse('line1\nline2')
288
+ expect(result.success).toBe(true)
289
+ })
290
+
291
+ it('rejects strings without matching lines', () => {
292
+ const result = schema.safeParse('hello\nworld')
293
+ expect(result.success).toBe(false)
294
+ })
295
+ })
296
+
297
+ describe('optional character pattern', () => {
298
+ const schema = new RegexpSchema(/^colou?r$/)
299
+
300
+ it('validates with optional character present', () => {
301
+ const result = schema.safeParse('colour')
302
+ expect(result.success).toBe(true)
303
+ })
304
+
305
+ it('validates with optional character absent', () => {
306
+ const result = schema.safeParse('color')
307
+ expect(result.success).toBe(true)
308
+ })
309
+
310
+ it('rejects strings not matching pattern', () => {
311
+ const result = schema.safeParse('colouur')
312
+ expect(result.success).toBe(false)
313
+ })
314
+ })
315
+
316
+ describe('character range pattern', () => {
317
+ const schema = new RegexpSchema(/^[0-5]+$/)
318
+
319
+ it('validates strings within range', () => {
320
+ const result = schema.safeParse('012345')
321
+ expect(result.success).toBe(true)
322
+ })
323
+
324
+ it('validates single character in range', () => {
325
+ const result = schema.safeParse('3')
326
+ expect(result.success).toBe(true)
327
+ })
328
+
329
+ it('rejects strings with characters outside range', () => {
330
+ const result = schema.safeParse('0123456')
331
+ expect(result.success).toBe(false)
332
+ })
333
+ })
334
+
335
+ describe('quantifier pattern', () => {
336
+ const schema = new RegexpSchema(/^a{3}$/)
337
+
338
+ it('validates exact repetition count', () => {
339
+ const result = schema.safeParse('aaa')
340
+ expect(result.success).toBe(true)
341
+ })
342
+
343
+ it('rejects fewer repetitions', () => {
344
+ const result = schema.safeParse('aa')
345
+ expect(result.success).toBe(false)
346
+ })
347
+
348
+ it('rejects more repetitions', () => {
349
+ const result = schema.safeParse('aaaa')
350
+ expect(result.success).toBe(false)
351
+ })
352
+ })
353
+
354
+ describe('range quantifier pattern', () => {
355
+ const schema = new RegexpSchema(/^a{2,4}$/)
356
+
357
+ it('validates minimum repetition count', () => {
358
+ const result = schema.safeParse('aa')
359
+ expect(result.success).toBe(true)
360
+ })
361
+
362
+ it('validates middle repetition count', () => {
363
+ const result = schema.safeParse('aaa')
364
+ expect(result.success).toBe(true)
365
+ })
366
+
367
+ it('validates maximum repetition count', () => {
368
+ const result = schema.safeParse('aaaa')
369
+ expect(result.success).toBe(true)
370
+ })
371
+
372
+ it('rejects fewer than minimum repetitions', () => {
373
+ const result = schema.safeParse('a')
374
+ expect(result.success).toBe(false)
375
+ })
376
+
377
+ it('rejects more than maximum repetitions', () => {
378
+ const result = schema.safeParse('aaaaa')
379
+ expect(result.success).toBe(false)
380
+ })
381
+ })
382
+
383
+ describe('alternation pattern', () => {
384
+ const schema = new RegexpSchema(/^(cat|dog|bird)$/)
385
+
386
+ it('validates first alternative', () => {
387
+ const result = schema.safeParse('cat')
388
+ expect(result.success).toBe(true)
389
+ })
390
+
391
+ it('validates second alternative', () => {
392
+ const result = schema.safeParse('dog')
393
+ expect(result.success).toBe(true)
394
+ })
395
+
396
+ it('validates third alternative', () => {
397
+ const result = schema.safeParse('bird')
398
+ expect(result.success).toBe(true)
399
+ })
400
+
401
+ it('rejects strings not matching any alternative', () => {
402
+ const result = schema.safeParse('fish')
403
+ expect(result.success).toBe(false)
404
+ })
405
+ })
406
+
407
+ describe('word boundary pattern', () => {
408
+ const schema = new RegexpSchema(/\bword\b/)
409
+
410
+ it('validates word with boundaries', () => {
411
+ const result = schema.safeParse('word')
412
+ expect(result.success).toBe(true)
413
+ })
414
+
415
+ it('validates word within sentence', () => {
416
+ const result = schema.safeParse('this is a word')
417
+ expect(result.success).toBe(true)
418
+ })
419
+
420
+ it('rejects word as part of larger word', () => {
421
+ const result = schema.safeParse('wording')
422
+ expect(result.success).toBe(false)
423
+ })
424
+ })
425
+
426
+ describe('lookahead pattern', () => {
427
+ const schema = new RegexpSchema(/^(?=.*[A-Z])(?=.*[0-9]).{8,}$/)
428
+
429
+ it('validates string meeting all lookahead conditions', () => {
430
+ const result = schema.safeParse('Password1')
431
+ expect(result.success).toBe(true)
432
+ })
433
+
434
+ it('validates string with multiple uppercase and numbers', () => {
435
+ const result = schema.safeParse('ABC12345')
436
+ expect(result.success).toBe(true)
437
+ })
438
+
439
+ it('rejects string without uppercase', () => {
440
+ const result = schema.safeParse('password1')
441
+ expect(result.success).toBe(false)
442
+ })
443
+
444
+ it('rejects string without number', () => {
445
+ const result = schema.safeParse('Password')
446
+ expect(result.success).toBe(false)
447
+ })
448
+
449
+ it('rejects string too short', () => {
450
+ const result = schema.safeParse('Pass1')
451
+ expect(result.success).toBe(false)
452
+ })
453
+ })
454
+
455
+ describe('unicode pattern', () => {
456
+ const schema = new RegexpSchema(/^[\u4e00-\u9fa5]+$/)
457
+
458
+ it('validates Chinese characters', () => {
459
+ const result = schema.safeParse('你好')
460
+ expect(result.success).toBe(true)
461
+ })
462
+
463
+ it('validates multiple Chinese characters', () => {
464
+ const result = schema.safeParse('世界')
465
+ expect(result.success).toBe(true)
466
+ })
467
+
468
+ it('rejects non-Chinese characters', () => {
469
+ const result = schema.safeParse('hello')
470
+ expect(result.success).toBe(false)
471
+ })
472
+
473
+ it('rejects mixed Chinese and English', () => {
474
+ const result = schema.safeParse('你好hello')
475
+ expect(result.success).toBe(false)
476
+ })
477
+ })
478
+
479
+ describe('empty string pattern', () => {
480
+ const schema = new RegexpSchema(/^$/)
481
+
482
+ it('validates empty strings', () => {
483
+ const result = schema.safeParse('')
484
+ expect(result.success).toBe(true)
485
+ })
486
+
487
+ it('rejects non-empty strings', () => {
488
+ const result = schema.safeParse('hello')
489
+ expect(result.success).toBe(false)
490
+ })
491
+
492
+ it('rejects whitespace strings', () => {
493
+ const result = schema.safeParse(' ')
494
+ expect(result.success).toBe(false)
495
+ })
496
+ })
497
+
498
+ describe('wildcard pattern', () => {
499
+ const schema = new RegexpSchema(/^.*$/)
500
+
501
+ it('validates empty strings', () => {
502
+ const result = schema.safeParse('')
503
+ expect(result.success).toBe(true)
504
+ })
505
+
506
+ it('validates any string', () => {
507
+ const result = schema.safeParse('anything goes here 123 !@#')
508
+ expect(result.success).toBe(true)
509
+ })
510
+
511
+ it('validates strings with special characters', () => {
512
+ const result = schema.safeParse('!@#$%^&*()')
513
+ expect(result.success).toBe(true)
514
+ })
515
+
516
+ it('validates strings with unicode', () => {
517
+ const result = schema.safeParse('Hello 世界 🌍')
518
+ expect(result.success).toBe(true)
519
+ })
520
+ })
521
+
522
+ describe('edge cases', () => {
523
+ it('handles pattern with escape sequences', () => {
524
+ const schema = new RegexpSchema(/^\d{3}\.\d{3}\.\d{3}\.\d{3}$/)
525
+ const result = schema.safeParse('192.168.001.001')
526
+ expect(result.success).toBe(true)
527
+ })
528
+
529
+ it('handles pattern with special regex characters', () => {
530
+ const schema = new RegexpSchema(/^\$\d+\.\d{2}$/)
531
+ const result = schema.safeParse('$99.99')
532
+ expect(result.success).toBe(true)
533
+ })
534
+
535
+ it('handles very long strings', () => {
536
+ const schema = new RegexpSchema(/^[a-z]+$/)
537
+ const longString = 'a'.repeat(10000)
538
+ const result = schema.safeParse(longString)
539
+ expect(result.success).toBe(true)
540
+ })
541
+
542
+ it('handles strings with newlines', () => {
543
+ const schema = new RegexpSchema(/^hello\nworld$/)
544
+ const result = schema.safeParse('hello\nworld')
545
+ expect(result.success).toBe(true)
546
+ })
547
+
548
+ it('handles strings with tabs', () => {
549
+ const schema = new RegexpSchema(/^hello\tworld$/)
550
+ const result = schema.safeParse('hello\tworld')
551
+ expect(result.success).toBe(true)
552
+ })
553
+
554
+ it('handles emoji patterns', () => {
555
+ const schema = new RegexpSchema(/^[\u{1F600}-\u{1F64F}]+$/u)
556
+ const result = schema.safeParse('😀😃😄')
557
+ expect(result.success).toBe(true)
558
+ })
559
+
560
+ it('handles global flag in pattern', () => {
561
+ const schema = new RegexpSchema(/test/g)
562
+ const result = schema.safeParse('test')
563
+ expect(result.success).toBe(true)
564
+ })
565
+
566
+ it('handles pattern matching anywhere in string', () => {
567
+ const schema = new RegexpSchema(/test/)
568
+ const result = schema.safeParse('this is a test string')
569
+ expect(result.success).toBe(true)
570
+ })
571
+
572
+ it('handles complex nested groups', () => {
573
+ const schema = new RegexpSchema(
574
+ /^((https?|ftp):\/\/)?([a-z0-9]+\.)+[a-z]{2,}$/,
575
+ )
576
+ const result = schema.safeParse('https://example.com')
577
+ expect(result.success).toBe(true)
578
+ })
579
+ })
580
+ })
@@ -0,0 +1,22 @@
1
+ import { Schema, ValidationResult, ValidatorContext } from '../validation'
2
+
3
+ export class RegexpSchema<T extends string> extends Schema<T> {
4
+ constructor(public readonly pattern: RegExp) {
5
+ super()
6
+ }
7
+
8
+ validateInContext(
9
+ input: unknown,
10
+ ctx: ValidatorContext,
11
+ ): ValidationResult<T> {
12
+ if (typeof input !== 'string') {
13
+ return ctx.issueInvalidType(input, 'string')
14
+ }
15
+
16
+ if (!this.pattern.test(input)) {
17
+ return ctx.issueInvalidFormat(input, this.pattern.toString())
18
+ }
19
+
20
+ return ctx.success(input as T)
21
+ }
22
+ }