Skip to content

Commit

Permalink
feat: added more primitives (#2)
Browse files Browse the repository at this point in the history
Co-authored-by: Vlad Frangu <[email protected]>
  • Loading branch information
kyranet and vladfrangu authored Dec 12, 2021
1 parent 80f1522 commit 16af17b
Show file tree
Hide file tree
Showing 14 changed files with 379 additions and 22 deletions.
67 changes: 61 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Creating a simple string schema
```typescript
import { s } from '@sapphire/shapeshift';

const mySchema = z.string;
const mySchema = s.string;

// Parse
mySchema.parse('sapphire'); // => returns 'sapphire'
Expand Down Expand Up @@ -66,9 +66,9 @@ import { s } from '@sapphire/shapeshift';
// Primitives
s.string;
s.number;
s.bigint; // TODO
s.boolean; // TODO
s.date; // TODO
s.bigint;
s.boolean;
s.date;

// Empty Types
s.undefined
Expand All @@ -80,7 +80,7 @@ s.any;
s.unknown

// Never Type
s.never; // TODO
s.never;
```

#### Literals
Expand All @@ -90,7 +90,7 @@ s.literal('sapphire');
s.literal(12);
s.literal(420n);
s.literal(true);
s.literal(new Date(1639278160000)); // TODO | s.date.eq(1639278160000);
s.literal(new Date(1639278160000)); // s.date.eq(1639278160000);
```

#### Strings
Expand Down Expand Up @@ -134,6 +134,61 @@ s.number.negative; // .lt(0)
s.number.divisibleBy(5); // TODO | Divisible by 5
```

And transformations:

```typescript
s.number.abs; // TODO | Transforms the number to an absolute number
s.number.sign; // TODO | Gets the number's sign

s.number.trunc; // TODO | Transforms the number to the result of Math.trunc`
s.number.floor; // TODO | Transforms the number to the result of Math.floor`
s.number.fround; // TODO | Transforms the number to the result of Math.fround`
s.number.round; // TODO | Transforms the number to the result of Math.round`
s.number.ceil; // TODO | Transforms the number to the result of Math.ceil`
```

#### BigInts

ShapeShift includes a handful of number-specific validations:

```typescript
s.bigint.gt(5n); // > 5n
s.bigint.ge(5n); // >= 5n
s.bigint.lt(5n); // < 5n
s.bigint.le(5n); // <= 5n
s.bigint.eq(5n); // === 5n
s.bigint.ne(5n); // !== 5n

s.bigint.positive; // .ge(0n)
s.bigint.negative; // .lt(0n)

s.bigint.divisibleBy(5n); // TODO | Divisible by 5n
```

And transformations:

```typescript
s.bigint.abs; // TODO | Transforms the bigint to an absolute bigint

s.bigint.intN(5); // TODO | Clamps to a bigint to a signed bigint with 5 digits, see BigInt.asIntN
s.bigint.uintN(5); // TODO | Clamps to a bigint to an unsigned bigint with 5 digits, see BigInt.asUintN
```

#### Booleans

ShapeShift includes a few boolean-specific validations:

```typescript
s.boolean.true; // value must be true
s.boolean.false; // value must be false

s.boolean.eq(true); // s.boolean.true
s.boolean.eq(false); // s.boolean.false

s.boolean.ne(true); // s.boolean.false
s.boolean.ne(false); // s.boolean.true
```

#### Arrays

```typescript
Expand Down
56 changes: 56 additions & 0 deletions src/constraints/BigIntConstraints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ConstraintError, ConstraintErrorMessageBuilder } from '../lib/errors/ConstraintError';
import { Result } from '../lib/Result';
import type { IConstraint } from './base/IConstraint';
import { Comparator, eq, gt, ge, lt, le, ne } from './util/operators';

function bigintComparator(
comparator: Comparator,
name: string,
messageBuilder: ConstraintErrorMessageBuilder<bigint>,
number: bigint
): IConstraint<bigint> {
return {
run(input: bigint) {
return comparator(input, number) //
? Result.ok(input)
: Result.err(new ConstraintError(name, messageBuilder(input, number), input, number));
}
};
}

export const bigintLt = bigintComparator.bind(
null,
lt,
'bigintLt',
(given, expected) => `Expected bigint to be less than ${expected}, but received ${given}`
);

export const bigintLe = bigintComparator.bind(
null,
le,
'bigintLe',
(given, expected) => `Expected bigint to be less or equals than ${expected}, but received ${given}`
);

export const bigintGt = bigintComparator.bind(
null,
gt,
'bigintGt',
(given, expected) => `Expected bigint to be greater than ${expected}, but received ${given}`
);

export const bigintGe = bigintComparator.bind(
null,
ge,
'bigintGe',
(given, expected) => `Expected bigint to be greater or equals than ${expected}, but received ${given}`
);

export const bigintEq = bigintComparator.bind(
null,
eq,
'bigintEq',
(given, expected) => `Expected bigint to be exactly ${expected}, but received ${given}`
);

export const bigintNe = bigintComparator.bind(null, ne, 'bigintNe', (_, expected) => `Expected bigint to not be ${expected}`);
19 changes: 19 additions & 0 deletions src/constraints/BooleanConstraints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ConstraintError } from '../lib/errors/ConstraintError';
import { Result } from '../lib/Result';
import type { IConstraint } from './base/IConstraint';

export const booleanTrue: IConstraint<boolean, true> = {
run(input: boolean) {
return input //
? Result.ok(input)
: Result.err(new ConstraintError('booleanTrue', 'Expected boolean to be true, but received false', input, true));
}
};

export const booleanFalse: IConstraint<boolean, false> = {
run(input: boolean) {
return input //
? Result.err(new ConstraintError('booleanFalse', 'Expected boolean to be false, but received true', input, false))
: Result.ok(input);
}
};
67 changes: 67 additions & 0 deletions src/constraints/DateConstraints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ConstraintError, ConstraintErrorMessageBuilder } 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';

function dateComparator(
comparator: Comparator,
name: string,
messageBuilder: ConstraintErrorMessageBuilder<Date>,
date: Date,
number = date.getTime()
): IConstraint<Date> {
return {
run(input: Date) {
return comparator(input.getTime(), number) //
? Result.ok(input)
: Result.err(new ConstraintError(name, messageBuilder(input, date), input, date));
}
};
}

export const dateLt = dateComparator.bind(
null,
lt,
'dateLt',
(given, expected) => `Expected date to be earlier than ${expected}, but received ${given}`
);

export const dateLe = dateComparator.bind(
null,
le,
'dateLe',
(given, expected) => `Expected date to be earlier or equals than ${expected}, but received ${given}`
);

export const dateGt = dateComparator.bind(
null,
gt,
'dateGt',
(given, expected) => `Expected date to be later than ${expected}, but received ${given}`
);

export const dateGe = dateComparator.bind(
null,
ge,
'dateGe',
(given, expected) => `Expected date to be later or equals than ${expected}, but received ${given}`
);

export const dateEq = dateComparator.bind(null, eq, 'dateEq', (given, expected) => `Expected date to be exactly ${expected}, but received ${given}`);
export const dateNe = dateComparator.bind(null, ne, 'dateNe', (_, expected) => `Expected date to not be ${expected}`);

export const dateInvalid: IConstraint<Date> = {
run(input: Date) {
return Number.isNaN(input.getTime()) //
? Result.ok(input)
: Result.err(new ConstraintError('dateInvalid', `Expected Date's time to be a NaN, but received ${input}`, input, 'An invalid Date'));
}
};

export const dateValid: IConstraint<Date> = {
run(input: Date) {
return Number.isNaN(input.getTime()) //
? Result.err(new ConstraintError('dateValid', `Expected Date's time to not be a NaN, but received ${input}`, input, 'A valid Date'))
: Result.ok(input);
}
};
2 changes: 1 addition & 1 deletion src/constraints/NumberConstraints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const numberEq = numberComparator.bind(
(given, expected) => `Expected number to be exactly ${expected}, but received ${given}`
);

export const numberNe = numberComparator.bind(null, ne, 'numberNe', (_, expected) => `Expected string to not be ${expected}`);
export const numberNe = numberComparator.bind(null, ne, 'numberNe', (_, expected) => `Expected number to not be ${expected}`);

export const numberInt: IConstraint<number> = {
run(input: number) {
Expand Down
2 changes: 1 addition & 1 deletion src/constraints/base/IConstraint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import type { ConstraintError } from '../../lib/errors/ConstraintError';
import type { Result } from '../../lib/Result';

export interface IConstraint<Input, Return extends Input = Input> {
run(input: Input): Result<Return, ConstraintError<Return>>;
run(input: Input): Result<Return, ConstraintError<Input>>;
}
25 changes: 19 additions & 6 deletions src/constraints/util/operators.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,40 @@
export function lt(a: number, b: number): boolean {
export function lt(a: number, b: number): boolean;
export function lt(a: bigint, b: bigint): boolean;
export function lt(a: number | bigint, b: number | bigint): boolean {
return a < b;
}

export function le(a: number, b: number): boolean {
export function le(a: number, b: number): boolean;
export function le(a: bigint, b: bigint): boolean;
export function le(a: number | bigint, b: number | bigint): boolean {
return a <= b;
}

export function gt(a: number, b: number): boolean {
export function gt(a: number, b: number): boolean;
export function gt(a: bigint, b: bigint): boolean;
export function gt(a: number | bigint, b: number | bigint): boolean {
return a > b;
}

export function ge(a: number, b: number): boolean {
export function ge(a: number, b: number): boolean;
export function ge(a: bigint, b: bigint): boolean;
export function ge(a: number | bigint, b: number | bigint): boolean {
return a > b;
}

export function eq(a: number, b: number): boolean {
export function eq(a: number, b: number): boolean;
export function eq(a: bigint, b: bigint): boolean;
export function eq(a: number | bigint, b: number | bigint): boolean {
return a === b;
}

export function ne(a: number, b: number): boolean {
export function ne(a: number, b: number): boolean;
export function ne(a: bigint, b: bigint): boolean;
export function ne(a: number | bigint, b: number | bigint): boolean {
return a !== b;
}

export interface Comparator {
(a: number, b: number): boolean;
(a: bigint, b: bigint): boolean;
}
23 changes: 22 additions & 1 deletion src/lib/Shapes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { ArrayValidator } from '../validators/ArrayValidator';
import type { BaseValidator } from '../validators/BaseValidator';
import { BigIntValidator } from '../validators/BigIntValidator';
import { BooleanValidator } from '../validators/BooleanValidator';
import { DateValidator } from '../validators/DateValidator';
import { Constructor, InstanceValidator } from '../validators/InstanceValidator';
import { LiteralValidator } from '../validators/LiteralValidator';
import { NeverValidator } from '../validators/NeverValidator';
import { NullishValidator } from '../validators/NullishValidator';
import { NumberValidator } from '../validators/NumberValidator';
import { PassthroughValidator } from '../validators/PassthroughValidator';
Expand All @@ -18,6 +22,18 @@ export class Shapes {
return new NumberValidator();
}

public get bigint() {
return new BigIntValidator();
}

public get boolean() {
return new BooleanValidator();
}

public get date() {
return new DateValidator();
}

public get undefined() {
return this.literal(undefined);
}
Expand All @@ -38,11 +54,16 @@ export class Shapes {
return new PassthroughValidator<unknown>();
}

public get never() {
return new NeverValidator();
}

public enum<T>(...values: readonly T[]) {
return this.union(...values.map((value) => this.literal(value)));
}

public literal<T>(value: T): LiteralValidator<T> {
public literal<T>(value: T): BaseValidator<T> {
if (value instanceof Date) return this.date.eq(value) as unknown as BaseValidator<T>;
return new LiteralValidator(value);
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/errors/ConstraintError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ export class ConstraintError<T = unknown> extends Error {
}
}

export interface ConstraintErrorMessageBuilder<T = unknown> {
(given: T, expected: unknown): string;
export interface ConstraintErrorMessageBuilder<Given = unknown, Expected = unknown> {
(given: Given, expected: Expected): string;
}
11 changes: 6 additions & 5 deletions src/validators/BaseValidator.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import type { IConstraint } from '../constraints/base/IConstraint';
import type { ValidationError } from '../lib/errors/ValidationError';
import type { Result } from '../lib/Result';
import { ArrayValidator } from './ArrayValidator';
import { LiteralValidator } from './LiteralValidator';
import { NullishValidator } from './NullishValidator';
import { SetValidator } from './SetValidator';
import { UnionValidator } from './UnionValidator';

export abstract class BaseValidator<T> {
protected constraints: readonly IConstraint<T>[] = [];
Expand Down Expand Up @@ -66,3 +61,9 @@ export abstract class BaseValidator<T> {
return clone;
}
}

import { ArrayValidator } from './ArrayValidator';
import { LiteralValidator } from './LiteralValidator';
import { NullishValidator } from './NullishValidator';
import { SetValidator } from './SetValidator';
import { UnionValidator } from './UnionValidator';
Loading

0 comments on commit 16af17b

Please sign in to comment.