From 0330e84394522ccb60a195124949eb6b21898f6d Mon Sep 17 00:00:00 2001 From: Pavel Tereschenko Date: Wed, 2 Dec 2020 19:01:42 +0000 Subject: [PATCH] feat: add intersection contract --- __tests__/contracts/intersection.js | 53 ++++++++++++++++ src/contracts/index.js | 1 + src/contracts/intersection.js | 94 +++++++++++++++++++++++++++++ src/index.d.ts | 60 ++++++++++++++++++ 4 files changed, 208 insertions(+) create mode 100644 __tests__/contracts/intersection.js create mode 100644 src/contracts/intersection.js diff --git a/__tests__/contracts/intersection.js b/__tests__/contracts/intersection.js new file mode 100644 index 0000000..9214b2a --- /dev/null +++ b/__tests__/contracts/intersection.js @@ -0,0 +1,53 @@ +/* @flow */ + +import { intersection } from '../../src/contracts/intersection'; +import { ValidationError } from '../../src/ValidationError'; + +const createError = (...types: $ReadOnlyArray) => ( + name: string, + value: mixed, +) => new ValidationError(name, value, types); + +describe('intersection', () => { + describe('Creates new Contract for one or more other contracts or validation functions', () => { + it('Return ValidationError if at least one Contract returns ValidationError', () => { + const validate1 = jest.fn().mockReturnValue({ a: 1, b: 2 }); + const validate2 = jest.fn().mockReturnValue({ a: 1, b: 2 }); + const validate3 = jest.fn(createError('type3')); + + const result = intersection( + validate1, + validate2, + validate3, + )('valueName', { a: 1, b: 2 }); + + expect(result).toBeInstanceOf(ValidationError); + expect(result.expectedTypes).toEqual(['Intersection']); + expect(result.nested).toEqual([ + new ValidationError('valueName', { a: 1, b: 2 }, 'type3'), + ]); + + expect(validate1).lastCalledWith('valueName', { a: 1, b: 2 }); + expect(validate2).lastCalledWith('valueName', { a: 1, b: 2 }); + expect(validate3).lastCalledWith('valueName', { a: 1, b: 2 }); + }); + + it('Return value in other cases', () => { + const validate1 = jest.fn().mockReturnValue({ a: 1, b: 2 }); + const validate2 = jest.fn().mockReturnValue({ a: 1, b: 2 }); + const validate3 = jest.fn().mockReturnValue({ a: 1, b: 2 }); + + expect( + intersection( + validate1, + validate2, + validate3, + )('valueName', { a: 1, b: 2 }), + ).toEqual({ a: 1, b: 2 }); + + expect(validate1).lastCalledWith('valueName', { a: 1, b: 2 }); + expect(validate2).lastCalledWith('valueName', { a: 1, b: 2 }); + expect(validate3).lastCalledWith('valueName', { a: 1, b: 2 }); + }); + }); +}); diff --git a/src/contracts/index.js b/src/contracts/index.js index 7545ab1..9776075 100644 --- a/src/contracts/index.js +++ b/src/contracts/index.js @@ -2,6 +2,7 @@ export * from './array'; export * from './boolean'; +export * from './intersection'; export * from './literal'; export * from './null'; export * from './number'; diff --git a/src/contracts/intersection.js b/src/contracts/intersection.js new file mode 100644 index 0000000..c51bd4d --- /dev/null +++ b/src/contracts/intersection.js @@ -0,0 +1,94 @@ +/* @flow */ + +import { ValidationError } from '../ValidationError'; +import * as contract from '../Contract'; + +export class IntersectionError extends ValidationError { + constructor( + valueName: string, + value: mixed, + errors: $ReadOnlyArray, + ) { + super(valueName, value, 'Intersection', errors); + this.name = 'IntersectionError'; + } +} + +declare export function intersection( + a: (name: string, value: mixed) => ValidationError | A, +): contract.Contract; + +declare export function intersection( + a: (name: string, value: mixed) => ValidationError | A, + b: (name: string, value: mixed) => ValidationError | B, +): contract.Contract; + +declare export function intersection( + a: (name: string, value: mixed) => ValidationError | A, + b: (name: string, value: mixed) => ValidationError | B, + c: (name: string, value: mixed) => ValidationError | C, +): contract.Contract; + +declare export function intersection( + a: (name: string, value: mixed) => ValidationError | A, + b: (name: string, value: mixed) => ValidationError | B, + c: (name: string, value: mixed) => ValidationError | C, + d: (name: string, value: mixed) => ValidationError | D, +): contract.Contract; + +declare export function intersection( + a: (name: string, value: mixed) => ValidationError | A, + b: (name: string, value: mixed) => ValidationError | B, + c: (name: string, value: mixed) => ValidationError | C, + d: (name: string, value: mixed) => ValidationError | D, + e: (name: string, value: mixed) => ValidationError | E, +): contract.Contract; + +declare export function intersection( + a: (name: string, value: mixed) => ValidationError | A, + b: (name: string, value: mixed) => ValidationError | B, + c: (name: string, value: mixed) => ValidationError | C, + d: (name: string, value: mixed) => ValidationError | D, + e: (name: string, value: mixed) => ValidationError | E, + f: (name: string, value: mixed) => ValidationError | F, +): contract.Contract; + +declare export function intersection( + a: (name: string, value: mixed) => ValidationError | A, + b: (name: string, value: mixed) => ValidationError | B, + c: (name: string, value: mixed) => ValidationError | C, + d: (name: string, value: mixed) => ValidationError | D, + e: (name: string, value: mixed) => ValidationError | E, + f: (name: string, value: mixed) => ValidationError | F, + g: (name: string, value: mixed) => ValidationError | G, +): contract.Contract; + +declare export function intersection( + a: (name: string, value: mixed) => ValidationError | A, + b: (name: string, value: mixed) => ValidationError | B, + c: (name: string, value: mixed) => ValidationError | C, + d: (name: string, value: mixed) => ValidationError | D, + e: (name: string, value: mixed) => ValidationError | E, + f: (name: string, value: mixed) => ValidationError | F, + g: (name: string, value: mixed) => ValidationError | G, + h: (name: string, value: mixed) => ValidationError | H, +): contract.Contract; + +export function intersection( + ...rules: $ReadOnlyArray<(name: string, value: mixed) => ValidationError | T> +): contract.Contract { + const validators = rules; + + const intersectionContract = (name, value: any): ValidationError | T => { + const errors = validators + .map(validate => validate(name, value)) + .reduce((scope, error) => { + if (error instanceof ValidationError) scope.push(error); + return scope; + }, []); + + return errors.length ? new IntersectionError(name, value, errors) : value; + }; + + return contract.of(intersectionContract); +} diff --git a/src/index.d.ts b/src/index.d.ts index c4fe965..dcb6418 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -112,6 +112,66 @@ export declare var str: typeof string; export declare var isStr: typeof string; export declare var passStr: typeof string; +export declare function intersection( + a: Validator, +): Contract; + + export declare function intersection( + a: Validator, + b: Validator, +): Contract; + + export declare function intersection( + a: Validator, + b: Validator, + c: Validator, +): Contract; + + export declare function intersection( + a: Validator, + b: Validator, + c: Validator, + d: Validator, +): Contract; + + export declare function intersection( + a: Validator, + b: Validator, + c: Validator, + d: Validator, + e: Validator, +): Contract; + +export declare function intersection( + a: Validator, + b: Validator, + c: Validator, + d: Validator, + e: Validator, + f: Validator, +): Contract; + + export declare function intersection( + a: Validator, + b: Validator, + c: Validator, + d: Validator, + e: Validator, + f: Validator, + g: Validator, +): Contract; + + export declare function intersection( + a: Validator, + b: Validator, + c: Validator, + d: Validator, + e: Validator, + f: Validator, + g: Validator, + h: Validator, +): Contract; + export declare function union | string | number | boolean>>( ...rules: T ): Contract<