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: add disabling of validators #125

Merged
merged 6 commits into from
Jun 11, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
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;
}
vladfrangu marked this conversation as resolved.
Show resolved Hide resolved

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();
}
);
});
});