Last active
March 17, 2020 07:44
-
-
Save beaucollins/28ab7ab4c58acea3932be7d9c5fb9340 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Take an `any` and turn it into something else. | |
| */ | |
| export type Failure = Readonly<{type: 'failure', reason: string, value: any}>; | |
| export type Success<T> = Readonly<{type: 'success', value: T}> | |
| export type Result<T> = Success<T> | Failure; | |
| export type Validator<T> = (value: any) => Result<T>; | |
| export function isString(value: any): Result<string> { | |
| return typeof value === 'string' ? success(value) : failTypeOf(value); | |
| } | |
| export function isExactly<S extends (string|number|boolean|undefined|null)>(exactly: S): Validator<S> { | |
| return (value) => value === exactly ? success(exactly) : failure(value, `is not ${exactly}`); | |
| } | |
| export function isNumber(value: any): Result<number> { | |
| return typeof(value) === 'number' ? success(value) : failTypeOf(value); | |
| } | |
| export function isUndefined(value: any): Result<undefined> { | |
| return value === undefined ? success(value) : failTypeOf(value); | |
| } | |
| export function isBoolean(value: any): Result<boolean> { | |
| return typeof(value) === 'boolean' ? success(value) : failTypeOf(value); | |
| } | |
| export function isObject(value: any): Result<Object> { | |
| return typeof(value) === 'object' ? success(value) : failTypeOf(value); | |
| } | |
| export function isArray(value: any): Result<any[]> { | |
| return Array.isArray(value) ? success(value) : failure(value, 'value is not Array.isArray'); | |
| } | |
| export function isAnyValue(value: any): Result<any> { | |
| return success(value); | |
| } | |
| export function objectOf<T extends {}>(validators: {[K in keyof T]: Validator<T[K]>}): Validator<T> { | |
| return function(value) { | |
| let result = {} as T; | |
| for (const key in validators) { | |
| const validated = validators[key](value ? value[key] : undefined); | |
| if (validated.type === 'failure') { | |
| return keyedFailure(value, key, validated); | |
| } | |
| result[key] = validated.value | |
| } | |
| return success(result); | |
| }; | |
| } | |
| export function optional<T>(validator: Validator<T>): Validator<null | T> { | |
| return (value) => value === null ? success(null) : validator(value); | |
| } | |
| export function voidable<T>(validator: Validator<T>): Validator<undefined | T> { | |
| return (value) => value === undefined ? success(undefined) : validator(value); | |
| } | |
| export function oneOf<A, B>(a: Validator<A>, b: Validator<B>): Validator<(A|B)> { | |
| return (value) => mapFailure(a(value), () => b(value)); | |
| } | |
| function mapValidator<A, B>(validator: Validator<A>, next: (value: A) => Result<B>): Validator<B> { | |
| return (value) => mapSuccess(validator(value), next); | |
| } | |
| export function mapFailure<A, B>(result: Result<A>, next: (failure: Failure) => B): (B | Success<A>) { | |
| return result.type === 'success' ? result : next(result); | |
| } | |
| export function mapSuccess<A, B>(result: Result<A>, next: (value: A) => B): (B | Failure) { | |
| return result.type === 'success' ? next(result.value) : result; | |
| } | |
| export function arrayOf<T>(validator: Validator<T>): Validator<T[]> { | |
| return mapValidator(isArray, (value) => | |
| value.reduce<Result<T[]>>( | |
| (result, member, index) => mapSuccess( | |
| result, | |
| items => mapFailure( | |
| mapSuccess( | |
| validator(member), | |
| valid => success(items.concat([valid])) | |
| ), | |
| failure => keyedFailure(value, index, failure) | |
| ) | |
| ), | |
| success([]) | |
| ) | |
| ) | |
| } | |
| export function success<T>(value: T): Success<T> { | |
| return { | |
| value, | |
| type: 'success' | |
| } | |
| } | |
| export function failure(value: any, reason: string): Failure { | |
| return { | |
| type: 'failure', | |
| value, | |
| reason, | |
| } | |
| } | |
| function failTypeOf(value: any): Failure { | |
| return failure(value, 'typeof value is ' + (typeof value)); | |
| } | |
| function keyedFailure(value: any, key: string | number, failure: Failure): Failure { | |
| return { | |
| ...failure, | |
| value, | |
| reason: `Failed at '${key}': ${failure.reason}`, | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| Validator, | |
| Result, | |
| isBoolean, | |
| isNumber, | |
| isString, | |
| isUndefined, | |
| failure, | |
| success, | |
| arrayOf, | |
| objectOf, | |
| optional, | |
| oneOf, | |
| isExactly, | |
| mapSuccess | |
| } from '../../src/fractal/validate'; | |
| describe('fractal/validator', () => { | |
| describe('isString', () => { | |
| it('succeeds', () => { | |
| const validator: Validator<string> = isString; | |
| const value: Result<string> = validator( 'yes' ); | |
| expect(value).toEqual(success('yes')); | |
| } ) | |
| it('fails', () => { | |
| const validator: Validator<string> = isString; | |
| const value = validator( 1 ); | |
| expect(value).toEqual(failure(1, 'typeof value is number')); | |
| }); | |
| }); | |
| describe('validateArrayOf', () => { | |
| it('succeeds', () => { | |
| const value = arrayOf(isNumber)([1, 2, 3]); | |
| expect(value).toEqual(success([1, 2, 3])); | |
| }); | |
| it('fails', () => { | |
| const value = arrayOf(isNumber)([1, '2', 3]); | |
| expect(value).toEqual(failure([1, '2', 3], 'Failed at \'1\': typeof value is string')); | |
| }); | |
| }) | |
| describe('optional', () => { | |
| it('succeeds', () => { | |
| const validator = arrayOf(optional(isNumber)); | |
| const result = validator([1, null, 3]); | |
| expect(result).toEqual(success([1, null, 3])); | |
| }); | |
| it('fails', () => { | |
| const validator = arrayOf(optional(isNumber)); | |
| const result = validator([1, false, 3]); | |
| expect(result).toEqual(failure([1, false, 3], 'Failed at \'1\': typeof value is boolean')); | |
| }); | |
| }); | |
| describe('isExactly', () => { | |
| const validate = isExactly('hello'); | |
| it('succeeds', () => { | |
| mapSuccess(validate(1), (value) => { | |
| }) | |
| expect(validate('hello')).toEqual(success('hello')); | |
| }); | |
| it('fails', () => { | |
| expect(validate('bye')).toEqual(failure('bye', 'is not hello')) | |
| }); | |
| }); | |
| describe('object keys', () => { | |
| it('succeeds', () => { | |
| type Record = { | |
| artist: string, | |
| yearReleased: number, | |
| name: string, | |
| }; | |
| const validator = objectOf<Record>({ | |
| artist: isString, | |
| yearReleased: isNumber, | |
| name: isString, | |
| }); | |
| const record = { | |
| artist: 'The Beatles', | |
| name: 'Revolver', | |
| yearReleased: 1966, | |
| }; | |
| const result = validator(record); | |
| expect(result).toEqual(success(record)); | |
| }) | |
| it('fails', () => { | |
| type Thing = {name: string}; | |
| const validator = objectOf<Thing>({'name': isString}); | |
| const result = validator({'some-key': 1}); | |
| expect(result).toEqual(failure({'some-key': 1}, 'Failed at \'name\': typeof value is undefined')); | |
| }); | |
| it('allows undefined keys', () => { | |
| const validator = objectOf({ | |
| name: isString, | |
| age: oneOf(isUndefined, isString) | |
| }); | |
| expect(validator({})).toEqual(failure({}, 'Failed at \'name\': typeof value is undefined')); | |
| expect(validator({name: 'Hello', age: 10})).toEqual(failure({name: 'Hello', age: 10}, 'Failed at \'age\': typeof value is number')) | |
| expect(validator({name: 'Hello'})).toEqual(success({name: 'Hello'})) | |
| }) | |
| const validate = objectOf({ | |
| name: isString, | |
| child: objectOf({ | |
| id: isNumber | |
| }), | |
| }); | |
| const valid = { | |
| name: 'Valid', | |
| child: { id: 1 }, | |
| }; | |
| const invalid = { | |
| name: 'Invalid', | |
| child: { id: 'not-number' }, | |
| }; | |
| expect(validate(valid)).toEqual(success(valid)); | |
| expect(validate(invalid)).toEqual(failure(invalid, 'Failed at \'child\': Failed at \'id\': typeof value is string' )); | |
| }) | |
| describe('oneOf', () => { | |
| it('validates multiple validators', () => { | |
| const year = oneOf(oneOf(isNumber, isString), isBoolean); | |
| expect(year(2015)).toEqual(success(2015)); | |
| expect(year('2015')).toEqual(success('2015')); | |
| expect(year(true)).toEqual(success(true)); | |
| expect(year(null)).toEqual(failure(null, 'typeof value is object')); | |
| }) | |
| }); | |
| describe('arrayOf oneOf', () => { | |
| const validator = arrayOf(oneOf(isNumber, isBoolean)); | |
| expect(validator([])).toEqual(success([])); | |
| expect(validator([1, 2, true, false])).toEqual(success([1, 2, true, false])); | |
| expect(validator([null, true])).toEqual(failure([null, true], 'Failed at \'0\': typeof value is object')); | |
| }); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment