Skip to content

Instantly share code, notes, and snippets.

@beaucollins
Last active March 17, 2020 07:44
Show Gist options
  • Select an option

  • Save beaucollins/28ab7ab4c58acea3932be7d9c5fb9340 to your computer and use it in GitHub Desktop.

Select an option

Save beaucollins/28ab7ab4c58acea3932be7d9c5fb9340 to your computer and use it in GitHub Desktop.
/**
* 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}`,
}
}
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