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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, it, expect, expectTypeOf } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { enforce } from '../../../n4s';
|
|
4
|
+
|
|
5
|
+
describe('enforce.tuple()', () => {
|
|
6
|
+
describe('basic validation', () => {
|
|
7
|
+
it('passes for valid tuple', () => {
|
|
8
|
+
const schema = enforce.tuple(enforce.isString(), enforce.isNumber());
|
|
9
|
+
expect(schema.test(['hello', 42])).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('fails when element has wrong type', () => {
|
|
13
|
+
const schema = enforce.tuple(enforce.isString(), enforce.isNumber());
|
|
14
|
+
// @ts-expect-error - intentionally passing wrong type for first element
|
|
15
|
+
expect(schema.test([42, 42])).toBe(false);
|
|
16
|
+
// @ts-expect-error - intentionally passing wrong type for second element
|
|
17
|
+
expect(schema.test(['hello', 'world'])).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('fails when too few elements', () => {
|
|
21
|
+
const schema = enforce.tuple(enforce.isString(), enforce.isNumber());
|
|
22
|
+
// @ts-expect-error - intentionally passing too few elements
|
|
23
|
+
expect(schema.test(['hello'])).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('fails when too many elements', () => {
|
|
27
|
+
const schema = enforce.tuple(enforce.isString(), enforce.isNumber());
|
|
28
|
+
// @ts-expect-error - intentionally passing too many elements
|
|
29
|
+
expect(schema.test(['hello', 42, true])).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('fails for non-array values', () => {
|
|
33
|
+
const schema = enforce.tuple(enforce.isString());
|
|
34
|
+
// @ts-expect-error - intentionally passing non-array
|
|
35
|
+
expect(schema.test('not an array')).toBe(false);
|
|
36
|
+
// @ts-expect-error - intentionally passing non-array
|
|
37
|
+
expect(schema.test({ 0: 'a' })).toBe(false);
|
|
38
|
+
// @ts-expect-error - intentionally passing null
|
|
39
|
+
expect(schema.test(null)).toBe(false);
|
|
40
|
+
// @ts-expect-error - intentionally passing undefined
|
|
41
|
+
expect(schema.test(undefined)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('passes for empty tuple schema with empty array', () => {
|
|
45
|
+
const emptyTuple = enforce.tuple();
|
|
46
|
+
expect(emptyTuple.test([])).toBe(true);
|
|
47
|
+
// @ts-expect-error - intentionally passing elements to empty tuple
|
|
48
|
+
expect(emptyTuple.test(['extra'])).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('with optional trailing elements', () => {
|
|
53
|
+
it('passes without optional element', () => {
|
|
54
|
+
const schema = enforce.tuple(
|
|
55
|
+
enforce.isString(),
|
|
56
|
+
enforce.optional(enforce.isNumber()),
|
|
57
|
+
);
|
|
58
|
+
// @ts-expect-error - runtime allows omitting optional trailing elements
|
|
59
|
+
expect(schema.test(['hello'])).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('passes with optional element present', () => {
|
|
63
|
+
const schema = enforce.tuple(
|
|
64
|
+
enforce.isString(),
|
|
65
|
+
enforce.optional(enforce.isNumber()),
|
|
66
|
+
);
|
|
67
|
+
expect(schema.test(['hello', 42])).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('fails when optional element has wrong type', () => {
|
|
71
|
+
const schema = enforce.tuple(
|
|
72
|
+
enforce.isString(),
|
|
73
|
+
enforce.optional(enforce.isNumber()),
|
|
74
|
+
);
|
|
75
|
+
// @ts-expect-error - intentionally passing wrong type for optional element
|
|
76
|
+
expect(schema.test(['hello', 'not a number'])).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('handles multiple trailing optional elements', () => {
|
|
80
|
+
const schema = enforce.tuple(
|
|
81
|
+
enforce.isString(),
|
|
82
|
+
enforce.optional(enforce.isNumber()),
|
|
83
|
+
enforce.optional(enforce.isBoolean()),
|
|
84
|
+
);
|
|
85
|
+
// @ts-expect-error - runtime allows omitting optional trailing elements
|
|
86
|
+
expect(schema.test(['hello'])).toBe(true);
|
|
87
|
+
// @ts-expect-error - runtime allows omitting optional trailing elements
|
|
88
|
+
expect(schema.test(['hello', 42])).toBe(true);
|
|
89
|
+
expect(schema.test(['hello', 42, true])).toBe(true);
|
|
90
|
+
// @ts-expect-error - intentionally passing too many elements
|
|
91
|
+
expect(schema.test(['hello', 42, true, 'extra'])).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('nested schemas', () => {
|
|
96
|
+
it('supports shape elements', () => {
|
|
97
|
+
const schema = enforce.tuple(
|
|
98
|
+
enforce.isString(),
|
|
99
|
+
enforce.shape({ id: enforce.isNumber() }),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(schema.test(['test', { id: 1 }])).toBe(true);
|
|
103
|
+
// @ts-expect-error - intentionally passing wrong type for nested field
|
|
104
|
+
expect(schema.test(['test', { id: 'x' }])).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('supports nested tuples', () => {
|
|
108
|
+
const schema = enforce.tuple(
|
|
109
|
+
enforce.isString(),
|
|
110
|
+
enforce.tuple(enforce.isNumber(), enforce.isNumber()),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(schema.test(['coords', [1, 2]])).toBe(true);
|
|
114
|
+
// @ts-expect-error - intentionally passing wrong type in nested tuple
|
|
115
|
+
expect(schema.test(['coords', [1, 'x']])).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('supports isArrayOf elements', () => {
|
|
119
|
+
const schema = enforce.tuple(
|
|
120
|
+
enforce.isString(),
|
|
121
|
+
enforce.isArrayOf(enforce.isNumber()),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(schema.test(['tags', [1, 2, 3]])).toBe(true);
|
|
125
|
+
// @ts-expect-error - intentionally passing wrong type in array
|
|
126
|
+
expect(schema.test(['tags', [1, 'x']])).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('eager API', () => {
|
|
131
|
+
it('throws on invalid tuple', () => {
|
|
132
|
+
expect(() => {
|
|
133
|
+
enforce(['hello', 'world']).tuple(
|
|
134
|
+
enforce.isString(),
|
|
135
|
+
enforce.isNumber(),
|
|
136
|
+
);
|
|
137
|
+
}).toThrow();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('does not throw on valid tuple', () => {
|
|
141
|
+
expect(() => {
|
|
142
|
+
enforce(['hello', 42]).tuple(enforce.isString(), enforce.isNumber());
|
|
143
|
+
}).not.toThrow();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('error reporting', () => {
|
|
148
|
+
it('reports index of failing element in path', () => {
|
|
149
|
+
const schema = enforce.tuple(
|
|
150
|
+
enforce.isString(),
|
|
151
|
+
enforce.isNumber(),
|
|
152
|
+
enforce.isBoolean(),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// @ts-expect-error - intentionally passing wrong type
|
|
156
|
+
const result = schema.run(['ok', 'bad', true]);
|
|
157
|
+
expect(result.pass).toBe(false);
|
|
158
|
+
expect(result.path).toBeDefined();
|
|
159
|
+
expect(result.path![0]).toBe('1');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('reports nested error path through tuple boundary', () => {
|
|
163
|
+
const schema = enforce.tuple(
|
|
164
|
+
enforce.isString(),
|
|
165
|
+
enforce.shape({ id: enforce.isNumber() }),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// @ts-expect-error - intentionally passing wrong type for nested field
|
|
169
|
+
const result = schema.run(['ok', { id: 'bad' }]);
|
|
170
|
+
expect(result.pass).toBe(false);
|
|
171
|
+
expect(result.path).toEqual(['1', 'id']);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('RuleInstance methods', () => {
|
|
176
|
+
it('.test() returns boolean', () => {
|
|
177
|
+
const schema = enforce.tuple(enforce.isString(), enforce.isNumber());
|
|
178
|
+
expect(typeof schema.test(['a', 1])).toBe('boolean');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('.run() returns RuleRunReturn', () => {
|
|
182
|
+
const schema = enforce.tuple(enforce.isString(), enforce.isNumber());
|
|
183
|
+
expect(schema.run(['a', 1]).pass).toBe(true);
|
|
184
|
+
// @ts-expect-error - intentionally passing wrong type
|
|
185
|
+
expect(schema.run(['a', 'b']).pass).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('.validate() returns StandardSchema result', () => {
|
|
189
|
+
const schema = enforce.tuple(enforce.isString(), enforce.isNumber());
|
|
190
|
+
const passing = schema.validate(['a', 1]);
|
|
191
|
+
expect(passing).toHaveProperty('value');
|
|
192
|
+
expect(passing).not.toHaveProperty('issues');
|
|
193
|
+
|
|
194
|
+
// @ts-expect-error - intentionally passing wrong type
|
|
195
|
+
const failing = schema.validate(['a', 'b']);
|
|
196
|
+
expect(failing).toHaveProperty('issues');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('.parse() returns value on success and throws on failure', () => {
|
|
200
|
+
const schema = enforce.tuple(enforce.isString(), enforce.isNumber());
|
|
201
|
+
expect(schema.parse(['a', 1])).toEqual(['a', 1]);
|
|
202
|
+
// @ts-expect-error - intentionally passing wrong type
|
|
203
|
+
expect(() => schema.parse(['a', 'b'])).toThrow();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('.message() override', () => {
|
|
208
|
+
it('supports custom message', () => {
|
|
209
|
+
const schema = enforce
|
|
210
|
+
.tuple(enforce.isString(), enforce.isNumber())
|
|
211
|
+
.message('Invalid coordinate');
|
|
212
|
+
const result = schema.run(['a', 'b']);
|
|
213
|
+
expect(result.pass).toBe(false);
|
|
214
|
+
expect(result.message).toBe('Invalid coordinate');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('type inference', () => {
|
|
219
|
+
it('infers tuple type from rules', () => {
|
|
220
|
+
const schema = enforce.tuple(
|
|
221
|
+
enforce.isString(),
|
|
222
|
+
enforce.isNumber(),
|
|
223
|
+
enforce.isBoolean(),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
type Inferred = typeof schema.infer;
|
|
227
|
+
expectTypeOf<Inferred>().toEqualTypeOf<[string, number, boolean]>();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('enforce.infer<> works with tuple', () => {
|
|
231
|
+
const schema = enforce.tuple(enforce.isString(), enforce.isNumber());
|
|
232
|
+
|
|
233
|
+
type ViaInfer = enforce.infer<typeof schema>;
|
|
234
|
+
expectTypeOf<ViaInfer>().toEqualTypeOf<[string, number]>();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('infers tuple inside shape', () => {
|
|
238
|
+
const schema = enforce.shape({
|
|
239
|
+
name: enforce.isString(),
|
|
240
|
+
coords: enforce.tuple(enforce.isNumber(), enforce.isNumber()),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
type S = typeof schema.infer;
|
|
244
|
+
expectTypeOf<S>().toEqualTypeOf<{
|
|
245
|
+
name: string;
|
|
246
|
+
coords: [number, number];
|
|
247
|
+
}>();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('parse() return type is correctly inferred', () => {
|
|
251
|
+
const schema = enforce.tuple(enforce.isString(), enforce.isNumber());
|
|
252
|
+
// eslint-disable-next-line vitest/valid-expect
|
|
253
|
+
expectTypeOf(schema.parse).returns.toEqualTypeOf<[string, number]>();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ctx } from '../../enforceContext';
|
|
2
|
+
import { RuleInstance } from '../../utils/RuleInstance';
|
|
3
|
+
import { RuleRunReturn } from '../../utils/RuleRunReturn';
|
|
4
|
+
import { addToChain } from '../genRuleChain';
|
|
5
|
+
|
|
6
|
+
export type LazyRuleInstance<T> = RuleInstance<T, [T]>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a lazy schema wrapper for recursive/self-referencing schemas.
|
|
10
|
+
* The factory function is called on first validation and cached.
|
|
11
|
+
*
|
|
12
|
+
* @param factory - A function that returns the RuleInstance to delegate to
|
|
13
|
+
* @returns A RuleInstance that defers schema resolution to validation time
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* type Category = { name: string; children: Category[] };
|
|
18
|
+
*
|
|
19
|
+
* const categorySchema = enforce.shape({
|
|
20
|
+
* name: enforce.isString(),
|
|
21
|
+
* children: enforce.isArrayOf(enforce.lazy(() => categorySchema)),
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* categorySchema.test({
|
|
25
|
+
* name: 'Root',
|
|
26
|
+
* children: [
|
|
27
|
+
* { name: 'Child', children: [] },
|
|
28
|
+
* ],
|
|
29
|
+
* }); // true
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function lazy<T>(
|
|
33
|
+
factory: () => RuleInstance<T, any>,
|
|
34
|
+
): LazyRuleInstance<T> {
|
|
35
|
+
let cached: RuleInstance<T, any> | null = null;
|
|
36
|
+
|
|
37
|
+
const resolve = (): RuleInstance<T, any> => {
|
|
38
|
+
if (!cached) {
|
|
39
|
+
cached = factory();
|
|
40
|
+
}
|
|
41
|
+
return cached;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return addToChain<LazyRuleInstance<T>>({}, (value: any) => {
|
|
45
|
+
const result = ctx.run({ value }, () => resolve().run(value));
|
|
46
|
+
return RuleRunReturn.create(result, value);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { isObject, mapFirst } from 'vest-utils';
|
|
2
|
+
|
|
3
|
+
import { ctx } from '../../enforceContext';
|
|
4
|
+
import type { RuleInstance } from '../../utils/RuleInstance';
|
|
5
|
+
import { RuleRunReturn } from '../../utils/RuleRunReturn';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
findDangerousOwnKey,
|
|
9
|
+
ownKeys,
|
|
10
|
+
safeShallowCopy,
|
|
11
|
+
} from './schemaObjectUtils';
|
|
12
|
+
import type { InferShapeInput } from './schemaRulesTypes';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validates that an object's dynamic keys and/or values match provided rules.
|
|
16
|
+
* Like TypeScript's Record<K, V>, it checks elements against shape rules.
|
|
17
|
+
*
|
|
18
|
+
* @param value - The object to validate
|
|
19
|
+
* @param arg1 - Either the key rule (if arg2 is present) or the value rule
|
|
20
|
+
* @param arg2 - The value rule (if arg1 is the key rule)
|
|
21
|
+
* @returns RuleRunReturn indicating success or failure
|
|
22
|
+
*/
|
|
23
|
+
export function record<T extends Record<string, any>>(
|
|
24
|
+
value: T,
|
|
25
|
+
arg1: any,
|
|
26
|
+
arg2?: any,
|
|
27
|
+
): RuleRunReturn<T> {
|
|
28
|
+
if (!isObject(value) || Array.isArray(value))
|
|
29
|
+
return RuleRunReturn.Failing(value);
|
|
30
|
+
|
|
31
|
+
const rules = parseRules(arg1, arg2);
|
|
32
|
+
const dangerousKey = findDangerousOwnKey(value);
|
|
33
|
+
if (dangerousKey)
|
|
34
|
+
return createRecordFailure(
|
|
35
|
+
value,
|
|
36
|
+
dangerousKey,
|
|
37
|
+
RuleRunReturn.Failing(value),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const parsedValue: Record<string, any> = safeShallowCopy(value);
|
|
41
|
+
|
|
42
|
+
const failingResult = mapFirst(ownKeys(value), (key, breakout) => {
|
|
43
|
+
const errorRes = evaluateRecordEntry(key, value, rules, parsedValue);
|
|
44
|
+
if (errorRes) {
|
|
45
|
+
breakout(true, errorRes);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
(failingResult as RuleRunReturn<T>) ||
|
|
51
|
+
RuleRunReturn.Passing(parsedValue as T)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseRules(arg1: any, arg2?: any) {
|
|
56
|
+
if (arg2 !== undefined) return { keyRule: arg1, valueRule: arg2 };
|
|
57
|
+
return { keyRule: undefined, valueRule: arg1 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validateKey(
|
|
61
|
+
key: string,
|
|
62
|
+
keyRule: RuleInstance<any, any>,
|
|
63
|
+
): RuleRunReturn<any> {
|
|
64
|
+
return ctx.run({ value: key, set: true }, () => keyRule.run(key));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function evaluateRecordEntry<T extends Record<string, any>>(
|
|
68
|
+
key: string,
|
|
69
|
+
value: T,
|
|
70
|
+
rules: {
|
|
71
|
+
keyRule?: RuleInstance<any, any>;
|
|
72
|
+
valueRule: RuleInstance<any, any>;
|
|
73
|
+
},
|
|
74
|
+
parsedValue: Record<string, any>,
|
|
75
|
+
): RuleRunReturn<T> | void {
|
|
76
|
+
if (rules.keyRule) {
|
|
77
|
+
const keyRes = validateKey(key, rules.keyRule);
|
|
78
|
+
if (!keyRes.pass) return createRecordFailure(value, key, keyRes);
|
|
79
|
+
if (keyRes.type !== key) {
|
|
80
|
+
delete parsedValue[key];
|
|
81
|
+
key = keyRes.type;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const valRes = ctx.run({ value: value[key], set: true, meta: { key } }, () =>
|
|
86
|
+
rules.valueRule.run(value[key]),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (!valRes.pass) return createRecordFailure(value, key, valRes);
|
|
90
|
+
parsedValue[key] = valRes.type;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createRecordFailure<T extends Record<string, any>>(
|
|
94
|
+
value: T,
|
|
95
|
+
key: string,
|
|
96
|
+
ruleRes: RuleRunReturn<any>,
|
|
97
|
+
): RuleRunReturn<T> {
|
|
98
|
+
const currentPath = ruleRes.path || [];
|
|
99
|
+
const newRes = RuleRunReturn.Failing(value, ruleRes.message);
|
|
100
|
+
newRes.path = [key, ...currentPath];
|
|
101
|
+
return newRes as RuleRunReturn<T>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type RecordKey<K> = [K] extends [never]
|
|
105
|
+
? string
|
|
106
|
+
: K extends RuleInstance<any, any>
|
|
107
|
+
? K['infer'] extends PropertyKey
|
|
108
|
+
? K['infer']
|
|
109
|
+
: string
|
|
110
|
+
: string;
|
|
111
|
+
|
|
112
|
+
type RecordInputKey<K> = [K] extends [never]
|
|
113
|
+
? string
|
|
114
|
+
: K extends RuleInstance<any, any>
|
|
115
|
+
? InferShapeInput<K> extends PropertyKey
|
|
116
|
+
? InferShapeInput<K>
|
|
117
|
+
: string
|
|
118
|
+
: string;
|
|
119
|
+
|
|
120
|
+
export type RecordRuleInstance<
|
|
121
|
+
K extends RuleInstance<any, any> | never,
|
|
122
|
+
V extends RuleInstance<any, any>,
|
|
123
|
+
> = RuleInstance<
|
|
124
|
+
Record<RecordKey<K>, V['infer']>,
|
|
125
|
+
[Record<RecordInputKey<K>, InferShapeInput<V>>]
|
|
126
|
+
>;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import './schemaRulesLazyTypes';
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
isArrayOf,
|
|
5
|
+
isArrayOf as list,
|
|
6
|
+
type IsArrayOfRuleInstance,
|
|
7
|
+
} from './isArrayOf';
|
|
8
|
+
export { type LazyRuleInstance } from './lazy';
|
|
4
9
|
export { loose, type LooseRuleInstance } from './loose';
|
|
5
10
|
export { optional, type OptionalRuleInstance } from './optional';
|
|
6
11
|
export { partial, type PartialRuleInstance } from './partial';
|
|
7
12
|
export { pick, type PickRuleInstance } from './pick';
|
|
8
13
|
export { omit, type OmitRuleInstance } from './omit';
|
|
9
14
|
export { shape, type ShapeRuleInstance } from './shape';
|
|
15
|
+
export { record, type RecordRuleInstance } from './record';
|
|
16
|
+
export { tuple, type TupleRuleInstance } from './tuple';
|
|
10
17
|
export type { SchemaRuleLazyTypes } from './schemaRulesLazyTypes';
|
|
@@ -8,18 +8,23 @@ import './partial';
|
|
|
8
8
|
import './pick';
|
|
9
9
|
import './omit';
|
|
10
10
|
import './shape';
|
|
11
|
+
import './record';
|
|
12
|
+
import './tuple';
|
|
11
13
|
|
|
12
14
|
import type { RuleInstance } from '../../utils/RuleInstance';
|
|
13
15
|
import { MultiTypeInput, MultiTypeInputArgs } from './schemaRulesTypes';
|
|
14
16
|
|
|
15
17
|
import type {
|
|
16
18
|
IsArrayOfRuleInstance,
|
|
19
|
+
LazyRuleInstance,
|
|
17
20
|
LooseRuleInstance,
|
|
18
21
|
OptionalRuleInstance,
|
|
19
22
|
PartialRuleInstance,
|
|
20
23
|
PickRuleInstance,
|
|
21
24
|
OmitRuleInstance,
|
|
22
25
|
ShapeRuleInstance,
|
|
26
|
+
RecordRuleInstance,
|
|
27
|
+
TupleRuleInstance,
|
|
23
28
|
} from './schemaRules';
|
|
24
29
|
|
|
25
30
|
/**
|
|
@@ -29,6 +34,10 @@ export type SchemaRuleLazyTypes = {
|
|
|
29
34
|
isArrayOf: <Rules extends RuleInstance<any, any>[]>(
|
|
30
35
|
...rules: Rules
|
|
31
36
|
) => IsArrayOfRuleInstance<MultiTypeInput<Rules>, MultiTypeInputArgs<Rules>>;
|
|
37
|
+
list: <Rules extends RuleInstance<any, any>[]>(
|
|
38
|
+
...rules: Rules
|
|
39
|
+
) => IsArrayOfRuleInstance<MultiTypeInput<Rules>, MultiTypeInputArgs<Rules>>;
|
|
40
|
+
lazy: <T>(factory: () => RuleInstance<T, any>) => LazyRuleInstance<T>;
|
|
32
41
|
loose: <S extends Record<string, RuleInstance<any>>>(
|
|
33
42
|
schema: S,
|
|
34
43
|
) => LooseRuleInstance<S>;
|
|
@@ -49,4 +58,16 @@ export type SchemaRuleLazyTypes = {
|
|
|
49
58
|
shape: <S extends Record<string, RuleInstance<any>>>(
|
|
50
59
|
schema: S,
|
|
51
60
|
) => ShapeRuleInstance<S>;
|
|
61
|
+
record: {
|
|
62
|
+
<V extends RuleInstance<any, any>>(
|
|
63
|
+
valueRule: V,
|
|
64
|
+
): RecordRuleInstance<never, V>;
|
|
65
|
+
<K extends RuleInstance<string, any>, V extends RuleInstance<any, any>>(
|
|
66
|
+
keyRule: K,
|
|
67
|
+
valueRule: V,
|
|
68
|
+
): RecordRuleInstance<K, V>;
|
|
69
|
+
};
|
|
70
|
+
tuple: <Rules extends RuleInstance<any, any>[]>(
|
|
71
|
+
...rules: Rules
|
|
72
|
+
) => TupleRuleInstance<Rules>;
|
|
52
73
|
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { greaterThan, isFunction, longerThan } from 'vest-utils';
|
|
2
|
+
|
|
3
|
+
import { ctx } from '../../enforceContext';
|
|
4
|
+
import type { RuleInstance } from '../../utils/RuleInstance';
|
|
5
|
+
import { RuleRunReturn } from '../../utils/RuleRunReturn';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validates that a value is a fixed-length array (tuple) where each position
|
|
9
|
+
* matches the corresponding rule. Enforces exact length unless trailing
|
|
10
|
+
* elements use enforce.optional().
|
|
11
|
+
*
|
|
12
|
+
* Parsed values are propagated: if a rule transforms its input (e.g. toNumber),
|
|
13
|
+
* the parsed tuple returned via `.parse()` carries the transformed values.
|
|
14
|
+
*
|
|
15
|
+
* @param value - The array to validate
|
|
16
|
+
* @param rules - One RuleInstance per tuple position
|
|
17
|
+
* @returns RuleRunReturn indicating success or failure, with `.type` holding
|
|
18
|
+
* the parsed tuple on success
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* // Eager API
|
|
23
|
+
* enforce(['hello', 42]).tuple(enforce.isString(), enforce.isNumber());
|
|
24
|
+
*
|
|
25
|
+
* // Lazy API
|
|
26
|
+
* const coordSchema = enforce.tuple(enforce.isNumber(), enforce.isNumber());
|
|
27
|
+
* coordSchema.test([40.7, -74.0]); // true
|
|
28
|
+
* coordSchema.test([40.7]); // false — too few
|
|
29
|
+
* coordSchema.test([40.7, -74, 0]);// false — too many
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function tuple(value: unknown, ...rules: any[]): RuleRunReturn<any> {
|
|
33
|
+
if (!Array.isArray(value)) return RuleRunReturn.Failing(value);
|
|
34
|
+
|
|
35
|
+
// Determine minimum required length (all rules minus trailing optionals)
|
|
36
|
+
const requiredCount = countRequired(rules);
|
|
37
|
+
|
|
38
|
+
// Reject arrays that are too short or too long
|
|
39
|
+
if (
|
|
40
|
+
greaterThan(requiredCount, value.length) ||
|
|
41
|
+
longerThan(value, rules.length)
|
|
42
|
+
) {
|
|
43
|
+
return RuleRunReturn.Failing(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return validateElements(value, rules);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Counts the number of required (non-optional) leading positions by scanning
|
|
51
|
+
* backwards from the end of the rules array. Stops at the first non-optional
|
|
52
|
+
* rule, so only *trailing* optionals reduce the required count.
|
|
53
|
+
*/
|
|
54
|
+
function countRequired(rules: any[]): number {
|
|
55
|
+
let count = rules.length;
|
|
56
|
+
for (let i = rules.length - 1; i >= 0; i--) {
|
|
57
|
+
if (isOptionalRule(rules[i])) count = i;
|
|
58
|
+
else break;
|
|
59
|
+
}
|
|
60
|
+
return count;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Iterates over each rule position, validates the corresponding array element,
|
|
65
|
+
* and collects parsed output values. Returns early on the first failing element
|
|
66
|
+
* with an index-based error path.
|
|
67
|
+
*/
|
|
68
|
+
function validateElements(value: any[], rules: any[]): RuleRunReturn<any> {
|
|
69
|
+
const parsedTuple: any[] = [];
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < rules.length; i++) {
|
|
72
|
+
// Skip positions beyond the array length (only reached for trailing optionals)
|
|
73
|
+
if (isBeyondArrayEnd(value, i, rules[i])) continue;
|
|
74
|
+
|
|
75
|
+
const res = runElementRule(value[i], rules[i], i);
|
|
76
|
+
|
|
77
|
+
if (!res.pass) return elementFailure(value, res, i);
|
|
78
|
+
|
|
79
|
+
// Use the parsed value (res.type) if the rule transformed it, otherwise keep the original
|
|
80
|
+
parsedTuple.push(res.type ?? value[i]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return RuleRunReturn.Passing(parsedTuple);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Checks whether the given index is past the array's actual length
|
|
88
|
+
* and the corresponding rule is optional, meaning it can be skipped.
|
|
89
|
+
*/
|
|
90
|
+
function isBeyondArrayEnd(value: any[], index: number, rule: any): boolean {
|
|
91
|
+
return index >= value.length && isOptionalRule(rule);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Runs a single element's rule within an enforce context that carries
|
|
96
|
+
* the element value and its positional index as metadata.
|
|
97
|
+
*/
|
|
98
|
+
function runElementRule(
|
|
99
|
+
item: any,
|
|
100
|
+
rule: any,
|
|
101
|
+
index: number,
|
|
102
|
+
): RuleRunReturn<any> {
|
|
103
|
+
return ctx.run({ value: item, set: true, meta: { index } }, () =>
|
|
104
|
+
rule.run(item),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Builds a failing RuleRunReturn with an error path that includes the
|
|
110
|
+
* tuple index, prepended to any nested path from the inner rule failure.
|
|
111
|
+
* For example, a shape failure at index 1 on key "id" yields path ["1", "id"].
|
|
112
|
+
*/
|
|
113
|
+
function elementFailure(
|
|
114
|
+
value: any[],
|
|
115
|
+
res: RuleRunReturn<any>,
|
|
116
|
+
index: number,
|
|
117
|
+
): RuleRunReturn<any> {
|
|
118
|
+
const failure = RuleRunReturn.Failing(value, res.message);
|
|
119
|
+
failure.path = [index.toString(), ...(res.path || [])];
|
|
120
|
+
return failure;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Determines whether a rule is optional by testing if it passes with undefined.
|
|
125
|
+
* This mirrors how shape/loose detect optional fields — a rule wrapping
|
|
126
|
+
* enforce.optional() will pass for undefined, while required rules will not.
|
|
127
|
+
*/
|
|
128
|
+
function isOptionalRule(rule: RuleInstance<any, any>): boolean {
|
|
129
|
+
if (!rule || !isFunction(rule.test)) return false;
|
|
130
|
+
return rule.test(undefined);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Maps a tuple of RuleInstances to their inferred output types.
|
|
135
|
+
* [RuleInstance<string>, RuleInstance<number>] → [string, number]
|
|
136
|
+
*/
|
|
137
|
+
type InferTuple<T extends RuleInstance<any, any>[]> = {
|
|
138
|
+
[K in keyof T]: T[K] extends RuleInstance<infer R, any> ? R : never;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Maps a tuple of RuleInstances to their inferred input types.
|
|
143
|
+
* Used for the Args parameter of the returned RuleInstance so that
|
|
144
|
+
* .test() and .parse() accept correctly typed tuple input.
|
|
145
|
+
*/
|
|
146
|
+
type InferTupleInput<T extends RuleInstance<any, any>[]> = {
|
|
147
|
+
[K in keyof T]: T[K] extends RuleInstance<any, [infer A, ...any[]]>
|
|
148
|
+
? A
|
|
149
|
+
: never;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The RuleInstance type returned by enforce.tuple().
|
|
154
|
+
* Infers both output and input types from the provided rule tuple.
|
|
155
|
+
*/
|
|
156
|
+
export type TupleRuleInstance<T extends RuleInstance<any, any>[]> =
|
|
157
|
+
RuleInstance<InferTuple<T>, [InferTupleInput<T>]>;
|
package/types/exports/date.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as WidenFirstParam } from "../n4sTypes-
|
|
1
|
+
import { n as WidenFirstParam } from "../n4sTypes-BSTzXRsU.cjs";
|
|
2
2
|
import isAfter from "validator/es/lib/isAfter";
|
|
3
3
|
import isBefore from "validator/es/lib/isBefore";
|
|
4
4
|
import isDate from "validator/es/lib/isDate";
|
package/types/exports/date.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as WidenFirstParam } from "../n4sTypes-
|
|
1
|
+
import { n as WidenFirstParam } from "../n4sTypes-3THfSmAQ.mjs";
|
|
2
2
|
import isAfter from "validator/es/lib/isAfter";
|
|
3
3
|
import isBefore from "validator/es/lib/isBefore";
|
|
4
4
|
import isDate from "validator/es/lib/isDate";
|