maketypes
Version:
Make TypeScript types and proxy objects from example JSON objects. Can use proxy objects to dynamically type check JSON at runtime.
639 lines (605 loc) • 17.5 kB
text/typescript
import {default as Emitter, emitProxyTypeCheck} from './emit';
export const enum BaseShape {
BOTTOM,
NULL,
RECORD,
STRING,
BOOLEAN,
NUMBER,
COLLECTION,
ANY
}
export type Shape = CBottomShape | CNullShape | CRecordShape | CStringShape | CBooleanShape | CNumberShape | CCollectionShape | CAnyShape;
export const enum ContextType {
ENTITY,
FIELD
}
function pascalCase(n: string): string {
return n.split("_").map((s) => (s[0] ? s[0].toUpperCase() : "") + s.slice(1)).join("");
}
export function getReferencedRecordShapes(e: Emitter, s: Set<CRecordShape>, sh: Shape): void {
switch (sh.type) {
case BaseShape.RECORD:
if (!s.has(sh)) {
s.add(sh);
sh.getReferencedRecordShapes(e, s);
}
break;
case BaseShape.COLLECTION:
getReferencedRecordShapes(e, s, sh.baseShape);
break;
case BaseShape.ANY:
sh.getDistilledShapes(e).forEach((sh) => getReferencedRecordShapes(e, s, sh));
break;
}
}
export class FieldContext {
public get type(): ContextType.FIELD {
return ContextType.FIELD;
}
public readonly parent: CRecordShape;
public readonly field: string;
constructor(parent: CRecordShape, field: string) {
this.parent = parent;
this.field = field;
}
public getName(e: Emitter): string {
const name = pascalCase(this.field);
return name;
}
}
export class EntityContext {
public get type(): ContextType.ENTITY {
return ContextType.ENTITY;
}
public readonly parent: CCollectionShape;
constructor(parent: CCollectionShape) {
this.parent = parent;
}
public getName(e: Emitter): string {
return `${this.parent.getName(e)}Entity`;
}
}
export type Context = FieldContext | EntityContext;
export class CBottomShape {
public get type(): BaseShape.BOTTOM {
return BaseShape.BOTTOM;
}
public get nullable(): boolean {
return false;
}
public makeNullable(): CBottomShape {
throw new TypeError(`Doesn't make sense.`);
}
public makeNonNullable(): CBottomShape {
return this;
}
public emitType(e: Emitter): void {
throw new Error(`Doesn't make sense.`);
}
public getProxyType(e: Emitter): string {
throw new Error(`Doesn't make sense.`);
}
public equal(t: Shape): boolean {
return this === t;
}
}
export const BottomShape = new CBottomShape();
export class CNullShape {
public get nullable(): boolean {
return true;
}
public get type(): BaseShape.NULL {
return BaseShape.NULL;
}
public makeNullable(): CNullShape {
return this;
}
public makeNonNullable(): CNullShape {
return this;
}
public emitType(e: Emitter): void {
e.interfaces.write("null");
}
public getProxyType(e: Emitter): string {
return "null";
}
public equal(t: Shape): boolean {
return this === t;
}
}
export const NullShape = new CNullShape();
export class CNumberShape {
public get nullable(): boolean {
return this === NullableNumberShape;
}
public get type(): BaseShape.NUMBER {
return BaseShape.NUMBER;
}
public makeNullable(): CNumberShape {
return NullableNumberShape;
}
public makeNonNullable(): CNumberShape {
return NumberShape;
}
public emitType(e: Emitter): void {
e.interfaces.write(this.getProxyType(e));
}
public getProxyType(e: Emitter): string {
let rv = "number";
if (this.nullable) {
rv += " | null";
}
return rv;
}
public equal(t: Shape): boolean {
return this === t;
}
}
export const NumberShape = new CNumberShape();
export const NullableNumberShape = new CNumberShape();
export class CStringShape {
public get type(): BaseShape.STRING {
return BaseShape.STRING;
}
public get nullable(): boolean {
return this === NullableStringShape;
}
public makeNullable(): CStringShape {
return NullableStringShape;
}
public makeNonNullable(): CStringShape {
return StringShape;
}
public emitType(e: Emitter): void {
e.interfaces.write(this.getProxyType(e));
}
public getProxyType(e: Emitter): string {
let rv = "string";
if (this.nullable) {
rv += " | null";
}
return rv;
}
public equal(t: Shape): boolean {
return this === t;
}
}
export const StringShape = new CStringShape();
export const NullableStringShape = new CStringShape();
export class CBooleanShape {
public get type(): BaseShape.BOOLEAN {
return BaseShape.BOOLEAN;
}
public get nullable(): boolean {
return this === NullableBooleanShape;
}
public makeNullable(): CBooleanShape {
return NullableBooleanShape;
}
public makeNonNullable(): CBooleanShape {
return BooleanShape;
}
public emitType(e: Emitter): void {
e.interfaces.write(this.getProxyType(e));
}
public getProxyType(e: Emitter): string {
let rv = "boolean";
if (this.nullable) {
rv += " | null";
}
return rv;
}
public equal(t: Shape): boolean {
return this === t;
}
}
export const BooleanShape = new CBooleanShape();
export const NullableBooleanShape = new CBooleanShape();
export class CAnyShape {
public get type(): BaseShape.ANY {
return BaseShape.ANY;
}
private readonly _shapes: Shape[];
private readonly _nullable: boolean = false;
private _hasDistilledShapes: boolean = false;
private _distilledShapes: Shape[] = [];
constructor(shapes: Shape[], nullable: boolean) {
this._shapes = shapes;
this._nullable = nullable;
}
public get nullable(): boolean {
return this._nullable === true;
}
public makeNullable(): CAnyShape {
if (this._nullable) {
return this;
} else {
return new CAnyShape(this._shapes, true);
}
}
public makeNonNullable(): CAnyShape {
if (this._nullable) {
return new CAnyShape(this._shapes, false);
} else {
return this;
}
}
private _ensureDistilled(e: Emitter): void {
if (!this._hasDistilledShapes) {
let shapes = new Map<BaseShape, Shape[]>();
for (let i = 0; i < this._shapes.length; i++) {
const s = this._shapes[i];
if (!shapes.has(s.type)) {
shapes.set(s.type, []);
}
shapes.get(s.type)!.push(s);
}
shapes.forEach((shapes, key) => {
let shape: Shape = BottomShape;
for (let i = 0; i < shapes.length; i++) {
shape = csh(e, shape, shapes[i]);
}
this._distilledShapes.push(shape);
});
this._hasDistilledShapes = true;
}
}
public getDistilledShapes(e: Emitter): Shape[] {
this._ensureDistilled(e);
return this._distilledShapes;
}
public addToShapes(shape: Shape): CAnyShape {
const shapeClone = this._shapes.slice(0);
shapeClone.push(shape);
return new CAnyShape(shapeClone, this._nullable);
}
public emitType(e: Emitter): void {
this._ensureDistilled(e);
this._distilledShapes.forEach((s, i) => {
s.emitType(e);
if (i < this._distilledShapes.length - 1) {
e.interfaces.write(" | ");
}
});
}
public getProxyType(e: Emitter): string {
this._ensureDistilled(e);
return this._distilledShapes.map((s) => s.getProxyType(e)).join(" | ");
}
public equal(t: Shape): boolean {
return this === t;
}
}
export class CRecordShape {
public get type(): BaseShape.RECORD {
return BaseShape.RECORD;
}
private readonly _nullable: boolean;
private readonly _fields: Map<string, Shape>;
public readonly contexts: Context[];
private _name: string | null = null;
private constructor(fields: Map<string, Shape>, nullable: boolean, contexts: Context[]) {
// Assign a context to all fields.
const fieldsWithContext = new Map<string, Shape>();
fields.forEach((val, index) => {
if (val.type === BaseShape.RECORD || val.type === BaseShape.COLLECTION) {
fieldsWithContext.set(index, val.addContext(new FieldContext(this, index)));
} else {
fieldsWithContext.set(index, val);
}
});
this._fields = fieldsWithContext;
this._nullable = nullable;
this.contexts = contexts;
}
public get nullable(): boolean {
return this._nullable;
}
/**
* Construct a new record shape. Returns an existing, equivalent record shape
* if applicable.
*/
public static Create(e: Emitter, fields: Map<string, Shape>, nullable: boolean, contexts: Context[] = []): CRecordShape {
const record = new CRecordShape(fields, nullable, contexts);
return e.registerRecordShape(record);
}
public makeNullable(): CRecordShape {
if (this._nullable) {
return this;
} else {
return new CRecordShape(this._fields, true, this.contexts);
}
}
public addContext(ctx: Context): CRecordShape {
this.contexts.push(ctx);
return this;
}
public makeNonNullable(): CRecordShape {
if (this._nullable) {
return new CRecordShape(this._fields, false, this.contexts);
} else {
return this;
}
}
public forEachField(cb: (t: Shape, name: string) => any): void {
this._fields.forEach(cb);
}
public getField(name: string): Shape {
const t = this._fields.get(name);
if (!t) {
return NullShape;
} else {
return t;
}
}
public equal(t: Shape): boolean {
if (t.type === BaseShape.RECORD && this._nullable === t._nullable && this._fields.size === t._fields.size) {
let rv = true;
const tFields = t._fields;
// Check all fields.
// NOTE: Since size is equal, no need to iterate over t. Either they have the same fields
// or t is missing fields from this one.
this.forEachField((t, name) => {
if (rv) {
const field = tFields.get(name);
if (field) {
rv = field.equal(t);
} else {
rv = false;
}
}
});
return rv;
}
return false;
}
public emitType(e: Emitter): void {
e.interfaces.write(this.getName(e));
if (this.nullable) {
e.interfaces.write(" | null");
}
}
public getProxyClass(e: Emitter): string {
return `${this.getName(e)}Proxy`;
}
public getProxyType(e: Emitter): string {
let rv = `${this.getName(e)}Proxy`;
if (this.nullable) {
rv += " | null";
}
return rv;
}
public emitInterfaceDefinition(e: Emitter): void {
const w = e.interfaces;
w.write(`export interface ${this.getName(e)} {`).endl();
this.forEachField((t, name) => {
w.tab(1).write(name);
if (t.nullable) {
w.write("?");
}
w.write(": ");
t.emitType(e);
w.write(";").endl();
});
w.write(`}`);
}
public emitProxyClass(e: Emitter): void {
const w = e.proxies;
w.writeln(`export class ${this.getProxyClass(e)} {`);
this.forEachField((t, name) => {
w.tab(1).writeln(`public readonly ${name}: ${t.getProxyType(e)};`);
});
w.tab(1).writeln(`public static Parse(d: string): ${this.getProxyType(e)} {`);
w.tab(2).writeln(`return ${this.getProxyClass(e)}.Create(JSON.parse(d));`);
w.tab(1).writeln(`}`);
w.tab(1).writeln(`public static Create(d: any, field: string = 'root'): ${this.getProxyType(e)} {`);
w.tab(2).writeln(`if (!field) {`);
w.tab(3).writeln(`obj = d;`);
w.tab(3).writeln(`field = "root";`);
w.tab(2).writeln(`}`);
w.tab(2).writeln(`if (d === null || d === undefined) {`);
w.tab(3);
if (this.nullable) {
w.writeln(`return null;`);
} else {
e.markHelperAsUsed('throwNull2NonNull');
w.writeln(`throwNull2NonNull(field, d);`);
}
w.tab(2).writeln(`} else if (typeof(d) !== 'object') {`);
e.markHelperAsUsed('throwNotObject');
w.tab(3).writeln(`throwNotObject(field, d, ${this.nullable});`);
w.tab(2).writeln(`} else if (Array.isArray(d)) {`)
e.markHelperAsUsed('throwIsArray');
w.tab(3).writeln(`throwIsArray(field, d, ${this.nullable});`);
w.tab(2).writeln(`}`);
// At this point, we know we have a non-null object.
// Check all fields.
this.forEachField((t, name) => {
emitProxyTypeCheck(e, w, t, 2, `d.${name}`, `field + ".${name}"`);
});
w.tab(2).writeln(`return new ${this.getProxyClass(e)}(d);`);
w.tab(1).writeln(`}`);
w.tab(1).writeln(`private constructor(d: any) {`);
// Emit an assignment for each field.
this.forEachField((t, name) => {
w.tab(2).writeln(`this.${name} = d.${name};`);
});
w.tab(1).writeln(`}`);
w.writeln('}');
}
public getReferencedRecordShapes(e: Emitter, rv: Set<CRecordShape>): void {
this.forEachField((t, name) => {
getReferencedRecordShapes(e, rv, t);
});
}
public markAsRoot(name: string): void {
this._name = name;
}
public getName(e: Emitter): string {
if (typeof(this._name) === 'string') {
return this._name;
}
// Calculate unique name.
const nameSet = new Set<string>();
let name = this.contexts
.map((c) => c.getName(e))
// Remove duplicate names.
.filter((n) => {
if (!nameSet.has(n)) {
nameSet.add(n);
return true;
}
return false;
})
.join("Or");
this._name = e.registerName(name);
return this._name;
}
}
export class CCollectionShape {
public get type(): BaseShape.COLLECTION {
return BaseShape.COLLECTION;
}
public readonly baseShape: Shape;
public readonly contexts: Context[];
private _name: string | null = null;
constructor(baseShape: Shape, contexts: Context[] = []) {
// Add context if a record/collection.
this.baseShape = (baseShape.type === BaseShape.RECORD || baseShape.type === BaseShape.COLLECTION) ? baseShape.addContext(new EntityContext(this)) : baseShape;
this.contexts = contexts;
}
public get nullable(): boolean {
return true;
}
public makeNullable(): CCollectionShape {
return this;
}
public makeNonNullable(): CCollectionShape {
return this;
}
public addContext(ctx: Context): CCollectionShape {
this.contexts.push(ctx);
return this;
}
public emitType(e: Emitter): void {
e.interfaces.write("(");
this.baseShape.emitType(e);
e.interfaces.write(")[] | null");
}
public getProxyType(e: Emitter): string {
const base = this.baseShape.getProxyType(e);
if (base.indexOf("|") !== -1) {
return `(${base})[] | null`;
} else {
return `${base}[] | null`;
}
}
public equal(t: Shape): boolean {
return t.type === BaseShape.COLLECTION && this.baseShape.equal(t.baseShape);
}
public getName(e: Emitter): string {
if (typeof(this._name) === 'string') {
return this._name;
}
const nameSet = new Set<string>();
// No need to make collection names unique.
this._name = this.contexts
.map((c) => c.getName(e))
.filter((name) => {
if (!nameSet.has(name)) {
nameSet.add(name);
return true;
}
return false;
})
.join("Or");
return this._name;
}
}
export function csh(e: Emitter, s1: Shape, s2: Shape): Shape {
// csh(σ, σ) = σ
if (s1 === s2) {
return s1;
}
if (s1.type === BaseShape.COLLECTION && s2.type === BaseShape.COLLECTION) {
// csh([σ1], [σ2]) = [csh(σ1, σ2)]
return new CCollectionShape(csh(e, s1.baseShape, s2.baseShape));
}
// csh(⊥, σ) = csh(σ, ⊥) = σ
if (s1.type === BaseShape.BOTTOM) {
return s2;
}
if (s2.type === BaseShape.BOTTOM) {
return s1;
}
// csh(null, σ) = csh(σ, null) = nullable<σ>
if (s1.type === BaseShape.NULL) {
return s2.makeNullable();
}
if (s2.type === BaseShape.NULL) {
return s1.makeNullable();
}
// csh(any, σ) = csh(σ, any) = any
if (s1.type === BaseShape.ANY) {
return s1.addToShapes(s2);
}
if (s2.type === BaseShape.ANY) {
return s2.addToShapes(s1);
}
// csh(σ2, nullable<σˆ1> ) = csh(nullable<σˆ1> , σ2) = nullable<csh(σˆ1, σ2)>
if (s1.nullable && s1.type !== BaseShape.COLLECTION) {
return csh(e, s1.makeNonNullable(), s2).makeNullable();
}
if (s2.nullable && s2.type !== BaseShape.COLLECTION) {
return csh(e, s2.makeNonNullable(), s1).makeNullable();
}
// (recd) rule
if (s1.type === BaseShape.RECORD && s2.type === BaseShape.RECORD) {
// Get all fields.
const fields = new Map<string, Shape>();
s1.forEachField((t, name) => {
fields.set(name, csh(e, t, s2.getField(name)));
});
s2.forEachField((t, name) => {
if (!fields.has(name)) {
fields.set(name, csh(e, t, s1.getField(name)));
}
});
return CRecordShape.Create(e, fields, false);
}
// (any) rule
return new CAnyShape([s1, s2], s1.nullable || s2.nullable);
}
export function d2s(e: Emitter, d: any): Shape {
if (d === undefined || d === null) {
return NullShape;
}
switch (typeof(d)) {
case 'number':
return NumberShape;
case 'string':
return StringShape;
case 'boolean':
return BooleanShape;
}
// Must be an object or array.
if (Array.isArray(d)) {
// Empty array: Not enough information to figure out a precise type.
if (d.length === 0) {
return new CCollectionShape(NullShape);
}
let t: Shape = BottomShape;
for (let i = 0; i < d.length; i++) {
t = csh(e, t, d2s(e, d[i]));
}
return new CCollectionShape(t);
}
const keys = Object.keys(d);
const fields = new Map<string, Shape>();
for (let i = 0; i < keys.length; i++) {
const name = keys[i];
fields.set(name, d2s(e, d[name]));
}
return CRecordShape.Create(e, fields, false);
}