n4s 6.1.12 → 6.2.0
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.
- package/dist/exports/date.cjs +1 -1
- package/dist/exports/date.mjs +1 -1
- package/dist/exports/email.cjs +1 -1
- package/dist/exports/email.mjs +1 -1
- package/dist/exports/isURL.cjs +1 -1
- package/dist/exports/isURL.mjs +1 -1
- package/dist/{n4s-CoDF5Fg6.cjs → n4s-BTHEz-bJ.cjs} +215 -5
- package/dist/n4s-BTHEz-bJ.cjs.map +1 -0
- package/dist/{n4s-KWquSyTb.mjs → n4s-BxrvnvKp.mjs} +218 -8
- package/dist/n4s-BxrvnvKp.mjs.map +1 -0
- package/dist/n4s.cjs +1 -1
- package/dist/n4s.mjs +1 -1
- package/package.json +5 -1
- package/src/eager/eagerTypes.ts +14 -0
- package/src/lazy.ts +24 -0
- package/src/n4s.ts +5 -0
- package/src/rules/schemaRules/__tests__/integrationSchemaRules.types.test.ts +92 -0
- package/src/rules/schemaRules/__tests__/isArrayOf.test.ts +37 -1
- package/src/rules/schemaRules/__tests__/lazy.test.ts +312 -0
- package/src/rules/schemaRules/__tests__/record.test.ts +205 -0
- package/src/rules/schemaRules/__tests__/schema.parse.integration.test.ts +79 -0
- package/src/rules/schemaRules/__tests__/tuple.test.ts +256 -0
- package/src/rules/schemaRules/lazy.ts +48 -0
- package/src/rules/schemaRules/record.ts +126 -0
- package/src/rules/schemaRules/schemaRules.ts +8 -1
- package/src/rules/schemaRules/schemaRulesLazyTypes.ts +21 -0
- package/src/rules/schemaRules/tuple.ts +157 -0
- package/types/exports/date.d.cts +1 -1
- package/types/exports/date.d.mts +1 -1
- package/types/n4s.d.cts +21 -2
- package/types/n4s.d.cts.map +1 -1
- package/types/n4s.d.mts +78 -69
- package/types/n4s.d.mts.map +1 -1
- package/types/n4s.d.ts +21 -2
- package/types/{n4sTypes-Bb1zNxyv.d.mts → n4sTypes-3THfSmAQ.d.mts} +79 -5
- package/types/n4sTypes-3THfSmAQ.d.mts.map +1 -0
- package/types/{n4sTypes-ChCugpFQ.d.cts → n4sTypes-BSTzXRsU.d.cts} +66 -2
- package/types/n4sTypes-BSTzXRsU.d.cts.map +1 -0
- package/dist/n4s-CoDF5Fg6.cjs.map +0 -1
- package/dist/n4s-KWquSyTb.mjs.map +0 -1
- package/types/n4sTypes-Bb1zNxyv.d.mts.map +0 -1
- package/types/n4sTypes-ChCugpFQ.d.cts.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { describe, it, expect, expectTypeOf } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import { enforce } from '../../../n4s';
|
|
4
4
|
|
|
@@ -70,6 +70,42 @@ describe('isArrayOf', () => {
|
|
|
70
70
|
});
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
describe('enforce.list() - alias for isArrayOf', () => {
|
|
74
|
+
it('should validate arrays identically to isArrayOf', () => {
|
|
75
|
+
const rule = enforce.list(enforce.isNumber());
|
|
76
|
+
expect(rule.run([1, 2, 3]).pass).toBe(true);
|
|
77
|
+
expect(runArrayRule(rule, [1, 'x']).pass).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should support multiple rules', () => {
|
|
81
|
+
const rule = enforce.list(enforce.isNumber(), enforce.isString());
|
|
82
|
+
expect(rule.run([1, '2', 3]).pass).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should chain array methods', () => {
|
|
86
|
+
const rule = enforce.list(enforce.isNumber()).minLength(1).maxLength(5);
|
|
87
|
+
expect(rule.run([1, 2]).pass).toBe(true);
|
|
88
|
+
expect(rule.run([]).pass).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should infer the same types as isArrayOf', () => {
|
|
92
|
+
const viaIsArrayOf = enforce.isArrayOf(enforce.isString());
|
|
93
|
+
const viaList = enforce.list(enforce.isString());
|
|
94
|
+
expectTypeOf(viaList.infer).toEqualTypeOf(viaIsArrayOf.infer);
|
|
95
|
+
// eslint-disable-next-line vitest/valid-expect
|
|
96
|
+
expectTypeOf(viaList.parse).returns.toEqualTypeOf<string[]>();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should work in the eager API', () => {
|
|
100
|
+
expect(() => {
|
|
101
|
+
enforce([1, 2, 3]).list(enforce.isNumber());
|
|
102
|
+
}).not.toThrow();
|
|
103
|
+
expect(() => {
|
|
104
|
+
enforce(['x']).list(enforce.isNumber());
|
|
105
|
+
}).toThrow();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
73
109
|
describe('isArrayOf - eager API', () => {
|
|
74
110
|
it('should chain array methods after isArrayOf (eager API)', () => {
|
|
75
111
|
expect(() => {
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { enforce, compose } from '../../../n4s';
|
|
4
|
+
import type { RuleInstance } from '../../../utils/RuleInstance';
|
|
5
|
+
|
|
6
|
+
describe('enforce.lazy()', () => {
|
|
7
|
+
describe('recursive shape validation', () => {
|
|
8
|
+
it('validates a simple recursive tree', () => {
|
|
9
|
+
const treeSchema: RuleInstance<any> = enforce.shape({
|
|
10
|
+
value: enforce.isNumber(),
|
|
11
|
+
children: enforce.isArrayOf(enforce.lazy(() => treeSchema)),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(
|
|
15
|
+
treeSchema.test({
|
|
16
|
+
value: 1,
|
|
17
|
+
children: [
|
|
18
|
+
{ value: 2, children: [] },
|
|
19
|
+
{
|
|
20
|
+
value: 3,
|
|
21
|
+
children: [{ value: 4, children: [] }],
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
}),
|
|
25
|
+
).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('fails on invalid nested node', () => {
|
|
29
|
+
const treeSchema: RuleInstance<any> = enforce.shape({
|
|
30
|
+
value: enforce.isNumber(),
|
|
31
|
+
children: enforce.isArrayOf(enforce.lazy(() => treeSchema)),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(
|
|
35
|
+
treeSchema.test({
|
|
36
|
+
value: 1,
|
|
37
|
+
children: [{ value: 'not a number', children: [] }],
|
|
38
|
+
}),
|
|
39
|
+
).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('validates leaf nodes (empty children array)', () => {
|
|
43
|
+
const treeSchema: RuleInstance<any> = enforce.shape({
|
|
44
|
+
value: enforce.isString(),
|
|
45
|
+
children: enforce.isArrayOf(enforce.lazy(() => treeSchema)),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(treeSchema.test({ value: 'leaf', children: [] })).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('fails when nested node is missing required fields', () => {
|
|
52
|
+
const treeSchema: RuleInstance<any> = enforce.shape({
|
|
53
|
+
value: enforce.isNumber(),
|
|
54
|
+
children: enforce.isArrayOf(enforce.lazy(() => treeSchema)),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(
|
|
58
|
+
treeSchema.test({
|
|
59
|
+
value: 1,
|
|
60
|
+
children: [{ value: 2 }],
|
|
61
|
+
}),
|
|
62
|
+
).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('optional recursive fields', () => {
|
|
67
|
+
it('validates a binary tree with optional left/right', () => {
|
|
68
|
+
const binaryTree: RuleInstance<any> = enforce.shape({
|
|
69
|
+
value: enforce.isNumber(),
|
|
70
|
+
left: enforce.optional(enforce.lazy(() => binaryTree)),
|
|
71
|
+
right: enforce.optional(enforce.lazy(() => binaryTree)),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(binaryTree.test({ value: 1 })).toBe(true);
|
|
75
|
+
expect(binaryTree.test({ value: 1, left: { value: 2 } })).toBe(true);
|
|
76
|
+
expect(
|
|
77
|
+
binaryTree.test({
|
|
78
|
+
value: 1,
|
|
79
|
+
left: { value: 2, right: { value: 3 } },
|
|
80
|
+
}),
|
|
81
|
+
).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('fails on invalid optional nested node', () => {
|
|
85
|
+
const binaryTree: RuleInstance<any> = enforce.shape({
|
|
86
|
+
value: enforce.isNumber(),
|
|
87
|
+
left: enforce.optional(enforce.lazy(() => binaryTree)),
|
|
88
|
+
right: enforce.optional(enforce.lazy(() => binaryTree)),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(
|
|
92
|
+
binaryTree.test({
|
|
93
|
+
value: 1,
|
|
94
|
+
left: { value: 'invalid' },
|
|
95
|
+
}),
|
|
96
|
+
).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('factory caching', () => {
|
|
101
|
+
it('calls the factory only once across recursive calls', () => {
|
|
102
|
+
let callCount = 0;
|
|
103
|
+
const schema: RuleInstance<any> = enforce.shape({
|
|
104
|
+
val: enforce.isNumber(),
|
|
105
|
+
next: enforce.optional(
|
|
106
|
+
enforce.lazy(() => {
|
|
107
|
+
callCount++;
|
|
108
|
+
return schema;
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
schema.test({ val: 1, next: { val: 2, next: { val: 3 } } });
|
|
114
|
+
expect(callCount).toBe(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('reuses cached schema across multiple validations', () => {
|
|
118
|
+
let callCount = 0;
|
|
119
|
+
const schema: RuleInstance<any> = enforce.shape({
|
|
120
|
+
val: enforce.isNumber(),
|
|
121
|
+
next: enforce.optional(
|
|
122
|
+
enforce.lazy(() => {
|
|
123
|
+
callCount++;
|
|
124
|
+
return schema;
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
schema.test({ val: 1, next: { val: 2 } });
|
|
130
|
+
schema.test({ val: 3, next: { val: 4 } });
|
|
131
|
+
expect(callCount).toBe(1);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('deep nesting', () => {
|
|
136
|
+
it('validates a deeply nested structure (5+ levels)', () => {
|
|
137
|
+
const nodeSchema: RuleInstance<any> = enforce.shape({
|
|
138
|
+
id: enforce.isNumber(),
|
|
139
|
+
child: enforce.optional(enforce.lazy(() => nodeSchema)),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(
|
|
143
|
+
nodeSchema.test({
|
|
144
|
+
id: 1,
|
|
145
|
+
child: {
|
|
146
|
+
id: 2,
|
|
147
|
+
child: {
|
|
148
|
+
id: 3,
|
|
149
|
+
child: {
|
|
150
|
+
id: 4,
|
|
151
|
+
child: {
|
|
152
|
+
id: 5,
|
|
153
|
+
child: { id: 6 },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('detects failure deep in nested structure', () => {
|
|
163
|
+
const nodeSchema: RuleInstance<any> = enforce.shape({
|
|
164
|
+
id: enforce.isNumber(),
|
|
165
|
+
child: enforce.optional(enforce.lazy(() => nodeSchema)),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(
|
|
169
|
+
nodeSchema.test({
|
|
170
|
+
id: 1,
|
|
171
|
+
child: {
|
|
172
|
+
id: 2,
|
|
173
|
+
child: {
|
|
174
|
+
id: 3,
|
|
175
|
+
child: {
|
|
176
|
+
id: 'invalid',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('RuleInstance methods', () => {
|
|
186
|
+
it('.test() returns boolean', () => {
|
|
187
|
+
const schema: RuleInstance<any> = enforce.shape({
|
|
188
|
+
val: enforce.isNumber(),
|
|
189
|
+
children: enforce.isArrayOf(enforce.lazy(() => schema)),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(typeof schema.test({ val: 1, children: [] })).toBe('boolean');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('.run() returns RuleRunReturn', () => {
|
|
196
|
+
const schema: RuleInstance<any> = enforce.shape({
|
|
197
|
+
val: enforce.isNumber(),
|
|
198
|
+
children: enforce.isArrayOf(enforce.lazy(() => schema)),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const passing = schema.run({ val: 1, children: [] });
|
|
202
|
+
expect(passing.pass).toBe(true);
|
|
203
|
+
|
|
204
|
+
const failing = schema.run({ val: 'bad', children: [] });
|
|
205
|
+
expect(failing.pass).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('.validate() returns StandardSchema result', () => {
|
|
209
|
+
const schema: RuleInstance<any> = enforce.shape({
|
|
210
|
+
val: enforce.isNumber(),
|
|
211
|
+
children: enforce.isArrayOf(enforce.lazy(() => schema)),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const passing = schema.validate({ val: 1, children: [] });
|
|
215
|
+
expect(passing).toHaveProperty('value');
|
|
216
|
+
expect(passing).not.toHaveProperty('issues');
|
|
217
|
+
|
|
218
|
+
const failing = schema.validate({ val: 'bad', children: [] });
|
|
219
|
+
expect(failing).toHaveProperty('issues');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('.parse() returns value on success and throws on failure', () => {
|
|
223
|
+
const schema: RuleInstance<any> = enforce.shape({
|
|
224
|
+
val: enforce.isNumber(),
|
|
225
|
+
children: enforce.isArrayOf(enforce.lazy(() => schema)),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const data = { val: 1, children: [] };
|
|
229
|
+
expect(schema.parse(data)).toEqual(data);
|
|
230
|
+
|
|
231
|
+
expect(() => schema.parse({ val: 'bad', children: [] })).toThrow();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('.message() override', () => {
|
|
236
|
+
it('supports custom message on lazy wrapper', () => {
|
|
237
|
+
const inner = enforce.shape({
|
|
238
|
+
val: enforce.isNumber(),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const lazyInner = enforce.lazy(() => inner).message('Custom lazy error');
|
|
242
|
+
const result = lazyInner.run({ val: 'bad' });
|
|
243
|
+
|
|
244
|
+
expect(result.pass).toBe(false);
|
|
245
|
+
expect(result.message).toBe('Custom lazy error');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('compose + lazy', () => {
|
|
250
|
+
it('works with compose()', () => {
|
|
251
|
+
const nodeSchema: RuleInstance<any> = enforce.shape({
|
|
252
|
+
id: enforce.isString(),
|
|
253
|
+
children: enforce.isArrayOf(enforce.lazy(() => nodeSchema)),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const validatedNode = compose(nodeSchema, enforce.isNotEmpty());
|
|
257
|
+
|
|
258
|
+
expect(
|
|
259
|
+
validatedNode.test({
|
|
260
|
+
id: 'root',
|
|
261
|
+
children: [{ id: 'a', children: [] }],
|
|
262
|
+
}),
|
|
263
|
+
).toBe(true);
|
|
264
|
+
|
|
265
|
+
expect(validatedNode.test({})).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('eager API integration', () => {
|
|
270
|
+
it('works when lazy is used inside an eager shape call', () => {
|
|
271
|
+
const treeSchema: RuleInstance<any> = enforce.shape({
|
|
272
|
+
value: enforce.isNumber(),
|
|
273
|
+
children: enforce.isArrayOf(enforce.lazy(() => treeSchema)),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(() => {
|
|
277
|
+
enforce({
|
|
278
|
+
value: 1,
|
|
279
|
+
children: [{ value: 2, children: [] }],
|
|
280
|
+
}).shape(treeSchema.__schema);
|
|
281
|
+
}).not.toThrow();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('standalone lazy', () => {
|
|
286
|
+
it('works as a standalone rule', () => {
|
|
287
|
+
const numberRule = enforce.isNumber();
|
|
288
|
+
const lazyNumber = enforce.lazy(() => numberRule);
|
|
289
|
+
|
|
290
|
+
expect(lazyNumber.test(42)).toBe(true);
|
|
291
|
+
// @ts-expect-error - testing runtime failure with invalid type
|
|
292
|
+
expect(lazyNumber.test('not a number')).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('error path propagation', () => {
|
|
297
|
+
it('propagates error path through lazy boundary', () => {
|
|
298
|
+
const schema: RuleInstance<any> = enforce.shape({
|
|
299
|
+
name: enforce.isString(),
|
|
300
|
+
children: enforce.isArrayOf(enforce.lazy(() => schema)),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const result = schema.run({
|
|
304
|
+
name: 'root',
|
|
305
|
+
children: [{ name: 123, children: [] }],
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(result.pass).toBe(false);
|
|
309
|
+
expect(result.path).toBeDefined();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { enforce } from '../../../n4s';
|
|
4
|
+
|
|
5
|
+
describe('enforce.record()', () => {
|
|
6
|
+
describe('lazy', () => {
|
|
7
|
+
it('should return a rule instance', () => {
|
|
8
|
+
const rule = enforce.record(enforce.isNumber());
|
|
9
|
+
expect(rule).toHaveProperty('run');
|
|
10
|
+
expect(rule).toHaveProperty('infer');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('value-only validation', () => {
|
|
14
|
+
it('passes when all values match rule', () => {
|
|
15
|
+
const rule = enforce.record(enforce.isNumber());
|
|
16
|
+
expect(rule.run({ a: 1, b: 2, c: 3 }).pass).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('fails when any value does not match', () => {
|
|
20
|
+
const rule = enforce.record(enforce.isNumber());
|
|
21
|
+
// @ts-expect-error - testing runtime with invalid value type
|
|
22
|
+
expect(rule.run({ a: 1, b: 'two', c: 3 }).pass).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('passes for empty objects', () => {
|
|
26
|
+
const rule = enforce.record(enforce.isNumber());
|
|
27
|
+
expect(rule.run({}).pass).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('fails for non-object values', () => {
|
|
31
|
+
const rule = enforce.record(enforce.isNumber());
|
|
32
|
+
// @ts-expect-error - testing runtime with non-object
|
|
33
|
+
expect(rule.run('not an object').pass).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('fails for arrays', () => {
|
|
37
|
+
const rule = enforce.record(enforce.isNumber());
|
|
38
|
+
// @ts-expect-error - testing runtime with array
|
|
39
|
+
expect(rule.run([1, 2, 3]).pass).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('skips prototype keys', () => {
|
|
43
|
+
const obj = Object.create({ inherited: 'bad' });
|
|
44
|
+
obj.own = 42;
|
|
45
|
+
const rule = enforce.record(enforce.isNumber());
|
|
46
|
+
expect(rule.run(obj).pass).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('fails on prototype pollution keys', () => {
|
|
50
|
+
const rule = enforce.record(enforce.isNumber());
|
|
51
|
+
const polluter = JSON.parse('{"__proto__": {"malicious": true}}');
|
|
52
|
+
const res = rule.run(polluter);
|
|
53
|
+
expect(res.pass).toBe(false);
|
|
54
|
+
expect(res.path).toEqual(['__proto__']);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('key + value validation', () => {
|
|
59
|
+
it('validates both keys and values', () => {
|
|
60
|
+
const rule = enforce.record(
|
|
61
|
+
enforce.isString().matches(/^[a-z]+$/),
|
|
62
|
+
enforce.isNumber(),
|
|
63
|
+
);
|
|
64
|
+
expect(rule.run({ abc: 1, def: 2 }).pass).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('fails when key does not match', () => {
|
|
68
|
+
const rule = enforce.record(
|
|
69
|
+
enforce.isString().matches(/^[a-z]+$/),
|
|
70
|
+
enforce.isNumber(),
|
|
71
|
+
);
|
|
72
|
+
const res = rule.run({ INVALID: 1 });
|
|
73
|
+
expect(res.pass).toBe(false);
|
|
74
|
+
expect(res.path).toEqual(['INVALID']);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('fails when value does not match', () => {
|
|
78
|
+
const rule = enforce.record(
|
|
79
|
+
enforce.isString().matches(/^[a-z]+$/),
|
|
80
|
+
enforce.isNumber(),
|
|
81
|
+
);
|
|
82
|
+
// @ts-expect-error - testing runtime with invalid value type
|
|
83
|
+
const res = rule.run({ valid: 'text' });
|
|
84
|
+
expect(res.pass).toBe(false);
|
|
85
|
+
expect(res.path).toEqual(['valid']);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('nested schemas', () => {
|
|
90
|
+
it('validates nested shapes as values', () => {
|
|
91
|
+
const rule = enforce.record(
|
|
92
|
+
enforce.shape({
|
|
93
|
+
name: enforce.isString(),
|
|
94
|
+
age: enforce.isNumber(),
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(
|
|
99
|
+
rule.run({
|
|
100
|
+
user1: { name: 'Alice', age: 30 },
|
|
101
|
+
user2: { name: 'Bob', age: 25 },
|
|
102
|
+
}).pass,
|
|
103
|
+
).toBe(true);
|
|
104
|
+
|
|
105
|
+
const failing = rule.run({
|
|
106
|
+
user1: { name: 'Alice', age: 30 },
|
|
107
|
+
// @ts-expect-error - testing runtime with invalid nested value
|
|
108
|
+
user2: { name: 'Bob', age: '25' },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(failing.pass).toBe(false);
|
|
112
|
+
expect(failing.path).toEqual(['user2', 'age']);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('validates nested records', () => {
|
|
116
|
+
const rule = enforce.record(enforce.record(enforce.isBoolean()));
|
|
117
|
+
|
|
118
|
+
expect(
|
|
119
|
+
rule.run({
|
|
120
|
+
group1: { a: true, b: false },
|
|
121
|
+
group2: { c: true },
|
|
122
|
+
}).pass,
|
|
123
|
+
).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('lazy chain type tests', () => {
|
|
128
|
+
it('works with .test()', () => {
|
|
129
|
+
const rule = enforce.record(enforce.isString());
|
|
130
|
+
expect(rule.test({ a: 'hello' })).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('chains with message() correctly', () => {
|
|
134
|
+
const rule = enforce.record(enforce.isNumber().message('custom error'));
|
|
135
|
+
const result = rule.run({ a: 1, b: 'err' });
|
|
136
|
+
expect(result.pass).toBe(false);
|
|
137
|
+
expect(result.message).toEqual('custom error');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('eager', () => {
|
|
143
|
+
describe('value-only validation', () => {
|
|
144
|
+
it('passes when all values match rule', () => {
|
|
145
|
+
expect(() => {
|
|
146
|
+
enforce({ a: 1, b: 2, c: 3 }).record(enforce.isNumber());
|
|
147
|
+
}).not.toThrow();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('fails when any value does not match', () => {
|
|
151
|
+
expect(() => {
|
|
152
|
+
enforce({ a: 1, b: 'two', c: 3 }).record(enforce.isNumber());
|
|
153
|
+
}).toThrow();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('passes for empty objects', () => {
|
|
157
|
+
expect(() => {
|
|
158
|
+
enforce({}).record(enforce.isNumber());
|
|
159
|
+
}).not.toThrow();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('fails for non-object values', () => {
|
|
163
|
+
expect(() => {
|
|
164
|
+
// @ts-expect-error - testing runtime with non-object
|
|
165
|
+
enforce('not an object').record(enforce.isNumber());
|
|
166
|
+
}).toThrow();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('fails for arrays', () => {
|
|
170
|
+
expect(() => {
|
|
171
|
+
// @ts-expect-error - testing runtime with array
|
|
172
|
+
enforce([1, 2, 3]).record(enforce.isNumber());
|
|
173
|
+
}).toThrow();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('skips prototype keys', () => {
|
|
177
|
+
const obj = Object.create({ inherited: 'bad' });
|
|
178
|
+
obj.own = 42;
|
|
179
|
+
expect(() => {
|
|
180
|
+
enforce(obj).record(enforce.isNumber());
|
|
181
|
+
}).not.toThrow();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('key + value validation', () => {
|
|
186
|
+
it('validates both keys and values', () => {
|
|
187
|
+
expect(() => {
|
|
188
|
+
enforce({ abc: 1, def: 2 }).record(
|
|
189
|
+
enforce.isString().matches(/^[a-z]+$/),
|
|
190
|
+
enforce.isNumber(),
|
|
191
|
+
);
|
|
192
|
+
}).not.toThrow();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('fails when key does not match', () => {
|
|
196
|
+
expect(() => {
|
|
197
|
+
enforce({ INVALID: 1 }).record(
|
|
198
|
+
enforce.isString().matches(/^[a-z]+$/),
|
|
199
|
+
enforce.isNumber(),
|
|
200
|
+
);
|
|
201
|
+
}).toThrow();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import { enforce } from '../../../n4s';
|
|
4
|
+
import type { RuleInstance } from '../../../utils/RuleInstance';
|
|
4
5
|
|
|
5
6
|
declare global {
|
|
6
7
|
namespace n4s {
|
|
@@ -155,4 +156,82 @@ describe('schema parse integration', () => {
|
|
|
155
156
|
expect(schema.parse({ label: undefined })).toEqual({ label: 'N/A' });
|
|
156
157
|
expect(schema.parse({ label: 'hello' })).toEqual({ label: 'hello' });
|
|
157
158
|
});
|
|
159
|
+
|
|
160
|
+
it('lazy propagates parse transformations from inner schema', () => {
|
|
161
|
+
const inner = enforce.shape({
|
|
162
|
+
name: enforce.trimString(),
|
|
163
|
+
age: enforce.toNumber(),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const schema = enforce.lazy(() => inner);
|
|
167
|
+
// @ts-expect-error - input type differs from output due to coercions
|
|
168
|
+
const result = schema.parse({ name: ' Jane ', age: '34' });
|
|
169
|
+
expect(result).toEqual({ name: 'Jane', age: 34 });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('tuple parses element values with custom coercions', () => {
|
|
173
|
+
const schema = enforce.tuple(enforce.trimString(), enforce.toNumber());
|
|
174
|
+
const result = schema.parse([' hello ', '42']);
|
|
175
|
+
expect(result).toEqual(['hello', 42]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('tuple parses nested shape elements', () => {
|
|
179
|
+
const schema = enforce.tuple(
|
|
180
|
+
enforce.trimString(),
|
|
181
|
+
enforce.shape({
|
|
182
|
+
name: enforce.trimString(),
|
|
183
|
+
age: enforce.toNumber(),
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const result = schema.parse([' label ', { name: ' Jane ', age: '34' }]);
|
|
188
|
+
expect(result).toEqual(['label', { name: 'Jane', age: 34 }]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('tuple parses with built-in parser chains', () => {
|
|
192
|
+
const schema = enforce.tuple(
|
|
193
|
+
enforce.isString().trim().toTitle(),
|
|
194
|
+
enforce.isNumeric().toNumber().clamp(0, 100),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const result = schema.parse([' jANE DOE ', '180']);
|
|
198
|
+
expect(result).toEqual(['Jane Doe', 100]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('tuple inside shape preserves parse transformations', () => {
|
|
202
|
+
const schema = enforce.shape({
|
|
203
|
+
label: enforce.trimString(),
|
|
204
|
+
coords: enforce.tuple(enforce.toNumber(), enforce.toNumber()),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const result = schema.parse({ label: ' origin ', coords: ['10', '20'] });
|
|
208
|
+
expect(result).toEqual({ label: 'origin', coords: [10, 20] });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('lazy propagates parse transformations through recursive schemas', () => {
|
|
212
|
+
const schema: RuleInstance<any> = enforce.shape({
|
|
213
|
+
name: enforce.trimString(),
|
|
214
|
+
children: enforce.isArrayOf(enforce.lazy(() => schema)),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const result = schema.parse({
|
|
218
|
+
name: ' Root ',
|
|
219
|
+
children: [
|
|
220
|
+
{
|
|
221
|
+
name: ' Child ',
|
|
222
|
+
children: [{ name: ' Grandchild ', children: [] }],
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(result).toEqual({
|
|
228
|
+
name: 'Root',
|
|
229
|
+
children: [
|
|
230
|
+
{
|
|
231
|
+
name: 'Child',
|
|
232
|
+
children: [{ name: 'Grandchild', children: [] }],
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
});
|
|
158
237
|
});
|