Skip to content

Commit

Permalink
feat: add disabling of validators (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
vladfrangu committed Jun 11, 2022
1 parent 007cc3d commit e17af95
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 1 deletion.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Blazing fast input validation and transformation ⚡
- [`.ignore`](#ignore)
- [`.passthrough`](#passthrough)
- [BaseValidator: methods and properties](#basevalidator-methods-and-properties)
- [Enabling and disabling validation](#enabling-and-disabling-validation)
- [Buy us some doughnuts](#buy-us-some-doughnuts)
- [Contributors ✨](#contributors-%E2%9C%A8)

Expand Down Expand Up @@ -740,6 +741,26 @@ s.object({ name: s.string }).or(s.string, s.number);
// => s.union(s.object({ name: s.string }), s.string, s.number)
```

### Enabling and disabling validation

[Back to top][toc]

At times, you might want to have a consistent code base with validation, but would like to keep validation to the strict necessities instead of the in-depth constraints available in shapeshift. By calling `setGlobalValidationEnabled` you can disable validation at a global level, and by calling `setValidationEnabled` you can disable validation on a per-validator level.

> When setting the validation enabled status per-validator, you can also set it to `null` to use the global setting.
```typescript
import { setGlobalValidationEnabled } from '@sapphire/shapeshift';

setGlobalValidationEnabled(false);
```

```typescript
import { s } from '@sapphire/shapeshift';

const predicate = s.string.lengthGreaterThan(5).setValidationEnabled(false);
```

## Buy us some doughnuts

[Back to top][toc]
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Shapes } from './lib/Shapes';

export const s = new Shapes();

export * from './lib/configs';
export * from './lib/errors/BaseError';
export * from './lib/errors/CombinedError';
export * from './lib/errors/CombinedPropertyError';
Expand Down
16 changes: 16 additions & 0 deletions src/lib/configs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
let validationEnabled = true;

/**
* Sets whether validators should run on the input, or if the input should be passed through.
* @param enabled Whether validation should be done on inputs
*/
export function setGlobalValidationEnabled(enabled: boolean) {
validationEnabled = enabled;
}

/**
* @returns Whether validation is enabled
*/
export function getGlobalValidationEnabled() {
return validationEnabled;
}
4 changes: 4 additions & 0 deletions src/validators/ArrayValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ export class ArrayValidator<T> extends BaseValidator<T[]> {
return Result.err(new ValidationError('s.array(T)', 'Expected an array', values));
}

if (!this.shouldRunConstraints) {
return Result.ok(values);
}

const errors: [number, BaseError][] = [];
const transformed: T[] = [];

Expand Down
30 changes: 29 additions & 1 deletion src/validators/BaseValidator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IConstraint } from '../constraints/base/IConstraint';
import { getGlobalValidationEnabled } from '../lib/configs';
import type { BaseError } from '../lib/errors/BaseError';
import type { CombinedError } from '../lib/errors/CombinedError';
import type { CombinedPropertyError } from '../lib/errors/CombinedPropertyError';
Expand All @@ -9,6 +10,7 @@ import { ArrayValidator, DefaultValidator, LiteralValidator, NullishValidator, S

export abstract class BaseValidator<T> {
protected constraints: readonly IConstraint<T>[] = [];
protected isValidationEnabled: boolean | null = null;

public constructor(constraints: readonly IConstraint<T>[] = []) {
this.constraints = constraints;
Expand Down Expand Up @@ -61,11 +63,37 @@ export abstract class BaseValidator<T> {
}

public parse(value: unknown): T {
// If validation is disabled (at the validator or global level), we only run the `handle` method, which will do some basic checks
// (like that the input is a string for a string validator)
if (!this.shouldRunConstraints) {
return this.handle(value).unwrap();
}

return this.constraints.reduce((v, constraint) => constraint.run(v).unwrap(), this.handle(value).unwrap());
}

/**
* Sets if the validator should also run constraints or just do basic checks.
* @param isValidationEnabled Whether this validator should be enabled or disabled. Set to `null` to go off of the global configuration.
*/
public setValidationEnabled(isValidationEnabled: boolean | null): this {
const clone = this.clone();
clone.isValidationEnabled = isValidationEnabled;
return clone;
}

public getValidationEnabled() {
return this.isValidationEnabled;
}

protected get shouldRunConstraints(): boolean {
return this.isValidationEnabled ?? getGlobalValidationEnabled();
}

protected clone(): this {
return Reflect.construct(this.constructor, [this.constraints]);
const clone: this = Reflect.construct(this.constructor, [this.constraints]);
clone.isValidationEnabled = this.isValidationEnabled;
return clone;
}

protected abstract handle(value: unknown): Result<T, ValidatorError>;
Expand Down
4 changes: 4 additions & 0 deletions src/validators/MapValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export class MapValidator<K, V> extends BaseValidator<Map<K, V>> {
return Result.err(new ValidationError('s.map(K, V)', 'Expected a map', value));
}

if (!this.shouldRunConstraints) {
return Result.ok(value);
}

const errors: [string, BaseError][] = [];
const transformed = new Map<K, V>();

Expand Down
4 changes: 4 additions & 0 deletions src/validators/ObjectValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ export class ObjectValidator<T extends NonNullObject> extends BaseValidator<T> {
return Result.err(new ValidationError('s.object(T)', 'Expected the value to not be an array', value));
}

if (!this.shouldRunConstraints) {
return Result.ok(value as T);
}

return this.handleStrategy(value as NonNullObject);
}

Expand Down
8 changes: 8 additions & 0 deletions src/validators/RecordValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ export class RecordValidator<T> extends BaseValidator<Record<string, T>> {
return Result.err(new ValidationError('s.record(T)', 'Expected the value to not be null', value));
}

if (Array.isArray(value)) {
return Result.err(new ValidationError('s.record(T)', 'Expected the value to not be an array', value));
}

if (!this.shouldRunConstraints) {
return Result.ok(value as Record<string, T>);
}

const errors: [string, BaseError][] = [];
const transformed: Record<string, T> = {};

Expand Down
4 changes: 4 additions & 0 deletions src/validators/SetValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export class SetValidator<T> extends BaseValidator<Set<T>> {
return Result.err(new ValidationError('s.set(T)', 'Expected a set', values));
}

if (!this.shouldRunConstraints) {
return Result.ok(values);
}

const errors: BaseError[] = [];
const transformed = new Set<T>();

Expand Down
4 changes: 4 additions & 0 deletions src/validators/TupleValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export class TupleValidator<T extends any[]> extends BaseValidator<[...T]> {
return Result.err(new ValidationError('s.tuple(T)', `Expected an array of length ${this.validators.length}`, values));
}

if (!this.shouldRunConstraints) {
return Result.ok(values as [...T]);
}

const errors: [number, BaseError][] = [];
const transformed: T = [] as unknown as T;

Expand Down
87 changes: 87 additions & 0 deletions tests/lib/configs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { s, setGlobalValidationEnabled } from '../../src';

describe('Validation enabled and disabled configurations', () => {
const stringPredicate = s.string.lengthGreaterThan(5);
const arrayPredicate = s.array(s.string).lengthGreaterThan(2);
const mapPredicate = s.map(s.string, s.number);
const objectPredicate = s.object({
owo: s.boolean
});
const recordPredicate = s.record(s.number);
const setPredicate = s.set(s.number);
const tuplePredicate = s.tuple([s.string, s.number]);

describe('Global configurations', () => {
beforeAll(() => {
setGlobalValidationEnabled(false);
});

afterAll(() => {
setGlobalValidationEnabled(true);
});

test.each([
//
['string', stringPredicate, ''],
['array', arrayPredicate, []],
['map', mapPredicate, new Map([[0, '']])],
['object', objectPredicate, { owo: 'string' }],
['record', recordPredicate, { one: 'one' }],
['set', setPredicate, new Set(['1'])],
['tuple', tuplePredicate, [0, 'zero']]
])('GIVEN globally disabled %s predicate THEN returns the input', (_, inputPredicate, input) => {
expect(inputPredicate.parse(input)).toStrictEqual(input);
});
});

describe('Validator level configurations', () => {
test.each([
//
['string', stringPredicate, ''],
['array', arrayPredicate, []],
['map', mapPredicate, new Map([[0, '']])],
['object', objectPredicate, { owo: 'string' }],
['record', recordPredicate, { one: 'one' }],
['set', setPredicate, new Set(['1'])],
['tuple', tuplePredicate, [0, 'zero']]
])('GIVEN disabled %s predicate THEN returns the input', (_, inputPredicate, input) => {
const predicate = inputPredicate.setValidationEnabled(false);

expect(predicate.parse(input)).toStrictEqual(input);
});

test("GIVEN disabled predicate THEN checking if it's disabled should return true", () => {
const predicate = s.string.setValidationEnabled(false);

expect(predicate.getValidationEnabled()).toBe(false);
});
});

describe('Globally disabled but locally enabled', () => {
beforeAll(() => {
setGlobalValidationEnabled(false);
});

afterAll(() => {
setGlobalValidationEnabled(true);
});

test.each([
//
['string', stringPredicate, ''],
['array', arrayPredicate, []],
['map', mapPredicate, new Map([[0, '']])],
['object', objectPredicate, { owo: 'string' }],
['record', recordPredicate, { one: 'one' }],
['set', setPredicate, new Set(['1'])],
['tuple', tuplePredicate, [0, 'zero']]
])(
'GIVEN enabled %s predicate while the global option is set to false THEN it should throw validation errors',
(_, inputPredicate, input) => {
const predicate = inputPredicate.setValidationEnabled(true);

expect(() => predicate.parse(input)).toThrowError();
}
);
});
});

0 comments on commit e17af95

Please sign in to comment.