model/InterfaceDescription.js


const { arrayToUnique } = require('../utils/array');
const MissingArgumentError = require('../error/MissingArgumentError');

/**
 * @typedef {Object} TypedProperty
 * @property {string} name the name of the property
 * @property {string} type the datatype of the property
 */

/**
 * A class describing an entity's interface, containing 
 * information about the name of the entity, it's properties
 * and it's inheritance.
 * 
 * This class provides getters to construct Typescript
 * interfaces as strings, from the object's contents.
 */
class InterfaceDescription {
  /**
   * A class describing an entity's interface, containing 
   * information about the name of the entity, it's properties
   * and it's inheritance.
   * 
   * This class provides getters to construct Typescript
   * interfaces as strings, from the object's contents.
   * 
   * @param {string} name 
   * @param {TypedProperty[]} typedProperties 
   * @param {string?} inherits 
   * 
   * @throws {MissingArgumentError} if name or typedProperties are missing, an error will be thrown.
   */
  constructor(name, typedProperties, inherits) {
    this.name = name;
    this.typedProperties = typedProperties;
    this.inherits = inherits;

    if (!this.name) {
      throw new MissingArgumentError('name');
    } else if (!this.typedProperties) {
      throw new MissingArgumentError('typedProperties');
    } else if (!this.inherits) {
      this.inherits = null;
    }

    if (!this.inherits && this.name === `${this.name}`.toUpperCase()) {
      this.inherits = "Node";
    }
  }

  /**
   * This getter returns a string of valid ESModule imports 
   * based on the types used by properties and inheritance
   * within this interface.
   * 
   * @returns {string} a valid string of ESModule imports for the types used by this module
   */
  get imports() {
    const defaultTypes = ["Number", "String", "Boolean", "Map"];
    return this.uniqueTypes
      .filter(type => defaultTypes.indexOf(type) < 0)
      .map(type => `import ${type} from "./${type}";`)
      .join("\n")
  }

  /**
   * This method returns an array of strings containing the
   * names of the unique data types leveraged by this 
   * interface.
   * 
   * @returns {string[]} an array of unique datatypes used in this interface
   */
  get uniqueTypes() {
    try {
      return arrayToUnique([
        this.inherits,
        ...(this.typedProperties || []).map(property => property.type.split("[")[0])
      ]
        .map(type => type && type.split("<").join(",").split(">").join("").split(","))
        .flat()
        .filter(type => !!type))
        .map(type => type.trim());
    } catch (e) {
      return [];
    }
  }

  /**
   * This method generates and returns the interface declaration
   * line of typescript for the described entity, including
   * extending an interface defined as inherited.
   * 
   * @returns {string} a valid typescript declaration string, defining an interface with potential inheritance.
   */
  get interfaceDeclaration() {
    return `interface ${this.name}${this.inherits ? ` extends ${this.inherits}` : ""}`
  }

  /**
   * This method generates a string consisting of valid
   * typescript, defining the properties that the entity
   * exposes.
   * 
   * @returns {string} a string of valid syntax for declaring typescript properties in an interface
   */
  get interfaceProperties() {
    return this.typedProperties
      .map(prop => `${prop.name}: ${prop.type};`)
      .join("\n  ");
  }

  /**
   * This method generates a string consisting of valid
   * typescript, defining a Typescript Interface including
   * imports, interface declaration, properties, inheritance
   * and default export of the interface.
   * 
   * @returns {string} a string that is valid code for a typescript interface file
   */
  get typeFileString() {
    return `${this.imports}

export default ${this.interfaceDeclaration} {
  ${this.interfaceProperties}
}`.trim()
  }
}

module.exports = InterfaceDescription;