Skip to content

Instantly share code, notes, and snippets.

@mikeplus64
Created July 21, 2017 04:09
Show Gist options
  • Save mikeplus64/465c0ee54b7f9a5b752183439defc7b1 to your computer and use it in GitHub Desktop.
Save mikeplus64/465c0ee54b7f9a5b752183439defc7b1 to your computer and use it in GitHub Desktop.
check if arbitrary objects match some schema, allowing them to be flowtyped; better than ((thing: any): Something)
/* @flow */
export type ParseError = {
expected: string,
got: mixed,
at?: mixed,
children?: ParseError[],
};
export type Result<T> =
{ type: 'OK', value: T } |
{ type: 'ERROR', error: ParseError }
export type Validator<I, T> = (value: I) => Result<T>;
// throws an exception if parse failed
export type Check<I, T> = (input: I) => T;
export default function check<I, T>(validator: Validator<I, T>): Check<I, T> {
return (input) => {
const r = validator(input);
if (r.type === 'ERROR') {
throw new Error(JSON.stringify(r.error, null, 2));
}
return r.value;
};
}
export const number: Validator<mixed, number> =
(value: mixed) => {
if (typeof value === 'number') {
return { type: 'OK', value };
}
return {
type: 'ERROR',
error: { expected: 'number', got: value },
};
};
export const string: Validator<mixed, string> =
(value: mixed) => {
if (typeof value === 'string') {
return { type: 'OK', value };
}
return {
type: 'ERROR',
error: { expected: 'string', got: value },
};
};
export const boolean: Validator<mixed, boolean> =
(value: mixed) => {
if (typeof value === 'boolean') {
return { type: 'OK', value };
}
return {
type: 'ERROR',
error: { expected: 'boolean', got: value },
};
};
export function nullable<T>(elem: Validator<mixed, T>): Validator<mixed, ?T> {
return (value: mixed) => {
if (value == null) {
return { type: 'OK', value };
}
return (elem(value): any);
};
}
export function shape<S: { [key: string]: <T>(value: mixed) => Result<T> }>(
objShape: S,
): Validator<mixed, $ObjMap<S, <Q>(() => Result<Q>) => Q>> {
const shapeKeys = Object.keys(objShape);
return (value: mixed) => {
if (typeof value !== 'object' || value == null) {
return {
type: 'ERROR',
error: {
expected: 'object',
got: value,
},
};
}
for (let i = 0; i < shapeKeys.length; i += 1) {
const key: string = shapeKeys[i];
const shapeHere: <T>(value: mixed) => Result<T> = objShape[key];
const elemHere: mixed = value[key];
const result: Result<mixed> = shapeHere(elemHere);
if (result.type === 'ERROR') {
return {
type: 'ERROR',
error: {
expected: 'shape',
got: value,
children: [({ at: shapeKeys[i], ...(result: any) }: any)],
},
};
}
}
return { type: 'OK', value: (value: any) };
};
}
export function object<T>(
elem: Validator<mixed, T>,
): Validator<{ [key: string]: mixed }, T> {
return (value: { [key: string]: mixed }) => {
const keys: string[] = Object.keys(value);
const errors: ParseError[] = [];
let hasParseError: boolean = false;
for (let i = 0; i < keys.length; i += 1) {
const key: string = keys[i];
const here: mixed = value[key];
const test: Result<T> = elem(here);
if (test.type === 'ERROR') {
hasParseError = true;
errors.push(test.error);
}
}
if (hasParseError) {
return {
type: 'ERROR',
error: {
expected: 'object',
got: ((value: any): mixed),
children: errors,
},
};
}
return { type: 'OK', value: (value: any) };
};
}
export function array<T>(elem: Validator<mixed, T>): Validator<mixed[], T> {
return (value: mixed[]) => {
if (!Array.isArray(value)) {
return {
type: 'ERROR',
error: { expected: 'array', got: value },
};
}
for (let i = 0; i < value.length; i += 1) {
const here: mixed = value[i];
const test: Result<T> = elem(here);
if (test.type === 'ERROR') {
return {
type: 'ERROR',
error: {
expected: 'array',
got: value,
children: [{ ...test.error, at: i }],
},
};
}
}
return { type: 'OK', value: (value: any) };
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment