Skip to content

Instantly share code, notes, and snippets.

@clinuxrulz
Last active August 12, 2023 01:14
Show Gist options
  • Save clinuxrulz/73c28cb7875e6e9206f6a964767b35cf to your computer and use it in GitHub Desktop.
Save clinuxrulz/73c28cb7875e6e9206f6a964767b35cf to your computer and use it in GitHub Desktop.
Type checked JSON parsing
import { Vec2 } from "../math/Vec2";
import { Result, err, ok } from "./Result";
export type PropertiesSchema<A> = PropertiesSchemaInvMap<unknown,A> | PropertiesSchema2<A>;
type PropertiesSchemaInvMap<A,B> = {
type: "InvMap",
propertiesSchema: PropertiesSchema<A>,
fn1: (a: A) => B,
fn2: (b: B) => A,
};
type PropertiesSchema2<A> = {
[key in keyof A]: PropertySchema<A[key]>
};
type PropertySchema<A> = {
displayName: string,
typeSchema: TypeSchema<A>,
optional?: boolean,
defaultValue: A,
};
type Extract<Type, Union> = Type extends Union ? Type : never;
export type TypeSchema<A> =
TypeSchemaInvariantMap<unknown, A> | (
A extends boolean ? { type: "Boolean", } :
A extends number ? { type: "Number", } :
A extends string ? { type: "String", } :
A extends any[] ? { type: "Array", elemTypeSchema: TypeSchema<A[keyof A]>, } :
A extends { type: string, value: unknown } ? { type: "Union", parts: { [key in Extract<A,{ type: string, }>["type"]]: TypeSchema<Extract<A, { type: key, value: unknown, }>["value"]> } } :
A extends any ? {
type: "Object",
properties: {
[key in keyof A]: {
displayName: string,
typeSchema: TypeSchema<A[key]>,
optional?: boolean,
defaultValue: A[key],
}
},
} :
never
);
type TypeSchemaInvariantMap<A,B> = {
type: "InvMap",
typeSchema: TypeSchema<A>,
fn1: (a: A) => B,
fn2: (b: B) => A,
};
export function invMapPropertiesSchema<A,B>(fn1: (a: A) => B, fn2: (b: B) => A, propertiesSchema: PropertiesSchema<A>): PropertiesSchema<B> {
return ({
type: "InvMap",
propertiesSchema,
fn1,
fn2,
} as PropertiesSchema<B>);
}
export function invMapTypeSchema<A,B>(fn1: (a: A) => B, fn2: (b: B) => A, typeSchema: TypeSchema<A>): TypeSchema<B> {
return ({
type: "InvMap",
typeSchema,
fn1,
fn2,
} as TypeSchema<B>);
}
export function parseJsonViaPropertiesSchema<A>(propertiesSchema: PropertiesSchema<A>, obj: any): Result<A> {
if (typeof obj != "object") {
return err("Expected JSON Object.");
}
if ((propertiesSchema as any).type == "InvMap" && (typeof ((propertiesSchema as any).fn1)) == "function") {
let propertiesSchema2 = propertiesSchema as PropertiesSchemaInvMap<unknown,A>;
let value = parseJsonViaPropertiesSchema(propertiesSchema2.propertiesSchema, obj);
if (value.type == "Err") {
return err("Failed to parse through properties invariant map. " + value.message);
}
return ok(propertiesSchema2.fn1(value.value));
}
let propertiesSchema2 = propertiesSchema as PropertiesSchema2<A>;
let result: { [key in keyof A]?: A[key] } = {};
for (let key in propertiesSchema2) {
let propertySchema = propertiesSchema2[key];
let optional = propertySchema.optional ?? false;
let value: A[typeof key];
if (!obj.hasOwnProperty(key)) {
if (optional) {
value = propertySchema.defaultValue;
} else {
return err("Missing key `" + key + "` on JSON Object.");
}
} else {
let fieldValue = parseJsonViaTypeSchema(propertySchema.typeSchema, obj[key]);
if (fieldValue.type == "Err") {
return err("Failed to parse JSON Object at key `" + key + "`. " + fieldValue.message);
}
value = fieldValue.value;
}
result[key] = value;
}
return ok(result as A);
}
export function parseJsonViaTypeSchema<A>(typeSchema: TypeSchema<A>, obj: any): Result<A> {
switch (typeSchema.type) {
case "Boolean": {
if (typeof obj != "boolean") {
return err("Expected Boolean.");
}
return ok(obj as A);
}
case "Number": {
if (typeof obj != "number") {
return err("Expected Number.");
}
return ok(obj as A);
}
case "String": {
if (typeof obj != "string") {
return err("Expected String.");
}
return ok(obj as A);
}
case "Array": {
if (typeof obj != "object") {
return err("Expected JSON Array.");
}
if (!obj.hasOwnProperty("length")) {
return err("Expected JSON Array.");
}
let length = obj["length"];
if (typeof length != "number") {
return err("Expected JSON Array.");
}
let result: any[] = [];
for (let i = 0; i < length; ++i) {
let elem = parseJsonViaTypeSchema(typeSchema.elemTypeSchema, obj[i]);
if (elem.type == "Err") {
return err("Failed to parse JSON Array element at " + i + ". " + elem.message);
}
result.push(elem.value);
}
return ok(result as A);
}
case "Union":
if (typeof obj != "object") {
return err("Expected JSON Object.");
}
if (!obj.hasOwnProperty("type")) {
return err("JSON Object missing field `type`. (Parsing union.)");
}
if (!obj.hasOwnProperty("value")) {
return err("JSON Object missing field `value`. (Parsing union.)");
}
let type = obj["type"];
let value = obj["value"];
if (typeof type != "string") {
return err("JSON Object field `type` is meant to be of type string.");
}
if (!typeSchema.parts.hasOwnProperty(type)) {
return err("JSON Object `type` is not one of the valid types in the union type schema.");
}
let typeSchema2 = typeSchema.parts[type as Extract<A, { type: string; }>["type"]];
let value2 = parseJsonViaTypeSchema(typeSchema2 as any, value);
if (value2.type == "Err") {
return err("Failed to parse union. " + value2.message);
}
return ok({ type, value: value2, } as A);
case "Object": {
if (typeof obj != "object") {
return err("Expected JSON Object.");
}
let result: { [key in keyof A]?: A[key] } = {};
for (let key in typeSchema.properties) {
let property = typeSchema.properties[key];
let propertyTypeSchema = property.typeSchema;
let value: A[typeof key];
let optional = property.optional ?? false;
if (!obj.hasOwnProperty(key)) {
if (optional) {
value = property.defaultValue;
} else {
return err("Missing key `" + key + "` on JSON Object.");
}
} else {
let fieldValue = parseJsonViaTypeSchema(propertyTypeSchema, obj[key]);
if (fieldValue.type == "Err") {
return err("Failed to parse JSON Object at key `" + key + "`. " + fieldValue.message);
}
value = fieldValue.value;
}
result[key] = value;
}
return ok(result as A);
}
case "InvMap": {
let typeSchema2 = typeSchema.typeSchema;
let result = parseJsonViaTypeSchema(typeSchema2, obj);
if (result.type == "Err") {
return err("Failed to parse through invariant map. " + result.message);
}
return ok(typeSchema.fn1(result.value));
}
}
}
export function writeJsonViaPropertiesSchema<A>(propertiesSchema: PropertiesSchema<A>, obj: A): any {
if ((propertiesSchema as any).type == "InvMap" && (typeof ((propertiesSchema as any).fn1)) == "function") {
let propertiesSchema2 = propertiesSchema as PropertiesSchemaInvMap<unknown,A>;
return writeJsonViaPropertiesSchema(propertiesSchema2.propertiesSchema, (propertiesSchema as any).fn2(obj));
}
let propertiesSchema2 = propertiesSchema as PropertiesSchema2<A>;
let result: any = {};
for (let key in propertiesSchema2) {
let property = propertiesSchema2[key];
let value = writeJsonViaTypeSchema(property.typeSchema, obj[key]);
result[key] = value;
}
return result;
}
export function writeJsonViaTypeSchema<A>(typeSchema: TypeSchema<A>, obj: A): any {
switch (typeSchema.type) {
case "Boolean": {
return obj;
}
case "Number": {
return obj;
}
case "String": {
return obj;
}
case "Array": {
let result: any[] = [];
let obj2 = obj as any[];
for (let i = 0; i < obj2.length; ++i) {
let elem = obj2[i];
let value = writeJsonViaTypeSchema(typeSchema.elemTypeSchema, elem);
result.push(value);
}
return result;
}
case "Union": {
let type = (obj as any)["type"] as Extract<A,{ type: string, }>["type"];
let obj2 = (obj as any)["value"];
let typeSchema2 = typeSchema.parts[type];
let value = writeJsonViaTypeSchema(typeSchema2, obj2);
return {
type,
value,
};
}
case "Object": {
let result: any = {};
for (let key in typeSchema.properties) {
let property = typeSchema.properties[key];
let value = writeJsonViaTypeSchema(property.typeSchema, obj[key]);
result[key] = value;
}
return result;
}
case "InvMap": {
let typeSchema2 = typeSchema.typeSchema;
return writeJsonViaTypeSchema(typeSchema2, typeSchema.fn2(obj));
}
}
}
let testProperties: PropertiesSchema<{ x: number, y: number }> = {
x: {
displayName: "X",
typeSchema: { type: "Number", },
defaultValue: 0.0,
},
y: {
displayName: "Y",
typeSchema: { type: "Number", },
defaultValue: 0.0,
},
};
let testProperties2: PropertiesSchema<Vec2> = invMapPropertiesSchema(
(a: { x: number, y: number }) => Vec2.create(a.x, a.y),
(a: Vec2) => ({ x: a.x, y: a.y }),
{
x: {
displayName: "X",
typeSchema: { type: "Number" },
defaultValue: 0.0,
},
y: {
displayName: "Y",
typeSchema: { type: "Number" },
defaultValue: 0.0,
},
}
);
let testTypeSchema: TypeSchema<Vec2> = invMapTypeSchema(
(a: { x: number, y: number }) => Vec2.create(a.x, a.y),
(a: Vec2) => ({ x: a.x, y: a.y }),
{
type: "Object",
properties: {
x: {
displayName: "X",
typeSchema: { type: "Number", },
defaultValue: 0.0,
},
y: {
displayName: "Y",
typeSchema: { type: "Number", },
defaultValue: 0.0,
},
}
}
);
let testObject: TypeSchema<{ a: number, b: string,}> = {
type: "Object",
properties: {
a: {
displayName: "A",
typeSchema: { type: "Number", },
defaultValue: 0.0,
},
b: {
displayName: "B",
typeSchema: { type: "String", },
defaultValue: "",
},
},
};
let testUnion: TypeSchema<{ type: "a", value: { x: number } } | { type: "b", value: { y: string } }> = {
type: "Union",
parts: {
a: {
type: "Object",
properties: {
x: {
displayName: "X",
typeSchema: { type: "Number" },
defaultValue: 0.0,
}
},
},
b: {
type: "Object",
properties: {
y: {
displayName: "Y",
typeSchema: { type: "String" },
defaultValue: "",
}
},
},
},
};
function test() {
debugger;
let result = parseJsonViaPropertiesSchema(testProperties2, { x: 3.0, y: 4.0 });
console.log(result);
if (result.type == "Ok") {
let result2 = writeJsonViaPropertiesSchema(testProperties2, result.value);
console.log(result2);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment