Skip to content

Instantly share code, notes, and snippets.

@codyromano
Created May 12, 2024 00:00
Show Gist options
  • Save codyromano/2ba95e4eee1516e617ec211a14295764 to your computer and use it in GitHub Desktop.
Save codyromano/2ba95e4eee1516e617ec211a14295764 to your computer and use it in GitHub Desktop.
Simple runtime validation for data types in complex web apps (e.g. games)
// Let's say we have coordinates in a game. Managing the location of objects can be tricky,
// and we want to validate that nothing flows off the screen
export type PositionDataType = {
x: number;
y: number;
};
// Here's a utility for creating a position data type with runtime validation
export const positionDataType = createDataType<PositionDataType>('position')
.defaultData(() => ({x: 0, y: 0}))
.rule('coords non-negative', ({x, y}) => x >= 0 && y >= 0)
.rule('coords less than screen width', ({x, y}) => {
const {availHeight, availWidth} = window.screen;
return x <= availWidth && y <= availHeight;
});
// And to create an instance of coordinates...
const position = positionDataType.instance();
// Below is how we implement it...
function createDataType<T>(dataTypeName: string): DataTypeFactory<T> {
return new DataTypeFactory<T>(dataTypeName);
}
// Data type - represents an instance of the type
type DataTypeParams<T> = {
data: T;
rules: Record<string, (data: T) => boolean>,
dataTypeName: string,
instanceName?: string,
};
export default class DataType<T> {
private params: DataTypeParams<T>;
constructor(params: DataTypeParams<T>) {
this.params = params;
}
private validate() {
const {params: { dataTypeName, instanceName, rules, data }} = this;
for (const ruleName in rules) {
if (rules[ruleName](data) === false) {
throw new Error(`Validation failed for dataType "${dataTypeName}" ` +
`(instance "${instanceName ?? 'Unspecified'}") on rule "${ruleName}". ` +
`Data state at failure: ${JSON.stringify(data)}`
)
}
}
}
get(): Readonly<T> {
return this.params.data;
}
set(newData: T): DataType<T> {
this.params.data = newData;
window.requestIdleCallback(() => this.validate());
return this;
}
update(updater: (currentData: T) => T): DataType<T> {
const newData = updater(this.params.data);
window.requestIdleCallback(() => this.validate());
return this;
}
}
export default class DataTypeFactory<T> {
private createDefaultData?: () => T;
private rules: Record<string, (data: T) => boolean> = {};
private dataTypeName: string;
constructor(dataTypeName: string) {
this.dataTypeName = dataTypeName;
}
defaultData(setter: () => T): DataTypeFactory<T> {
this.createDefaultData = setter;
return this;
}
rule(name: string, predicate: (data: T) => boolean): DataTypeFactory<T> {
this.rules[name] = predicate;
return this;
}
instance(instanceName?: string): DataType<T> {
if (!this.createDefaultData) {
throw new Error('Use .defaultData to specify initial data');
}
const { rules, dataTypeName } = this;
return new DataType<T>({
data: this.createDefaultData(),
dataTypeName,
instanceName,
rules
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment