UNPKG

@hpx7/delta-pack

Version:

A TypeScript code generator and runtime for binary serialization based on schemas.

679 lines (676 loc) 24.9 kB
import { isPrimitiveType } from "./schema"; export function codegenTypescript(schema) { return renderSchema(schema); } function renderSchema(schema) { return `import * as _ from "@hpx7/delta-pack/helpers"; ${Object.entries(schema) .map(([name, type]) => { if (type.type === "enum") { return ` export type ${name} = ${type.options.map((option) => `"${option}"`).join(" | ")}; `; } else { return `export type ${name} = ${renderTypeArg(type, name)};`; } }) .join("\n")} ${Object.entries(schema) .map(([name, type]) => { if (type.type === "enum") { return ` const ${name} = { ${type.options.map((option, i) => `${i}: "${option}",`).join("\n ")} ${type.options.map((option, i) => `${option}: ${i},`).join("\n ")} };`; } if (type.type === "object") { return ` export const ${name} = { default(): ${name} { return { ${Object.entries(type.properties) .map(([name, childType]) => { return `${name}: ${renderDefault(childType, name)},`; }) .join("\n ")} }; }, fromJson(obj: Record<string, unknown>): ${name} { if (typeof obj !== "object" || obj == null || Object.getPrototypeOf(obj) !== Object.prototype) { throw new Error(\`Invalid ${name}: \${obj}\`); } return { ${Object.entries(type.properties) .map(([childName, childType]) => { return `${childName}: _.tryParseField(() => ${renderFromJson(childType, childName, `obj.${childName}`)}, "${name}.${childName}"),`; }) .join("\n ")} }; }, toJson(obj: ${name}): Record<string, unknown> { const result: Record<string, unknown> = {}; ${Object.entries(type.properties) .map(([childName, childType]) => { if (childType.type === "optional") { return `if (obj.${childName} != null) { result.${childName} = ${renderToJson(childType, childName, `obj.${childName}`)}; }`; } else { return `result.${childName} = ${renderToJson(childType, childName, `obj.${childName}`)};`; } }) .join("\n ")} return result; }, equals(a: ${name}, b: ${name}): boolean { return ( ${Object.entries(type.properties) .map(([childName, childType]) => { return renderEquals(childType, name, `a.${childName}`, `b.${childName}`); }) .join(" &&\n ")} ); }, encode(obj: ${name}): Uint8Array { const tracker = new _.Tracker(); ${name}._encode(obj, tracker); return tracker.toBuffer(); }, _encode(obj: ${name}, tracker: _.Tracker): void { ${Object.entries(type.properties) .map(([childName, childType]) => { return `${renderEncode(childType, name, `obj.${childName}`)};`; }) .join("\n ")} }, encodeDiff(a: ${name}, b: ${name}): Uint8Array { const tracker = new _.Tracker(); ${name}._encodeDiff(a, b, tracker); return tracker.toBuffer(); }, _encodeDiff(a: ${name}, b: ${name}, tracker: _.Tracker): void { const dirty = b._dirty; const changed = dirty == null ? !${name}.equals(a, b) : dirty.size > 0; tracker.pushBoolean(changed); if (!changed) { return; } ${Object.entries(type.properties) .map(([childName, childType]) => { return `// Field: ${childName} if (dirty != null && !dirty.has("${childName}")) { tracker.pushBoolean(false); } else { ${renderEncodeDiff(childType, name, `a.${childName}`, `b.${childName}`)}; }`; }) .join("\n ")} }, decode(input: Uint8Array): ${name} { return ${name}._decode(_.Tracker.parse(input)); }, _decode(tracker: _.Tracker): ${name} { return { ${Object.entries(type.properties) .map(([childName, childType]) => { return `${childName}: ${renderDecode(childType, name, `obj.${childName}`)},`; }) .join("\n ")} }; }, decodeDiff(obj: ${name}, input: Uint8Array): ${name} { const tracker = _.Tracker.parse(input); return ${name}._decodeDiff(obj, tracker); }, _decodeDiff(obj: ${name}, tracker: _.Tracker): ${name} { const changed = tracker.nextBoolean(); if (!changed) { return obj; } return { ${Object.entries(type.properties) .map(([childName, childType]) => { return `${childName}: ${renderDecodeDiff(childType, name, `obj.${childName}`)},`; }) .join("\n ")} }; }, };`; } else if (type.type === "union") { return ` export const ${name} = { default(): ${name} { return { type: "${type.options[0].reference}", val: ${renderDefault(type.options[0], type.options[0].reference)}, }; }, values() { return [${type.options.map((option) => `"${option.reference}"`).join(", ")}]; }, fromJson(obj: Record<string, unknown>): ${name} { if (typeof obj !== "object" || obj == null) { throw new Error(\`Invalid ${name}: \${obj}\`); } // check if it's delta-pack format: { type: "TypeName", val: ... } if ("type" in obj && typeof obj.type === "string" && "val" in obj) { ${type.options .map((reference, i) => { return `${i > 0 ? "else " : ""}if (obj.type === "${reference.reference}") { return { type: "${reference.reference}", val: ${renderFromJson(reference, reference.reference, "obj.val")}, }; }`; }) .join("\n ")} else { throw new Error(\`Invalid ${name}: \${obj}\`); } } // check if it's protobuf format: { TypeName: ... } const entries = Object.entries(obj); if (entries.length === 1) { const [fieldName, fieldValue] = entries[0]; ${type.options .map((reference, i) => { return `${i > 0 ? "else " : ""}if (fieldName === "${reference.reference}") { return { type: "${reference.reference}", val: ${renderFromJson(reference, reference.reference, "fieldValue")}, }; }`; }) .join("\n ")} } throw new Error(\`Invalid ${name}: \${obj}\`); }, toJson(obj: ${name}): Record<string, unknown> { ${type.options .map((reference, i) => { return `${i > 0 ? "else " : ""}if (obj.type === "${reference.reference}") { return { ${reference.reference}: ${renderToJson(reference, reference.reference, "obj.val")}, }; }`; }) .join("\n ")} throw new Error(\`Invalid ${name}: \${obj}\`); }, equals(a: ${name}, b: ${name}): boolean { ${type.options .map((reference, i) => { return `${i > 0 ? "else " : ""}if (a.type === "${reference.reference}" && b.type === "${reference.reference}") { return ${renderEquals(reference, reference.reference, "a.val", "b.val")}; }`; }) .join("\n ")} return false; }, encode(obj: ${name}): Uint8Array { const tracker = new _.Tracker(); ${name}._encode(obj, tracker); return tracker.toBuffer(); }, _encode(obj: ${name}, tracker: _.Tracker): void { ${Object.entries(type.options) .map(([childName, reference], i) => { return `${i > 0 ? "else " : ""}if (obj.type === "${reference.reference}") { tracker.pushUInt(${childName}); ${renderEncode(reference, reference.reference, "obj.val")}; }`; }) .join("\n ")} }, encodeDiff(a: ${name}, b: ${name}): Uint8Array { const tracker = new _.Tracker(); ${name}._encodeDiff(a, b, tracker); return tracker.toBuffer(); }, _encodeDiff(a: ${name}, b: ${name}, tracker: _.Tracker): void { ${type.options .map((reference, i) => { return `${i > 0 ? "else " : ""}if (b.type === "${reference.reference}") { tracker.pushBoolean(a.type === "${reference.reference}"); if (a.type === "${reference.reference}") { ${renderEncodeDiff(reference, reference.reference, "a.val", "b.val")}; } else { tracker.pushUInt(${i}); ${renderEncode(reference, reference.reference, "b.val")}; } }`; }) .join("\n ")} }, decode(input: Uint8Array): ${name} { return ${name}._decode(_.Tracker.parse(input)); }, _decode(tracker: _.Tracker): ${name} { const type = tracker.nextUInt(); ${type.options .map((reference, i) => { return `${i > 0 ? "else " : ""}if (type === ${i}) { return { type: "${reference.reference}", val: ${renderDecode(reference, reference.reference, "obj.val")} }; }`; }) .join("\n ")} throw new Error("Invalid union"); }, decodeDiff(obj: ${name}, input: Uint8Array): ${name} { const tracker = _.Tracker.parse(input); return ${name}._decodeDiff(obj, tracker); }, _decodeDiff(obj: ${name}, tracker: _.Tracker): ${name} { const isSameType = tracker.nextBoolean(); if (isSameType) { ${type.options .map((reference, i) => { return `${i > 0 ? "else " : ""}if (obj.type === "${reference.reference}") { return { type: "${reference.reference}", val: ${renderDecodeDiff(reference, reference.reference, "obj.val")}, }; }`; }) .join("\n ")} throw new Error("Invalid union diff"); } else { const type = tracker.nextUInt(); ${type.options .map((reference, i) => { return `${i > 0 ? "else " : ""}if (type === ${i}) { return { type: "${reference.reference}", val: ${renderDecode(reference, reference.reference, "obj.val")} }; }`; }) .join("\n ")} throw new Error("Invalid union diff"); } } }`; } }) .join("\n")}`; function lookup(type) { if (type.reference in schema) { return schema[type.reference]; } throw new Error(`Reference ${JSON.stringify(type.reference)} not found, searched ${Object.keys(schema)}`); } function renderTypeArg(type, name) { if (type.type === "object") { return `{ ${Object.entries(type.properties) .map(([name, childType]) => { return `${name}${childType.type === "optional" ? "?" : ""}: ${renderTypeArg(childType, name)};`; }) .join("\n ")} } & { _dirty?: Set<keyof ${name}> }`; } else if (type.type === "union") { return type.options .map((option) => `{ type: "${renderTypeArg(option, name)}"; val: ${renderTypeArg(option, name)} }`) .join(" | "); } else if (type.type === "array") { return `${renderTypeArg(type.value, name)}[] & { _dirty?: Set<number> }`; } else if (type.type === "optional") { return `${renderTypeArg(type.value, name)}`; } else if (type.type === "record") { return `Map<${renderTypeArg(type.key, name)}, ${renderTypeArg(type.value, name)}> & { _dirty?: Set<${renderTypeArg(type.key, name)}> }`; } else if (type.type === "reference") { return type.reference; } else if (type.type === "int" || type.type === "uint" || type.type === "float") { return "number"; } return type.type; } function renderDefault(type, name) { if (type.type === "array") { return "[]"; } else if (type.type === "optional") { return "undefined"; } else if (type.type === "record") { return "new Map()"; } else if (type.type === "reference") { return renderDefault(lookup(type), type.reference); } else if (type.type === "string") { return '""'; } else if (type.type === "int") { return "0"; } else if (type.type === "uint") { return "0"; } else if (type.type === "float") { return "0.0"; } else if (type.type === "boolean") { return "false"; } else if (type.type === "enum") { return `"${type.options[0]}"`; } return `${name}.default()`; } function renderFromJson(type, name, key) { if (type.type === "array") { return `_.parseArray(${key}, (x) => ${renderFromJson(type.value, name, "x")})`; } else if (type.type === "optional") { return `_.parseOptional(${key}, (x) => ${renderFromJson(type.value, name, "x")})`; } else if (type.type === "record") { const keyFn = renderFromJson(type.key, name, "x"); const valueFn = renderFromJson(type.value, name, "x"); return `_.parseRecord(${key}, (x) => ${keyFn}, (x) => ${valueFn})`; } else if (type.type === "reference") { return renderFromJson(lookup(type), type.reference, key); } else if (type.type === "string") { return `_.parseString(${key})`; } else if (type.type === "int") { return `_.parseInt(${key})`; } else if (type.type === "uint") { return `_.parseUInt(${key})`; } else if (type.type === "float") { return `_.parseFloat(${key})`; } else if (type.type === "boolean") { return `_.parseBoolean(${key})`; } else if (type.type === "enum") { return `_.parseEnum(${key}, ${name})`; } return `${name}.fromJson(${key} as ${name})`; } function renderToJson(type, name, key) { if (type.type === "array") { return `${key}.map((x) => ${renderToJson(type.value, name, "x")})`; } else if (type.type === "optional") { return renderToJson(type.value, name, key); } else if (type.type === "record") { return `_.mapToObject(${key}, (x) => ${renderToJson(type.value, name, "x")})`; } else if (type.type === "reference") { return renderToJson(lookup(type), type.reference, key); } else if (type.type === "string" || type.type === "int" || type.type === "uint" || type.type === "float" || type.type === "boolean" || type.type === "enum") { return `${key}`; } return `${name}.toJson(${key})`; } function renderEquals(type, name, keyA, keyB) { if (type.type === "array") { return `_.equalsArray(${keyA}, ${keyB}, (x, y) => ${renderEquals(type.value, name, "x", "y")})`; } else if (type.type === "optional") { return `_.equalsOptional(${keyA}, ${keyB}, (x, y) => ${renderEquals(type.value, name, "x", "y")})`; } else if (type.type === "record") { const keyEquals = renderEquals(type.key, name, "x", "y"); const valueEquals = renderEquals(type.value, name, "x", "y"); return `_.equalsRecord(${keyA}, ${keyB}, (x, y) => ${keyEquals}, (x, y) => ${valueEquals})`; } else if (type.type === "reference") { return renderEquals(lookup(type), type.reference, keyA, keyB); } else if (type.type === "float") { if (type.precision) { return `_.equalsFloatQuantized(${keyA}, ${keyB}, ${type.precision})`; } return `_.equalsFloat(${keyA}, ${keyB})`; } else if (type.type === "string" || type.type === "int" || type.type === "uint" || type.type === "boolean" || type.type === "enum") { return `${keyA} === ${keyB}`; } return `${name}.equals(${keyA}, ${keyB})`; } function renderEncode(type, name, key) { if (type.type === "array") { return `tracker.pushArray(${key}, (x) => ${renderEncode(type.value, name, "x")})`; } else if (type.type === "optional") { return `tracker.pushOptional(${key}, (x) => ${renderEncode(type.value, name, "x")})`; } else if (type.type === "record") { const keyFn = renderEncode(type.key, name, "x"); const valueFn = renderEncode(type.value, name, "x"); return `tracker.pushRecord(${key}, (x) => ${keyFn}, (x) => ${valueFn})`; } else if (type.type === "reference") { return renderEncode(lookup(type), type.reference, key); } else if (type.type === "string") { return `tracker.pushString(${key})`; } else if (type.type === "int") { return `tracker.pushInt(${key})`; } else if (type.type === "uint") { return `tracker.pushUInt(${key})`; } else if (type.type === "float") { if (type.precision) { return `tracker.pushFloatQuantized(${key}, ${type.precision})`; } return `tracker.pushFloat(${key})`; } else if (type.type === "boolean") { return `tracker.pushBoolean(${key})`; } else if (type.type === "enum") { return `tracker.pushUInt(${name}[${key}])`; } return `${name}._encode(${key}, tracker)`; } function renderDecode(type, name, key) { if (type.type === "array") { return `tracker.nextArray(() => ${renderDecode(type.value, name, "x")})`; } else if (type.type === "optional") { return `tracker.nextOptional(() => ${renderDecode(type.value, name, "x")})`; } else if (type.type === "record") { const keyFn = renderDecode(type.key, name, "x"); const valueFn = renderDecode(type.value, name, "x"); return `tracker.nextRecord(() => ${keyFn}, () => ${valueFn})`; } else if (type.type === "reference") { return renderDecode(lookup(type), type.reference, key); } else if (type.type === "string") { return `tracker.nextString()`; } else if (type.type === "int") { return `tracker.nextInt()`; } else if (type.type === "uint") { return `tracker.nextUInt()`; } else if (type.type === "float") { if (type.precision) { return `tracker.nextFloatQuantized(${type.precision})`; } return `tracker.nextFloat()`; } else if (type.type === "boolean") { return `tracker.nextBoolean()`; } else if (type.type === "enum") { return `(${name} as any)[tracker.nextUInt()]`; } return `${name}._decode(tracker)`; } function renderEncodeDiff(type, name, keyA, keyB) { if (type.type === "array") { const valueType = renderTypeArg(type.value, name); const equalsFn = renderEquals(type.value, name, "x", "y"); const encodeFn = renderEncode(type.value, name, "x"); const encodeDiffFn = renderEncodeDiff(type.value, name, "x", "y"); return `tracker.pushArrayDiff<${valueType}>( ${keyA}, ${keyB}, (x, y) => ${equalsFn}, (x) => ${encodeFn}, (x, y) => ${encodeDiffFn} )`; } else if (type.type === "optional") { const valueType = renderTypeArg(type.value, name); const encodeFn = renderEncode(type.value, name, "x"); if (isPrimitiveType(type.value, schema)) { return `tracker.pushOptionalDiffPrimitive<${valueType}>( ${keyA}, ${keyB}, (x) => ${encodeFn} )`; } else { const encodeDiffFn = renderEncodeDiff(type.value, name, "x", "y"); return `tracker.pushOptionalDiff<${valueType}>( ${keyA}, ${keyB}, (x) => ${encodeFn}, (x, y) => ${encodeDiffFn} )`; } } else if (type.type === "record") { const keyType = renderTypeArg(type.key, name); const valueType = renderTypeArg(type.value, name); const equalsFn = renderEquals(type.value, name, "x", "y"); const encodeKeyFn = renderEncode(type.key, name, "x"); const encodeValFn = renderEncode(type.value, name, "x"); const encodeDiffFn = renderEncodeDiff(type.value, name, "x", "y"); return `tracker.pushRecordDiff<${keyType}, ${valueType}>( ${keyA}, ${keyB}, (x, y) => ${equalsFn}, (x) => ${encodeKeyFn}, (x) => ${encodeValFn}, (x, y) => ${encodeDiffFn} )`; } else if (type.type === "reference") { return renderEncodeDiff(lookup(type), type.reference, keyA, keyB); } else if (type.type === "string") { return `tracker.pushStringDiff(${keyA}, ${keyB})`; } else if (type.type === "int") { return `tracker.pushIntDiff(${keyA}, ${keyB})`; } else if (type.type === "uint") { return `tracker.pushUIntDiff(${keyA}, ${keyB})`; } else if (type.type === "float") { if (type.precision) { return `tracker.pushFloatQuantizedDiff(${keyA}, ${keyB}, ${type.precision})`; } return `tracker.pushFloatDiff(${keyA}, ${keyB})`; } else if (type.type === "boolean") { return `tracker.pushBooleanDiff(${keyA}, ${keyB})`; } else if (type.type === "enum") { return `tracker.pushUIntDiff(${name}[${keyA}], ${name}[${keyB}])`; } return `${name}._encodeDiff(${keyA}, ${keyB}, tracker)`; } function renderDecodeDiff(type, name, key) { if (type.type === "array") { const valueType = renderTypeArg(type.value, name); const decodeFn = renderDecode(type.value, name, "x"); const decodeDiffFn = renderDecodeDiff(type.value, name, "x"); return `tracker.nextArrayDiff<${valueType}>( ${key}, () => ${decodeFn}, (x) => ${decodeDiffFn} )`; } else if (type.type === "optional") { const valueType = renderTypeArg(type.value, name); const decodeFn = renderDecode(type.value, name, "x"); if (isPrimitiveType(type.value, schema)) { return `tracker.nextOptionalDiffPrimitive<${valueType}>( ${key}, () => ${decodeFn} )`; } else { const decodeDiffFn = renderDecodeDiff(type.value, name, "x"); return `tracker.nextOptionalDiff<${valueType}>( ${key}, () => ${decodeFn}, (x) => ${decodeDiffFn} )`; } } else if (type.type === "record") { const keyType = renderTypeArg(type.key, name); const valueType = renderTypeArg(type.value, name); const decodeKeyFn = renderDecode(type.key, name, "x"); const decodeValueFn = renderDecode(type.value, name, "x"); const decodeDiffFn = renderDecodeDiff(type.value, name, "x"); return `tracker.nextRecordDiff<${keyType}, ${valueType}>( ${key}, () => ${decodeKeyFn}, () => ${decodeValueFn}, (x) => ${decodeDiffFn} )`; } else if (type.type === "reference") { return renderDecodeDiff(lookup(type), type.reference, key); } else if (type.type === "string") { return `tracker.nextStringDiff(${key})`; } else if (type.type === "int") { return `tracker.nextIntDiff(${key})`; } else if (type.type === "uint") { return `tracker.nextUIntDiff(${key})`; } else if (type.type === "float") { if (type.precision) { return `tracker.nextFloatQuantizedDiff(${key}, ${type.precision})`; } return `tracker.nextFloatDiff(${key})`; } else if (type.type === "boolean") { return `tracker.nextBooleanDiff(${key})`; } else if (type.type === "enum") { return `(${name} as any)[tracker.nextUIntDiff((${name} as any)[${key}])]`; } return `${name}._decodeDiff(${key}, tracker)`; } }