Created
May 20, 2019 16:57
-
-
Save ccorcos/88848fb34ad9da0d72199af0a9299142 to your computer and use it in GitHub Desktop.
This file contains 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 * as _ from "lodash" | |
/** This is how DataTypes are serialized. */ | |
export type DataType = | |
| { type: "string" } | |
| { type: "number" } | |
| { type: "boolean" } | |
| { type: "literal"; value: string | number | boolean } | |
| { type: "array"; inner: DataType } | |
// Tuple types are not quite working yet. | |
// | { type: "tuple"; values: Array<DataType> } | |
| { type: "map"; inner: DataType } | |
| { type: "object"; properties: { [key: string]: DataType } } | |
| { type: "any" } | |
| { type: "optional"; inner: DataType } | |
| { type: "or"; values: Array<DataType> } | |
type DataTypeMap = { [K in DataType["type"]]: Extract<DataType, { type: K }> } | |
type IsDataTypeMap = { | |
[K in keyof DataTypeMap]: ( | |
dataType: DataTypeMap[K], | |
value: unknown | |
) => boolean | |
} | |
function isPlainObject(value: unknown): value is {} { | |
return _.isPlainObject(value) | |
} | |
/** A map of DataType.type to validator functions. */ | |
const isDataTypeMap: IsDataTypeMap = { | |
string: (dataType, value) => _.isString(value), | |
number: (dataType, value) => _.isNumber(value), | |
boolean: (dataType, value) => _.isBoolean(value), | |
literal: (dataType, value) => _.isEqual(value, dataType.value), | |
array: (dataType, value) => | |
Array.isArray(value) && | |
value.every(innerValue => { | |
return isDataType(dataType.inner, innerValue) | |
}), | |
// Tuple types are not quite working yet. | |
// tuple: (dataType, value) => | |
// Array.isArray(value) && | |
// value.length === dataType.values.length && | |
// value.every((innerValue, index) => { | |
// return isDataType(dataType.values[index], innerValue) | |
// }), | |
map: (dataType, value) => | |
isPlainObject(value) && | |
Object.keys(value).every(_.isString) && | |
Object.values(value).every(innerValue => { | |
return isDataType(dataType.inner, innerValue) | |
}), | |
object: (dataType, value) => | |
isPlainObject(value) && | |
Object.keys(value).every(key => { | |
return isDataType(dataType.properties[key], value[key]) | |
}), | |
any: (dataType, value) => true, | |
or: (dataType, value) => | |
dataType.values.some(possibleDataType => { | |
return isDataType(possibleDataType, value) | |
}), | |
optional: (dataType, value) => | |
value === undefined || isDataType(dataType.inner, value), | |
} | |
/** Runtime validation for DataTypes. */ | |
export function isDataType<T extends DataType>(dataType: T, value: unknown) { | |
const is = isDataTypeMap[dataType.type] as ( | |
schema: DataType, | |
value: unknown | |
) => boolean | |
return is(dataType, value) | |
} | |
/** | |
* A runtime representation of a DataType that is serializable with runtime validation | |
* as well as TypeScript types available with `typeof DataType.value`. | |
*/ | |
export class RuntimeDataType<T> { | |
value: T | |
dataType: DataType | |
constructor(dataType: DataType) { | |
this.dataType = dataType | |
} | |
/** Convenient wrapper for `isDataType`. */ | |
is(value: unknown): value is T { | |
return isDataType(this.dataType, value) | |
} | |
toJSON() { | |
return this.dataType | |
} | |
} | |
// Runtime representations of each DataType. | |
export const string = new RuntimeDataType<string>({ type: "string" }) | |
export const number = new RuntimeDataType<number>({ type: "number" }) | |
export const boolean = new RuntimeDataType<boolean>({ type: "boolean" }) | |
export function literal<T extends string | number>(x: T) { | |
return new RuntimeDataType<T>({ type: "literal", value: x }) | |
} | |
export function optional<T>(inner: RuntimeDataType<T>) { | |
return new RuntimeDataType<T | undefined>({ | |
type: "optional", | |
inner: inner.dataType, | |
}) | |
} | |
export function array<T>(inner: RuntimeDataType<T>) { | |
return new RuntimeDataType<Array<T>>({ | |
type: "array", | |
inner: inner.dataType, | |
}) | |
} | |
// Tuple types are not quite working yet. I'm not sure how to make the generic | |
// a tuple of unwrapped values and then specify the argument as a tuple of | |
// wrapped values. | |
// export function tuple<T extends Array<RuntimeDataType<any>>>(...values: T) { | |
// return new RuntimeDataType<T>({ | |
// type: "tuple", | |
// values: values.map(value => value.dataType), | |
// }) | |
// } | |
export function map<T>(inner: RuntimeDataType<T>) { | |
return new RuntimeDataType<{ [key: string]: T }>({ | |
type: "map", | |
inner: inner.dataType, | |
}) | |
} | |
export function object<O extends { [key: string]: any }>( | |
schema: { [K in keyof O]: RuntimeDataType<O[K]> } | |
) { | |
const properties: { [key: string]: DataType } = {} | |
Object.keys(schema).forEach(key => { | |
properties[key] = schema[key].dataType | |
}) | |
return new RuntimeDataType<O>({ | |
type: "object", | |
properties: properties, | |
}) | |
} | |
export const any = new RuntimeDataType<any>({ type: "any" }) | |
export function or<T extends Array<RuntimeDataType<any>>>(...values: T) { | |
return new RuntimeDataType<T[number]["value"]>({ | |
type: "or", | |
values: values.map(value => value.dataType), | |
}) | |
} | |
// RuntimeDataTypes for DataTypes. Very Meta 🤯 | |
// We're going to mutate this array to avoid circular references. | |
const dataTypeDataTypeValues: Array<any> = [] | |
export const dataTypeDataType = new RuntimeDataType<DataType>({ | |
type: "or", | |
values: dataTypeDataTypeValues, | |
}) | |
const stringDataType = object({ type: literal("string") }) | |
const numberDataType = object({ type: literal("number") }) | |
const booleanDataType = object({ type: literal("boolean") }) | |
const literalDataType = object({ | |
type: literal("literal"), | |
value: or(string, number, boolean), | |
}) | |
const arrayDataType = object({ | |
type: literal("array"), | |
inner: dataTypeDataType, | |
}) | |
// Tuple types are not quite working yet. | |
// const tupleDataType = object({ | |
// type: literal("tuple"), | |
// values: array(dataTypeDataType), | |
// }) | |
const mapDataType = object({ | |
type: literal("map"), | |
inner: dataTypeDataType, | |
}) | |
const objectDataType = object({ | |
type: literal("object"), | |
properties: map(dataTypeDataType), | |
}) | |
const anyDataType = object({ type: literal("any") }) | |
const orDataType = object({ | |
type: literal("or"), | |
values: array(dataTypeDataType), | |
}) | |
const optionalDataType = object({ | |
type: literal("optional"), | |
inner: dataTypeDataType, | |
}) | |
// Specify all runtime type parameters | |
const runtimeDataTypeMap: { | |
[K in keyof DataTypeMap]: RuntimeDataType<DataTypeMap[K]> | |
} = { | |
string: stringDataType, | |
number: numberDataType, | |
boolean: booleanDataType, | |
literal: literalDataType, | |
array: arrayDataType, | |
// Tuple types are not quite working yet. | |
// tuple: tupleDataType, | |
map: mapDataType, | |
object: objectDataType, | |
any: anyDataType, | |
or: orDataType, | |
optional: optionalDataType, | |
} | |
// Type contrained! :) | |
dataTypeDataTypeValues.push(...Object.values(runtimeDataTypeMap)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment