diff --git a/packages/generator/README.md b/packages/generator/README.md index b5da59c2..dfd561d8 100644 --- a/packages/generator/README.md +++ b/packages/generator/README.md @@ -193,8 +193,8 @@ Custom configuration is expected to be a JSON file with the following structure: ```ts export interface IConfig { api: string; - entitiesPath?: string; - enumsPath?: string; + entitiesPath?: string | IPathConfig; + defaultEntitiesPath?: string; // Used only when IPathConfig is used for entitiesPath observable?: ObservableConfig; enums?: "enum" | "string"; dates?: "native" | "date-fns"; @@ -207,6 +207,10 @@ interface HasExclude { exclude?: string[]; } +export interface IPathConfig { + [key: string]: string | RegExp; // Provide RegExp decoded as "//" string +} + export type ObservableConfig = | boolean | { diff --git a/packages/generator/src/openapi/defaultConfig.json b/packages/generator/src/openapi/defaultConfig.json index d36bb64b..0375ab6f 100644 --- a/packages/generator/src/openapi/defaultConfig.json +++ b/packages/generator/src/openapi/defaultConfig.json @@ -2,6 +2,7 @@ "api": "https://fruits-demo.herokuapp.com/api/swagger-json", "entitiesPath": "src/entities", + "defaultEntitiesPath": "src/entities", "repositoriesPath": "src/repositories", "validation": true, diff --git a/packages/generator/src/openapi/fileGenerator.ts b/packages/generator/src/openapi/fileGenerator.ts index b1088ca5..bd7b94ff 100644 --- a/packages/generator/src/openapi/fileGenerator.ts +++ b/packages/generator/src/openapi/fileGenerator.ts @@ -1,9 +1,10 @@ +import { SingleBar } from "cli-progress"; import fs from "fs"; import Handlebars from "handlebars"; import { camelCase, groupBy } from "lodash"; import path from "path"; import { Project } from "ts-morph"; -import { getRelativePath, pascalCase } from "../helpers"; +import { pascalCase } from "../helpers"; import { createProgressBar } from "../progressBar"; import Endpoint from "./models/endpoint"; import Enum from "./models/enum"; @@ -12,6 +13,7 @@ import ObjectEntity from "./models/objectEntity"; import TypeReference from "./models/typeReference"; import UnionEntity from "./models/unionEntity"; import { IConfig, IGeneratorParams } from "./types"; +import { patternMath } from "./utils"; import EnumWriter from "./writers/enumWriter"; import ObjectEntityWriter from "./writers/objectEntityWriter"; import RepositoryWriter from "./writers/repositoryWriter"; @@ -42,6 +44,42 @@ export default class FileGenerator { const saveSteps = Math.ceil(items.length * 0.1 + 1); progress.start(1 + items.length + saveSteps, 0); + Handlebars.registerPartial("generatedEntityHeader", await this.readTemplate("generatedEntityHeader")); + + const entitiesPath = this.config.entitiesPath; + const groups = groupBy(items, item => { + const name = item.getTypeName() ?? ""; + + if (typeof entitiesPath === "object") { + for (const path in entitiesPath) { + if (entitiesPath.hasOwnProperty(path)) { + const pattern = entitiesPath[path]; + const include = patternMath(pattern, name); + + if (include) { + return path; + } + } + } + return this.config.defaultEntitiesPath; + } else { + return entitiesPath; + } + }); + + progress.increment(1); + + for (const path in groups) { + await this.generateEntityGroup(groups[path], path, progress); + } + + await this.project.save(); + progress.increment(saveSteps); + + progress.stop(); + } + + async generateEntityGroup(items: TypeReference[], path: string, progress: SingleBar) { const templates = { enumEntity: await this.readTemplate("enumEntity"), enumEntityFile: await this.readTemplate("enumEntityFile"), @@ -52,21 +90,13 @@ export default class FileGenerator { unionEntity: await this.readTemplate("unionEntity"), unionEntityFile: await this.readTemplate("unionEntityFile"), }; + const directory = this.project.createDirectory(path); - Handlebars.registerPartial("generatedEntityHeader", await this.readTemplate("generatedEntityHeader")); - - const directory = this.project.createDirectory(this.config.entitiesPath); - let enumDirectory = directory; - if (this.config.enumsPath) { - enumDirectory = this.project.createDirectory(this.config.enumsPath); - } const enumWriter = - this.config.enums === "enum" ? new EnumWriter(enumDirectory, templates) : new StringLiteralWriter(enumDirectory, templates); + this.config.enums === "enum" ? new EnumWriter(directory, templates) : new StringLiteralWriter(directory, templates); const objectWriter = new ObjectEntityWriter(directory, this.config, templates); const unionWriter = new UnionEntityWriter(directory, templates); - progress.increment(1); - for (const { type } of items) { if (type instanceof Enum) { enumWriter.write(type); @@ -82,11 +112,6 @@ export default class FileGenerator { progress.increment(); } - - await this.project.save(); - progress.increment(saveSteps); - - progress.stop(); } async generateRepositories(endpoints: Endpoint[]) { @@ -106,13 +131,7 @@ export default class FileGenerator { }; const directory = this.project.createDirectory(this.config.repositoriesPath); - const writer = new RepositoryWriter( - directory, - { - entitiesRelativePath: getRelativePath(this.config.repositoriesPath, this.config.entitiesPath), - }, - templates - ); + const writer = new RepositoryWriter(directory, this.config, templates); progress.increment(1); diff --git a/packages/generator/src/openapi/index.ts b/packages/generator/src/openapi/index.ts index 7e5ab7f4..bf01e75a 100644 --- a/packages/generator/src/openapi/index.ts +++ b/packages/generator/src/openapi/index.ts @@ -7,6 +7,21 @@ import ModelProcessor from "./modelProcessor"; import { IConfig, IGeneratorParams } from "./types"; export default class OpenApiGenerator extends GeneratorBase { + async init(): Promise { + await super.init(); + const entitiesPath = this.config.entitiesPath; + + // Create RegExp from "//" strings + if (typeof entitiesPath === "object") { + for (const path in entitiesPath) { + const pattern = entitiesPath[path] as string; + if (pattern.startsWith("/") && pattern.endsWith("/")) { + entitiesPath[path] = new RegExp(pattern.slice(0, pattern.length - 1).slice(1)); + } + } + } + } + async run() { if (!this.config.api) { console.warn("Api definition is missing"); diff --git a/packages/generator/src/openapi/types.ts b/packages/generator/src/openapi/types.ts index 753052bf..c2feb73e 100644 --- a/packages/generator/src/openapi/types.ts +++ b/packages/generator/src/openapi/types.ts @@ -22,6 +22,10 @@ export type ValidationConfig = filter?: string; // regex matched against the rule param }; +export interface IPathConfig { + [key: string]: string | RegExp; +} + export interface IConfig { api: string; observable?: ObservableConfig; @@ -29,9 +33,9 @@ export interface IConfig { dates?: "native" | "date-fns"; validations?: Record; - entitiesPath: string; + entitiesPath: string | IPathConfig; + defaultEntitiesPath?: string; repositoriesPath: string; - enumsPath?: string; validation?: boolean; conversion?: boolean; diff --git a/packages/generator/src/openapi/utils.ts b/packages/generator/src/openapi/utils.ts new file mode 100644 index 00000000..4830aaec --- /dev/null +++ b/packages/generator/src/openapi/utils.ts @@ -0,0 +1,28 @@ +import { IPathConfig } from "./types"; + +export function patternMath(pattern: string | RegExp, name: string): boolean { + if (typeof pattern === "string" && pattern === name) { + return true; + } else if (pattern instanceof RegExp && pattern.test(name)) { + return true; + } else { + return false; + } +} + +export function getPath(pathConfig: IPathConfig | string, name: string, defaultPath?: string) { + let finalPath; + + if (typeof pathConfig === "string") { + finalPath = pathConfig; + } else { + for (const path in pathConfig) { + const pattern = pathConfig[path]; + if (patternMath(pattern, name)) { + finalPath = path; + } + } + } + + return finalPath ?? defaultPath ?? "./"; +} diff --git a/packages/generator/src/openapi/writers/objectEntityWriter.ts b/packages/generator/src/openapi/writers/objectEntityWriter.ts index fada893c..447c5875 100644 --- a/packages/generator/src/openapi/writers/objectEntityWriter.ts +++ b/packages/generator/src/openapi/writers/objectEntityWriter.ts @@ -1,8 +1,8 @@ import camelCase from "lodash/camelCase"; import uniq from "lodash/uniq"; -import path from "path"; import { Directory, SourceFile } from "ts-morph"; import GeneratorBase from "../../generatorBase"; +import { getRelativePath } from "../../helpers"; import ObservableFormatter from "../formatters/observableFormatter"; import AliasEntity from "../models/aliasEntity"; import EntityProperty from "../models/entityProperty"; @@ -11,11 +11,12 @@ import ObjectEntity from "../models/objectEntity"; import Restriction from "../models/restriction"; import TypeReference from "../models/typeReference"; import { IConfig, ValidationConfig } from "../types"; +import { getPath } from "../utils"; export default class ObjectEntityWriter { constructor( private parentDirectory: Directory, - private config: Partial, + private config: IConfig, private templates: Record<"objectEntityContent" | "objectEntityFile", Handlebars.TemplateDelegate> ) {} @@ -48,43 +49,25 @@ export default class ObjectEntityWriter { private createFile(fileName: string, definition: ObjectEntity, baseClass: ObjectEntity | undefined) { const decoratorImports = this.getPropertyDecoratorsImports(definition.properties); - const propertiesToImport = definition.properties.filter(x => x.type.isImportRequired); - - interface SplitImports { - enumsToImport: Array; - entitiesToImport: Array; - } - - const { entitiesToImport, enumsToImport } = propertiesToImport.reduce( - (accumulator: SplitImports, property) => { - const typeName = property.type.getTypeName(); - if (typeName) { - if (property.type.type instanceof Enum) { - accumulator.enumsToImport.push(typeName); - } else { - accumulator.entitiesToImport.push(typeName); - } - } - return accumulator; - }, - { entitiesToImport: [], enumsToImport: [] } - ); - + const entitiesToImport: Array = definition.properties.filter(x => x.type.isImportRequired); if (baseClass) { - entitiesToImport.push(baseClass.name); + entitiesToImport.push(baseClass); } - const entityImports = uniq(entitiesToImport) - .sort() - .map((x: string) => `import ${x} from "./${camelCase(x)}";`); + const entitiesImports = entitiesToImport.sort().map(x => { + let name; + if (x instanceof EntityProperty) { + name = x.type.getTypeName() ?? x.name; + } else { + name = x.name; + } + const path = this.getImportPath(x, definition); - const pathToEnums = this.getPathToEnums(); - const enumsImports = uniq(enumsToImport) - .sort() - .map((x: string) => `import ${x} from "${pathToEnums}/${camelCase(x)}";`); + return `import ${name} from "${path}/${camelCase(name)}";`; + }); const result = this.templates.objectEntityFile({ - imports: [...decoratorImports, ...entityImports, ...enumsImports], + imports: [...decoratorImports, ...uniq(entitiesImports)], content: () => this.getEntityContent(definition, baseClass), entity: definition, baseClass, @@ -93,23 +76,24 @@ export default class ObjectEntityWriter { return this.parentDirectory.createSourceFile(fileName, result, { overwrite: true }); } - getPathToEnums() { - function convertPath(windowsPath: string) { - return windowsPath - .replace(/^\\\\\?\\/, "") - .replace(/\\/g, "/") - .replace(/\/\/+/g, "/"); + getImportPath(targetEntity: EntityProperty | ObjectEntity, sourceEntity: ObjectEntity) { + let targetEntityName; + if (targetEntity instanceof EntityProperty) { + targetEntityName = targetEntity.type.getTypeName() ?? targetEntity.name; + } else { + targetEntityName = targetEntity.name; } - if (this.config.enumsPath && this.config.entitiesPath) { - const relativePath = convertPath(path.relative(this.config.entitiesPath, this.config.enumsPath)); - if (!relativePath.startsWith(".")) { - return `./${relativePath}`; - } - return relativePath; + const targetPath = getPath(this.config.entitiesPath, targetEntityName, this.config.defaultEntitiesPath); + const sourcePath = getPath(this.config.entitiesPath, sourceEntity.name, this.config.defaultEntitiesPath); + + const path = getRelativePath(sourcePath, targetPath); + + if (path.endsWith("/")) { + return path.slice(0, path.length - 1); } - return "."; + return path; } getPropertyDecoratorsImports(properties: EntityProperty[]) { diff --git a/packages/generator/src/openapi/writers/repositoryWriter.ts b/packages/generator/src/openapi/writers/repositoryWriter.ts index 624b448b..a0336d7c 100644 --- a/packages/generator/src/openapi/writers/repositoryWriter.ts +++ b/packages/generator/src/openapi/writers/repositoryWriter.ts @@ -3,18 +3,16 @@ import camelCase from "lodash/camelCase"; import uniq from "lodash/uniq"; import { Directory, SourceFile } from "ts-morph"; import GeneratorBase from "../../generatorBase"; -import { pascalCase } from "../../helpers"; +import { getRelativePath, pascalCase } from "../../helpers"; import Endpoint from "../models/endpoint"; import TypeReference from "../models/typeReference"; - -export interface RepositoryWriterConfig { - entitiesRelativePath: string; -} +import { IConfig } from "../types"; +import { getPath } from "../utils"; export default class RepositoryWriter { constructor( private parentDirectory: Directory, - private config: RepositoryWriterConfig, + private config: IConfig, private templates: Record<"repositoryAction" | "repositoryFile", Handlebars.TemplateDelegate> ) {} @@ -62,8 +60,17 @@ export default class RepositoryWriter { .flatMap(action => [action.queryParam, action.requestBody?.typeReference, getMainResponse(action)?.typeReference]) .filter((x): x is TypeReference => !!x && x.isImportRequired) ).map(entity => { - const name = entity.getTypeName(); - return `import ${name} from "${this.config.entitiesRelativePath}/${camelCase(name)}";`; + const name = entity.getTypeName() ?? ""; + const entitiesPath = this.config.entitiesPath; + let entityPath: string; + + if (typeof entitiesPath === "object") { + entityPath = getPath(entitiesPath, name, this.config.defaultEntitiesPath); + } else { + entityPath = entitiesPath; + } + + return `import ${name} from "${getRelativePath(this.config.repositoriesPath, entityPath)}/${camelCase(name)}";`; }); }