Last active
August 12, 2023 01:14
-
-
Save clinuxrulz/73c28cb7875e6e9206f6a964767b35cf to your computer and use it in GitHub Desktop.
Type checked JSON parsing
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 { 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