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.
Files changed (42) hide show
  1. package/dist/exports/date.cjs +1 -1
  2. package/dist/exports/date.mjs +1 -1
  3. package/dist/exports/email.cjs +1 -1
  4. package/dist/exports/email.mjs +1 -1
  5. package/dist/exports/isURL.cjs +1 -1
  6. package/dist/exports/isURL.mjs +1 -1
  7. package/dist/{n4s-CoDF5Fg6.cjs → n4s-BTHEz-bJ.cjs} +215 -5
  8. package/dist/n4s-BTHEz-bJ.cjs.map +1 -0
  9. package/dist/{n4s-KWquSyTb.mjs → n4s-BxrvnvKp.mjs} +218 -8
  10. package/dist/n4s-BxrvnvKp.mjs.map +1 -0
  11. package/dist/n4s.cjs +1 -1
  12. package/dist/n4s.mjs +1 -1
  13. package/package.json +5 -1
  14. package/src/eager/eagerTypes.ts +14 -0
  15. package/src/lazy.ts +24 -0
  16. package/src/n4s.ts +5 -0
  17. package/src/rules/schemaRules/__tests__/integrationSchemaRules.types.test.ts +92 -0
  18. package/src/rules/schemaRules/__tests__/isArrayOf.test.ts +37 -1
  19. package/src/rules/schemaRules/__tests__/lazy.test.ts +312 -0
  20. package/src/rules/schemaRules/__tests__/record.test.ts +205 -0
  21. package/src/rules/schemaRules/__tests__/schema.parse.integration.test.ts +79 -0
  22. package/src/rules/schemaRules/__tests__/tuple.test.ts +256 -0
  23. package/src/rules/schemaRules/lazy.ts +48 -0
  24. package/src/rules/schemaRules/record.ts +126 -0
  25. package/src/rules/schemaRules/schemaRules.ts +8 -1
  26. package/src/rules/schemaRules/schemaRulesLazyTypes.ts +21 -0
  27. package/src/rules/schemaRules/tuple.ts +157 -0
  28. package/types/exports/date.d.cts +1 -1
  29. package/types/exports/date.d.mts +1 -1
  30. package/types/n4s.d.cts +21 -2
  31. package/types/n4s.d.cts.map +1 -1
  32. package/types/n4s.d.mts +78 -69
  33. package/types/n4s.d.mts.map +1 -1
  34. package/types/n4s.d.ts +21 -2
  35. package/types/{n4sTypes-Bb1zNxyv.d.mts → n4sTypes-3THfSmAQ.d.mts} +79 -5
  36. package/types/n4sTypes-3THfSmAQ.d.mts.map +1 -0
  37. package/types/{n4sTypes-ChCugpFQ.d.cts → n4sTypes-BSTzXRsU.d.cts} +66 -2
  38. package/types/n4sTypes-BSTzXRsU.d.cts.map +1 -0
  39. package/dist/n4s-CoDF5Fg6.cjs.map +0 -1
  40. package/dist/n4s-KWquSyTb.mjs.map +0 -1
  41. package/types/n4sTypes-Bb1zNxyv.d.mts.map +0 -1
  42. 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 { isArrayOf, type IsArrayOfRuleInstance } from './isArrayOf';
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>]>;
@@ -1,4 +1,4 @@
1
- import { n as WidenFirstParam } from "../n4sTypes-ChCugpFQ.cjs";
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";
@@ -1,4 +1,4 @@
1
- import { n as WidenFirstParam } from "../n4sTypes-Bb1zNxyv.mjs";
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";