Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/bootstrap/src/controls/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as React from "react";
import { Form, FormControlProps } from "react-bootstrap";
import { ValidationControlBase } from "./validationControlBase";

type FormControlElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;

export interface InputProps {
onBlur?: (e: React.FormEvent<any>) => void;
onFocus?: (e: React.FormEvent<any>) => void;
Expand All @@ -25,7 +27,7 @@ export function formatValueForControl(value: any) {

export class Input<TTarget, TOtherProps = unknown> extends ValidationControlBase<
TTarget,
InputProps & FormControlProps & TOtherProps
InputProps & React.InputHTMLAttributes<FormControlElement> & FormControlProps & TOtherProps
> {
@bound
protected renderInner() {
Expand Down
6 changes: 6 additions & 0 deletions packages/generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | IPathConfig;
defaultEntitiesPath?: string; // Used only when IPathConfig is used for entitiesPath
observable?: ObservableConfig;
enums?: "enum" | "string";
dates?: "native" | "date-fns";
Expand All @@ -205,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
59 changes: 41 additions & 18 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,17 +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);
const enumWriter =
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 @@ -78,11 +112,6 @@ export default class FileGenerator {

progress.increment();
}

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

progress.stop();
}

async generateRepositories(endpoints: Endpoint[]) {
Expand All @@ -102,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
7 changes: 6 additions & 1 deletion packages/generator/src/openapi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,19 @@ 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;

validation?: 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 ?? "./";
}
45 changes: 37 additions & 8 deletions packages/generator/src/openapi/writers/objectEntityWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import camelCase from "lodash/camelCase";
import uniq from "lodash/uniq";
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 @@ -10,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 @@ -47,18 +49,25 @@ 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 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 => `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);

return `import ${name} from "${path}/${camelCase(name)}";`;
});

const result = this.templates.objectEntityFile({
imports: [...decoratorImports, ...entityImports],
imports: [...decoratorImports, ...uniq(entitiesImports)],
content: () => this.getEntityContent(definition, baseClass),
entity: definition,
baseClass,
Expand All @@ -67,6 +76,26 @@ export default class ObjectEntityWriter {
return this.parentDirectory.createSourceFile(fileName, result, { overwrite: true });
}

getImportPath(targetEntity: EntityProperty | ObjectEntity, sourceEntity: ObjectEntity) {
let targetEntityName;
if (targetEntity instanceof EntityProperty) {
targetEntityName = targetEntity.type.getTypeName() ?? targetEntity.name;
} else {
targetEntityName = targetEntity.name;
}

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 path;
}

getPropertyDecoratorsImports(properties: EntityProperty[]) {
const result = new Set<string>();

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