All files codegen-types.ts

86.3% Statements 63/73
81.54% Branches 53/65
100% Functions 10/10
86.11% Lines 62/72

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141    2x                       2x 10x 10x   10x       18x       18x       10x 17x 17x 17x 17x         18x 18x 16x 21x     16x 6x 6x 6x 6x     2x 3x         24x 24x 2x   24x 24x 24x       30x 17x     30x                   17x 17x 30x 30x 28x 28x     17x       24x 24x 11x 2x   1x 10x               10x 10x 2x 2x 2x 2x     2x 2x   2x 1x 1x     8x 5x 1x 1x 1x                   16x    
import { JSONSchema4 } from "json-schema";
import { CodeMaker } from "codemaker";
import {  
  camelCase,
  pascalCase
} from "change-case";
 
export interface ResolvedTypes {
  type: string;
  assignable: boolean;
  optional: boolean;
  referencable: boolean;
}
 
export class TypeGenerator {
  private readonly emitted = new Set<string>();
  public readonly types: {[key: string]: {[key: string]: ResolvedTypes}} = { };
 
  constructor(private readonly type: string) {
  }
 
  public addType(typeName: string, def: JSONSchema4) {    
    Iif (this.emitted.has(typeName)) {
      console.log('hier')
      return;
    }
    this.resolveTypes(typeName, def.block || def);    
  }
 
  public generate(code: CodeMaker) {
    for (const type of Object.keys(this.types)) {
      const spec = this.types[type];
      this.emitStruct(code, type, spec)
      code.line();
      this.emitted.add(type);
    }    
  }
 
  private resolveTypes(typeName: string, def: JSONSchema4) {
    const self = this;
    if (def.attributes || def.block_types) {
      for (const [ propName, propSpec ] of Object.entries(def.attributes || {})) {
        resolveProperty(propName, propSpec as JSONSchema4);
      }
 
      for (const [ blockName, blockSpec ] of Object.entries(def.block_types || {})) {
        const newTypeName = `${typeName}${pascalCase(blockName)}Props`
        self.addType(newTypeName, blockSpec as JSONSchema4)
        const emptyType = (Object.keys((blockSpec as JSONSchema4)?.block?.attributes).length === 0)
        addProperty(blockName, emptyType ? 'any' : newTypeName, true, true, false)
      }
    } else {
      for (const [ propName, propSpec ] of Object.entries(def || {})) {
        resolveProperty(propName, propSpec as JSONSchema4);
      }
    }
 
    function resolveProperty(name: string, def: JSONSchema4) {
      let assignable = true      
      if ((def.computed && !def.optional) || (self.type !== 'data' && name === 'id')) {
        assignable = false
      }
      const propertyType = self.typeForProperty(def, name, typeName);
      const optional = (def.optional || def.computed)
      addProperty(name, propertyType, assignable, optional, def.computed)
    }  
 
    function addProperty(name: string, propertyType: string, assignable: boolean, optional: boolean, referencable: boolean) {
      if (!self.types[typeName]) {
        self.types[typeName] = {}
      }
 
      self.types[typeName][name] = {
        type: propertyType,
        assignable,
        optional,
        referencable
      }
    }
  }
 
  private emitStruct(code: CodeMaker, typeName: string, def: {[key: string]: ResolvedTypes}) {
    code.openBlock(`export interface ${typeName}`);
    for (const propertyName of Object.keys(def)) {
      const property = def[propertyName]
      if (!property.assignable) continue;
      code.line(`readonly ${camelCase(propertyName)}${property.optional ? '?' : ''}: ${property.type};`);
      code.line();
    }
    
    code.closeBlock();
  }
 
  private typeForProperty(def: JSONSchema4, name: string, typeName: string): string {  
    const comparable = def.type || def
    switch (true) {
      case (comparable === "string"): return 'string';
      case (comparable === "number"): return 'number';
      case (comparable === "integer"): return 'number';
      case (comparable?.toString() === "bool"): return 'boolean';
      case (Array.isArray(comparable)): return this.handleArrayType(comparable as JSONSchema4, name, typeName);
      default: 
        console.log({typeForProperty: def, name, typeName, comparable})
        return 'any';
    }
  }
 
  private handleArrayType(def: JSONSchema4, name: string, typeName: string): string {
    const element = ((def.type || def) as Array<any>)
    if (Array.isArray(element[element.length - 1])) {
      const type = element[0]
      const obj = element[element.length - 1]
      let newTypeName = undefined;
      Iif (this.jsonEqual(obj, ["map", "string"])) {
        newTypeName = `{[key: string]: string}`
      } else {
        newTypeName = `${typeName}${pascalCase(name)}Props`  
        this.addType(newTypeName, obj[obj.length - 1] as JSONSchema4)  
      }
      switch(type) {
        case 'list': return `${newTypeName}[]`;
        case 'set': return `Set<${newTypeName}>`;
      }    
    }
    switch (true) {
      case (this.jsonEqual(element, ["map", "string"])): return '{[k:string]: string}';
      case (this.jsonEqual(element, ["map", "bool"])): return '{[k:string]: boolean}';
      case (this.jsonEqual(element, ["map", "number"])): return '{[k:string]: number}';
      case (this.jsonEqual(element, ["set", "number"])): return 'Set<number>';
      case (this.jsonEqual(element, ["set", "string"])): return 'Set<string>';
      case (this.jsonEqual(element, ["list", "string"])): return 'string[]';      
      default: 
        console.log({handleArrayType: def, name, typeName})
        return 'any';
    }
  }
 
  private jsonEqual(a: any, b: any): boolean {
    return JSON.stringify(a) === JSON.stringify(b);
  }
}