Skip to content

Commit

Permalink
eManPrague#41 Works on advanced entities path config
Browse files Browse the repository at this point in the history
  • Loading branch information
Michal Kopecký committed Dec 23, 2021
1 parent 2928faa commit 1208a42
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 81 deletions.
8 changes: 6 additions & 2 deletions packages/generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -207,6 +207,10 @@ interface HasExclude {
exclude?: string[];
}

export interface IPathConfig {
[key: string]: string | RegExp; // Provide RegExp decoded as "//" string
}

export type ObservableConfig =
| boolean
| {
Expand Down
1 change: 1 addition & 0 deletions packages/generator/src/openapi/defaultConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"api": "https://fruits-demo.herokuapp.com/api/swagger-json",

"entitiesPath": "src/entities",
"defaultEntitiesPath": "src/entities",
"repositoriesPath": "src/repositories",

"validation": true,
Expand Down
65 changes: 42 additions & 23 deletions packages/generator/src/openapi/fileGenerator.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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"),
Expand All @@ -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);
Expand All @@ -82,11 +112,6 @@ export default class FileGenerator {

progress.increment();
}

await this.project.save();
progress.increment(saveSteps);

progress.stop();
}

async generateRepositories(endpoints: Endpoint[]) {
Expand All @@ -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);

Expand Down
15 changes: 15 additions & 0 deletions packages/generator/src/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ import ModelProcessor from "./modelProcessor";
import { IConfig, IGeneratorParams } from "./types";

export default class OpenApiGenerator extends GeneratorBase<IGeneratorParams, IConfig> {
async init(): Promise<void> {
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");
Expand Down
8 changes: 6 additions & 2 deletions packages/generator/src/openapi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,20 @@ 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;
enums?: "enum" | "string";
dates?: "native" | "date-fns";
validations?: Record<string, ValidationConfig>;

entitiesPath: string;
entitiesPath: string | IPathConfig;
defaultEntitiesPath?: string;
repositoriesPath: string;
enumsPath?: string;

validation?: boolean;
conversion?: boolean;
Expand Down
28 changes: 28 additions & 0 deletions packages/generator/src/openapi/utils.ts
Original file line number Diff line number Diff line change
@@ -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 ?? "./";
}
76 changes: 30 additions & 46 deletions packages/generator/src/openapi/writers/objectEntityWriter.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<IConfig>,
private config: IConfig,
private templates: Record<"objectEntityContent" | "objectEntityFile", Handlebars.TemplateDelegate>
) {}

Expand Down Expand Up @@ -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<string>;
entitiesToImport: Array<string>;
}

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<EntityProperty | ObjectEntity> = 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,
Expand All @@ -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[]) {
Expand Down
23 changes: 15 additions & 8 deletions packages/generator/src/openapi/writers/repositoryWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
) {}

Expand Down Expand Up @@ -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)}";`;
});
}

Expand Down

0 comments on commit 1208a42

Please sign in to comment.