From d58066cfd20045d751c0aa23620f30e025f0e97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kopeck=C3=BD?= Date: Tue, 21 Dec 2021 09:58:27 +0100 Subject: [PATCH 1/5] #56 Add prop types for input --- packages/bootstrap/src/controls/input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bootstrap/src/controls/input.tsx b/packages/bootstrap/src/controls/input.tsx index 288f857b..b8b8fc31 100644 --- a/packages/bootstrap/src/controls/input.tsx +++ b/packages/bootstrap/src/controls/input.tsx @@ -25,7 +25,7 @@ export function formatValueForControl(value: any) { export class Input extends ValidationControlBase< TTarget, - InputProps & FormControlProps & TOtherProps + InputProps & React.InputHTMLAttributes & FormControlProps & TOtherProps > { @bound protected renderInner() { From c89b913d46bcbf71c900dadd1714c733c4c9b55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kopeck=C3=BD?= Date: Tue, 21 Dec 2021 10:48:25 +0100 Subject: [PATCH 2/5] #56 Better support for FormControl props --- packages/bootstrap/src/controls/input.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bootstrap/src/controls/input.tsx b/packages/bootstrap/src/controls/input.tsx index b8b8fc31..fa7369f0 100644 --- a/packages/bootstrap/src/controls/input.tsx +++ b/packages/bootstrap/src/controls/input.tsx @@ -1,6 +1,6 @@ import { bound } from "@frui.ts/helpers"; import * as React from "react"; -import { Form, FormControlProps } from "react-bootstrap"; +import { Form, FormControl } from "react-bootstrap"; import { ValidationControlBase } from "./validationControlBase"; export interface InputProps { @@ -25,7 +25,7 @@ export function formatValueForControl(value: any) { export class Input extends ValidationControlBase< TTarget, - InputProps & React.InputHTMLAttributes & FormControlProps & TOtherProps + InputProps & React.PropsWithRef & TOtherProps > { @bound protected renderInner() { From 7b3c990457f497f45f2697b25d9e46b32b01e8c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kopeck=C3=BD?= Date: Tue, 21 Dec 2021 11:25:06 +0100 Subject: [PATCH 3/5] #56 Final solution --- packages/bootstrap/src/controls/input.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/bootstrap/src/controls/input.tsx b/packages/bootstrap/src/controls/input.tsx index fa7369f0..5038b5c3 100644 --- a/packages/bootstrap/src/controls/input.tsx +++ b/packages/bootstrap/src/controls/input.tsx @@ -1,8 +1,10 @@ import { bound } from "@frui.ts/helpers"; import * as React from "react"; -import { Form, FormControl } from "react-bootstrap"; +import { Form, FormControlProps } from "react-bootstrap"; import { ValidationControlBase } from "./validationControlBase"; +type FormControlElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; + export interface InputProps { onBlur?: (e: React.FormEvent) => void; onFocus?: (e: React.FormEvent) => void; @@ -25,7 +27,7 @@ export function formatValueForControl(value: any) { export class Input extends ValidationControlBase< TTarget, - InputProps & React.PropsWithRef & TOtherProps + InputProps & React.InputHTMLAttributes & FormControlProps & TOtherProps > { @bound protected renderInner() { From 2928faac644b40ea73d968c5c3b873050dd744e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kopeck=C3=BD?= Date: Thu, 29 Jul 2021 13:46:36 +0200 Subject: [PATCH 4/5] #41 Allow to specify custom folder for Enums --- packages/generator/README.md | 2 + .../generator/src/openapi/fileGenerator.ts | 6 ++- packages/generator/src/openapi/types.ts | 1 + .../src/openapi/writers/objectEntityWriter.ts | 51 +++++++++++++++++-- 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/generator/README.md b/packages/generator/README.md index 387bc2d9..b5da59c2 100644 --- a/packages/generator/README.md +++ b/packages/generator/README.md @@ -193,6 +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; observable?: ObservableConfig; enums?: "enum" | "string"; dates?: "native" | "date-fns"; diff --git a/packages/generator/src/openapi/fileGenerator.ts b/packages/generator/src/openapi/fileGenerator.ts index 7d7b6177..b1088ca5 100644 --- a/packages/generator/src/openapi/fileGenerator.ts +++ b/packages/generator/src/openapi/fileGenerator.ts @@ -56,8 +56,12 @@ export default class FileGenerator { 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(directory, templates) : new StringLiteralWriter(directory, templates); + this.config.enums === "enum" ? new EnumWriter(enumDirectory, templates) : new StringLiteralWriter(enumDirectory, templates); const objectWriter = new ObjectEntityWriter(directory, this.config, templates); const unionWriter = new UnionEntityWriter(directory, templates); diff --git a/packages/generator/src/openapi/types.ts b/packages/generator/src/openapi/types.ts index d51d06bf..753052bf 100644 --- a/packages/generator/src/openapi/types.ts +++ b/packages/generator/src/openapi/types.ts @@ -31,6 +31,7 @@ export interface IConfig { entitiesPath: string; repositoriesPath: string; + enumsPath?: string; validation?: boolean; conversion?: boolean; diff --git a/packages/generator/src/openapi/writers/objectEntityWriter.ts b/packages/generator/src/openapi/writers/objectEntityWriter.ts index 9470e952..fada893c 100644 --- a/packages/generator/src/openapi/writers/objectEntityWriter.ts +++ b/packages/generator/src/openapi/writers/objectEntityWriter.ts @@ -1,5 +1,6 @@ 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 ObservableFormatter from "../formatters/observableFormatter"; @@ -47,7 +48,27 @@ export default class ObjectEntityWriter { private createFile(fileName: string, definition: ObjectEntity, baseClass: ObjectEntity | undefined) { const decoratorImports = this.getPropertyDecoratorsImports(definition.properties); - const entitiesToImport = definition.properties.filter(x => x.type.isImportRequired).map(x => x.type.getTypeName()); + 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: [] } + ); if (baseClass) { entitiesToImport.push(baseClass.name); @@ -55,10 +76,15 @@ export default class ObjectEntityWriter { const entityImports = uniq(entitiesToImport) .sort() - .map(x => `import ${x} from "./${camelCase(x)}";`); + .map((x: string) => `import ${x} from "./${camelCase(x)}";`); + + const pathToEnums = this.getPathToEnums(); + const enumsImports = uniq(enumsToImport) + .sort() + .map((x: string) => `import ${x} from "${pathToEnums}/${camelCase(x)}";`); const result = this.templates.objectEntityFile({ - imports: [...decoratorImports, ...entityImports], + imports: [...decoratorImports, ...entityImports, ...enumsImports], content: () => this.getEntityContent(definition, baseClass), entity: definition, baseClass, @@ -67,6 +93,25 @@ export default class ObjectEntityWriter { return this.parentDirectory.createSourceFile(fileName, result, { overwrite: true }); } + getPathToEnums() { + function convertPath(windowsPath: string) { + return windowsPath + .replace(/^\\\\\?\\/, "") + .replace(/\\/g, "/") + .replace(/\/\/+/g, "/"); + } + + 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; + } + + return "."; + } + getPropertyDecoratorsImports(properties: EntityProperty[]) { const result = new Set(); From 1208a42ddc642432da815d2d6cd12c1c825bbca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kopeck=C3=BD?= Date: Thu, 23 Dec 2021 14:56:37 +0100 Subject: [PATCH 5/5] #41 Works on advanced entities path config --- packages/generator/README.md | 8 +- .../generator/src/openapi/defaultConfig.json | 1 + .../generator/src/openapi/fileGenerator.ts | 65 ++++++++++------ packages/generator/src/openapi/index.ts | 15 ++++ packages/generator/src/openapi/types.ts | 8 +- packages/generator/src/openapi/utils.ts | 28 +++++++ .../src/openapi/writers/objectEntityWriter.ts | 76 ++++++++----------- .../src/openapi/writers/repositoryWriter.ts | 23 ++++-- 8 files changed, 143 insertions(+), 81 deletions(-) create mode 100644 packages/generator/src/openapi/utils.ts 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)}";`; }); }