Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ArrayValidator): add length ranges #53

Merged
merged 6 commits into from
Mar 5, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ const stringArray = s.array(s.string);
const stringArray = s.string.array;
```

ShapeShift includes a handful of string-specific validations:
ShapeShift includes a handful of array-specific validations:

```typescript
s.string.array.lengthLt(5); // Must have less than 5 elements
Expand All @@ -210,6 +210,9 @@ s.string.array.lengthGt(5); // Must have more than 5 elements
s.string.array.lengthGe(5); // Must have 5 or more elements
s.string.array.lengthEq(5); // Must have exactly 5 elements
s.string.array.lengthNe(5); // Must not have exactly 5 elements
s.string.array.lengthRange(0, 4); // Must have at least 0 elements and less than 4 elements (in math, that is [0, 4))
vladfrangu marked this conversation as resolved.
Show resolved Hide resolved
vladfrangu marked this conversation as resolved.
Show resolved Hide resolved
s.string.array.lengthRangeInclusive(0, 4); // Must have at least 0 elements and at most 4 elements (in math, that is [0, 4])
s.string.array.lengthRangeExclusive(0, 4); // Must have more than 0 element and less than 4 elements (in math, that is [firstNumber + 1, 4))
```

> **Note**: All `.length` methods define tuple types with the given amount of elements. For example, `s.string.array.lengthGe(2)`'s inferred type is `[string, string, ...string[]]`
Expand Down
35 changes: 34 additions & 1 deletion src/constraints/ArrayLengthConstraints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Result } from '../lib/Result';
import type { IConstraint } from './base/IConstraint';
import { Comparator, eq, ge, gt, le, lt, ne } from './util/operators';

export type ArrayConstraintName = `s.array(T).length${'Lt' | 'Le' | 'Gt' | 'Ge' | 'Eq' | 'Ne'}`;
export type ArrayConstraintName = `s.array(T).length${'Lt' | 'Le' | 'Gt' | 'Ge' | 'Eq' | 'Ne' | 'Range' | 'RangeInclusive' | 'RangeExclusive'}`;

function arrayLengthComparator<T>(comparator: Comparator, name: ArrayConstraintName, expected: string, length: number): IConstraint<T[]> {
return {
Expand Down Expand Up @@ -44,3 +44,36 @@ export function arrayLengthNe<T>(value: number): IConstraint<T[]> {
const expected = `expected.length !== ${value}`;
return arrayLengthComparator(ne, 's.array(T).lengthNe', expected, value);
}

export function arrayLengthRange<T>(start: number, endBefore: number): IConstraint<T[]> {
const expected = `expected.length >= ${start} && expected.length < ${endBefore}`;
return {
run(input: T[]) {
return input.length >= start && input.length < endBefore //
? Result.ok(input)
: Result.err(new ConstraintError('s.array(T).lengthRange', 'Invalid Array length', input, expected));
}
};
}

export function arrayLengthRangeInclusive<T>(start: number, end: number): IConstraint<T[]> {
const expected = `expected.length >= ${start} && expected.length <= ${end}`;
return {
run(input: T[]) {
return input.length >= start && input.length <= end //
? Result.ok(input)
: Result.err(new ConstraintError('s.array(T).lengthRangeInclusive', 'Invalid Array length', input, expected));
}
};
}

export function arrayLengthRangeExclusive<T>(startAfter: number, endBefore: number): IConstraint<T[]> {
const expected = `expected.length > ${startAfter} && expected.length < ${endBefore}`;
return {
run(input: T[]) {
return input.length > startAfter && input.length < endBefore //
? Result.ok(input)
: Result.err(new ConstraintError('s.array(T).lengthRangeExclusive', 'Invalid Array length', input, expected));
}
};
}
4 changes: 2 additions & 2 deletions src/constraints/StringConstraints.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { isIP, isIPv4, isIPv6 } from 'node:net';
import { ConstraintError } from '../lib/errors/ConstraintError';
import { Result } from '../lib/Result';
import type { IConstraint } from './base/IConstraint';
import { Comparator, eq, ge, gt, le, lt, ne } from './util/operators';
import { isIP, isIPv4, isIPv6 } from 'node:net';
import { validateEmail } from './util/emailValidator';
import { Comparator, eq, ge, gt, le, lt, ne } from './util/operators';

export type StringConstraintName =
| `s.string.${`length${'Lt' | 'Le' | 'Gt' | 'Ge' | 'Eq' | 'Ne'}` | 'regex' | 'url' | 'uuid' | 'email' | `ip${'v4' | 'v6' | ''}`}`;
Expand Down
14 changes: 8 additions & 6 deletions src/constraints/type-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ export type {
arrayLengthGt,
arrayLengthLe,
arrayLengthLt,
arrayLengthNe
arrayLengthNe,
arrayLengthRange,
arrayLengthRangeInclusive
} from './ArrayLengthConstraints';
export type { IConstraint } from './base/IConstraint';
export type { BigIntConstraintName, bigintEq, bigintGe, bigintGt, bigintLe, bigintLt, bigintNe, bigintDivisibleBy } from './BigIntConstraints';
export type { BigIntConstraintName, bigintDivisibleBy, bigintEq, bigintGe, bigintGt, bigintLe, bigintLt, bigintNe } from './BigIntConstraints';
export type { BooleanConstraintName, booleanFalse, booleanTrue } from './BooleanConstraints';
export type { DateConstraintName, dateEq, dateGe, dateGt, dateInvalid, dateLe, dateLt, dateNe, dateValid } from './DateConstraints';
export type {
Expand All @@ -28,17 +30,17 @@ export type {
} from './NumberConstraints';
export type {
StringConstraintName,
StringProtocol,
StringDomain,
UrlOptions,
stringEmail,
stringIp,
stringLengthEq,
stringLengthGe,
stringLengthGt,
stringLengthLe,
stringLengthLt,
stringLengthNe,
stringEmail,
StringProtocol,
stringRegex,
stringUrl,
stringIp
UrlOptions
} from './StringConstraints';
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,5 @@ export * from './lib/errors/ExpectedValidationError';
export * from './lib/errors/MissingPropertyError';
export * from './lib/errors/UnknownPropertyError';
export * from './lib/errors/ValidationError';

export * from './lib/Result';
export * from './type-exports';
43 changes: 37 additions & 6 deletions src/validators/ArrayValidator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { arrayLengthEq, arrayLengthGe, arrayLengthGt, arrayLengthLe, arrayLengthLt, arrayLengthNe } from '../constraints/ArrayLengthConstraints';
import {
arrayLengthEq,
arrayLengthGe,
arrayLengthGt,
arrayLengthLe,
arrayLengthLt,
arrayLengthNe,
arrayLengthRange,
arrayLengthRangeExclusive,
arrayLengthRangeInclusive
} from '../constraints/ArrayLengthConstraints';
import type { IConstraint } from '../constraints/base/IConstraint';
import type { BaseError } from '../lib/errors/BaseError';
import { CombinedPropertyError } from '../lib/errors/CombinedPropertyError';
Expand All @@ -19,23 +29,44 @@ export class ArrayValidator<T> extends BaseValidator<T[]> {
}

public lengthLe<N extends number>(length: N): BaseValidator<ExpandSmallerTuples<[...Tuple<T, N>]>> {
return this.addConstraint(arrayLengthLe(length) as IConstraint<T[]>) as any;
return this.addConstraint(arrayLengthLe(length)) as any;
}

public lengthGt<N extends number>(length: N): BaseValidator<[...Tuple<T, N>, T, ...T[]]> {
return this.addConstraint(arrayLengthGt(length) as IConstraint<T[]>) as any;
return this.addConstraint(arrayLengthGt(length)) as any;
}

public lengthGe<N extends number>(length: N): BaseValidator<[...Tuple<T, N>, ...T[]]> {
return this.addConstraint(arrayLengthGe(length) as IConstraint<T[]>) as any;
return this.addConstraint(arrayLengthGe(length)) as any;
}

public lengthEq<N extends number>(length: N): BaseValidator<[...Tuple<T, N>]> {
return this.addConstraint(arrayLengthEq(length) as IConstraint<T[]>) as any;
return this.addConstraint(arrayLengthEq(length)) as any;
}

public lengthNe(length: number): BaseValidator<[...T[]]> {
return this.addConstraint(arrayLengthNe(length) as IConstraint<T[]>);
return this.addConstraint(arrayLengthNe(length)) as any;
}

public lengthRange<S extends number, E extends number>(
start: S,
endBefore: E
): BaseValidator<Exclude<ExpandSmallerTuples<UnshiftTuple<[...Tuple<T, E>]>>, ExpandSmallerTuples<UnshiftTuple<[...Tuple<T, S>]>>>> {
return this.addConstraint(arrayLengthRange(start, endBefore)) as any;
}

public lengthRangeInclusive<S extends number, E extends number>(
startAt: S,
endAt: E
): BaseValidator<Exclude<ExpandSmallerTuples<[...Tuple<T, E>]>, ExpandSmallerTuples<UnshiftTuple<[...Tuple<T, S>]>>>> {
return this.addConstraint(arrayLengthRangeInclusive(startAt, endAt)) as any;
}

public lengthRangeExclusive<S extends number, E extends number>(
startAfter: S,
endBefore: E
): BaseValidator<Exclude<ExpandSmallerTuples<UnshiftTuple<[...Tuple<T, E>]>>, ExpandSmallerTuples<[...Tuple<T, S>]>>> {
return this.addConstraint(arrayLengthRangeExclusive(startAfter, endBefore)) as any;
}

protected override clone(): this {
Expand Down
2 changes: 1 addition & 1 deletion src/validators/BaseValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { CombinedError } from '../lib/errors/CombinedError';
import type { CombinedPropertyError } from '../lib/errors/CombinedPropertyError';
import type { ValidationError } from '../lib/errors/ValidationError';
import { Result } from '../lib/Result';
import { ArrayValidator, LiteralValidator, NullishValidator, SetValidator, UnionValidator, DefaultValidator } from './imports';
import { ArrayValidator, DefaultValidator, LiteralValidator, NullishValidator, SetValidator, UnionValidator } from './imports';

export abstract class BaseValidator<T> {
protected constraints: readonly IConstraint<T>[] = [];
Expand Down
2 changes: 1 addition & 1 deletion src/validators/BigIntValidator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IConstraint } from '../constraints/base/IConstraint';
import { bigintEq, bigintGe, bigintGt, bigintLe, bigintLt, bigintNe, bigintDivisibleBy } from '../constraints/BigIntConstraints';
import { bigintDivisibleBy, bigintEq, bigintGe, bigintGt, bigintLe, bigintLt, bigintNe } from '../constraints/BigIntConstraints';
import { ValidationError } from '../lib/errors/ValidationError';
import { Result } from '../lib/Result';
import { BaseValidator } from './imports';
Expand Down
8 changes: 4 additions & 4 deletions src/validators/TupleValidator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { BaseValidator } from './imports';
import { Result } from '../lib/Result';
import { ValidationError } from '../lib/errors/ValidationError';
import { CombinedPropertyError } from '../lib/errors/CombinedPropertyError';
import type { IConstraint } from '../constraints/base/IConstraint';
import type { BaseError } from '../lib/errors/BaseError';
import { CombinedPropertyError } from '../lib/errors/CombinedPropertyError';
import { ValidationError } from '../lib/errors/ValidationError';
import { Result } from '../lib/Result';
import { BaseValidator } from './imports';

export class TupleValidator<T extends any[]> extends BaseValidator<[...T]> {
private readonly validators: BaseValidator<[...T]>[] = [];
Expand Down
89 changes: 66 additions & 23 deletions tests/validators/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,7 @@ describe('ArrayValidator', () => {
expect<[string] | []>(lengthLtPredicate.parse(value)).toEqual(value);
});

test.each([
[
['Hello', 'there'],
['foo', 'bar', 'baaz']
]
])('GIVEN %p THEN throws ConstraintError', (value) => {
test.each([[['Hello', 'there']], [['foo', 'bar', 'baaz']]])('GIVEN %p THEN throws ConstraintError', (value) => {
expectError(
() => lengthLtPredicate.parse(value),
new ConstraintError('s.array(T).lengthLt', 'Invalid Array length', value, 'expected.length < 2')
Expand All @@ -46,7 +41,7 @@ describe('ArrayValidator', () => {
describe('lengthLe', () => {
const lengthLePredicate = s.string.array.lengthLe(2);

test.each([[['Hello'], ['Hello', 'there']]])('GIVEN %p THEN returns given value', (value) => {
test.each([[['Hello']], [['Hello', 'there']]])('GIVEN %p THEN returns given value', (value) => {
expect<[string, string] | [string] | []>(lengthLePredicate.parse(value)).toEqual(value);
});

Expand All @@ -65,7 +60,7 @@ describe('ArrayValidator', () => {
expect<[string, string, string, ...string[]]>(lengthGtPredicate.parse(value)).toEqual(value);
});

test.each([[['Hello'], []]])('GIVEN %p THEN throws ConstraintError', (value) => {
test.each([[['Hello']], [[]]])('GIVEN %p THEN throws ConstraintError', (value) => {
expectError(
() => lengthGtPredicate.parse(value),
new ConstraintError('s.array(T).lengthGt', 'Invalid Array length', value, 'expected.length > 2')
Expand All @@ -76,16 +71,11 @@ describe('ArrayValidator', () => {
describe('lengthGe', () => {
const lengthGePredicate = s.string.array.lengthGe(2);

test.each([
[
['Hello', 'there'],
['foo', 'bar', 'baaz']
]
])('GIVEN %p THEN returns given value', (value) => {
test.each([[['Hello', 'there']], [['foo', 'bar', 'baaz']]])('GIVEN %p THEN returns given value', (value) => {
expect<[string, string, ...string[]]>(lengthGePredicate.parse(value)).toEqual(value);
});

test.each([[[], ['foo']]])('GIVEN %p THEN throws ConstraintError', (value) => {
test.each([[[]], [['foo']]])('GIVEN %p THEN throws ConstraintError', (value) => {
expectError(
() => lengthGePredicate.parse(value),
new ConstraintError('s.array(T).lengthGe', 'Invalid Array length', value, 'expected.length >= 2')
Expand All @@ -100,7 +90,7 @@ describe('ArrayValidator', () => {
expect<[string, string]>(lengthPredicate.parse(value)).toEqual(value);
});

test.each([[[], ['Hello']]])('GIVEN %p THEN throws ConstraintError', (value) => {
test.each([[[]], [['Hello']]])('GIVEN %p THEN throws ConstraintError', (value) => {
expectError(
() => lengthPredicate.parse(value),
new ConstraintError('s.array(T).lengthEq', 'Invalid Array length', value, 'expected.length === 2')
Expand All @@ -111,22 +101,75 @@ describe('ArrayValidator', () => {
describe('lengthNe', () => {
const lengthNotEqPredicate = s.string.array.lengthNe(2);

test.each([[['foo', 'bar', 'baaz'], ['foo']]])('GIVEN %p THEN returns given value', (value) => {
test.each([[['foo', 'bar', 'baaz']], [['foo']]])('GIVEN %p THEN returns given value', (value) => {
expect<string[]>(lengthNotEqPredicate.parse(value)).toEqual(value);
});

test.each([
[
['Hello', 'there'],
['foo', 'bar']
]
])('GIVEN %p THEN throws ConstraintError', (value) => {
test.each([[['Hello', 'there']], [['foo', 'bar']]])('GIVEN %p THEN throws ConstraintError', (value) => {
expectError(
() => lengthNotEqPredicate.parse(value),
new ConstraintError('s.array(T).lengthNe', 'Invalid Array length', value, 'expected.length !== 2')
);
});
});

describe('lengthRange', () => {
const lengthRangePredicate = s.string.array.lengthRange(0, 2);

test.each([[[] as string[]], [['foo']]])('GIVEN %p THEN returns given value', (value) => {
expect<[] | [string]>(lengthRangePredicate.parse(value)).toEqual(value);
});

test.each([[['hewwo', 'there']]])('GIVEN %p THEN throws ConstraintError', (value) => {
expectError(
() => lengthRangePredicate.parse(value),
new ConstraintError('s.array(T).lengthRange', 'Invalid Array length', value, 'expected.length >= 0 && expected.length < 2')
);
});
});

describe('lengthRangeInclusive', () => {
const lengthRangeInclusivePredicate = s.string.array.lengthRangeInclusive(0, 2);

test.each([[[] as string[]], [['foo']], [['hewwo', 'there']]])('GIVEN %p THEN returns given value', (value) => {
expect<[] | [string] | [string, string]>(lengthRangeInclusivePredicate.parse(value)).toEqual(value);
});

test.each([[['hewwo', 'there', 'buddy']]])('GIVEN %p THEN throws ConstraintError', (value) => {
expectError(
() => lengthRangeInclusivePredicate.parse(value),
new ConstraintError(
's.array(T).lengthRangeInclusive',
'Invalid Array length',
value,
'expected.length >= 0 && expected.length <= 2'
)
);
});

describe('lengthRangeExclusive', () => {
const lengthRangeExclusivePredicate = s.string.array.lengthRangeExclusive(0, 2);

test.each([[['foo']]])('GIVEN %p THEN returns given value', (value) => {
expect<[string]>(lengthRangeExclusivePredicate.parse(value)).toEqual(value);
});

test.each([[[] as string[]], [['hewwo', 'there']], [['hewwo', 'there', 'buddy']]])(
'GIVEN %p THEN throws ConstraintError',
(value) => {
expectError(
() => lengthRangeExclusivePredicate.parse(value),
new ConstraintError(
's.array(T).lengthRangeExclusive',
'Invalid Array length',
value,
'expected.length > 0 && expected.length < 2'
)
);
}
);
});
});
});

test('GIVEN clone THEN returns similar instance', () => {
Expand Down
Loading