Skip to content

Commit

Permalink
Repository generator uses Handlebars template
Browse files Browse the repository at this point in the history
  • Loading branch information
gius committed Apr 17, 2021
1 parent a2c42f9 commit a2cc61b
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 117 deletions.
2 changes: 2 additions & 0 deletions packages/generator/src/openapi/defaultConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"objectEntityContent": "@objectEntityContent.hbs",
"objectEntityFile": "@objectEntityFile.hbs",
"generatedEntityHeader": "@generatedEntityHeader.hbs",
"repositoryAction": "@repositoryAction.hbs",
"repositoryFile": "@repositoryFile.hbs",
"stringLiteralEntity": "@stringLiteralEntity.hbs",
"stringLiteralEntityFile": "@stringLiteralEntityFile.hbs",
"unionEntity": "@unionEntity.hbs",
Expand Down
16 changes: 12 additions & 4 deletions packages/generator/src/openapi/fileGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,19 @@ export default class FileGenerator {
const saveSteps = Math.ceil(repositories.length * 0.1 + 1);
progress.start(1 + repositories.length + saveSteps, 0);

const directory = this.project.createDirectory(this.config.repositoriesPath);
const templates = {
repositoryAction: await this.readTemplate("repositoryAction"),
repositoryFile: await this.readTemplate("repositoryFile"),
};

const writer = new RepositoryWriter(directory, {
entitiesRelativePath: getRelativePath(this.config.repositoriesPath, this.config.entitiesPath),
});
const directory = this.project.createDirectory(this.config.repositoriesPath);
const writer = new RepositoryWriter(
directory,
{
entitiesRelativePath: getRelativePath(this.config.repositoriesPath, this.config.entitiesPath),
},
templates
);

progress.increment(1);

Expand Down
2 changes: 1 addition & 1 deletion packages/generator/src/openapi/parsers/openApi3Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export default class OpenApi3Parser implements ApiModel {
endpoint.path = path;
endpoint.method = method;
endpoint.repository = action.tags?.[0] ?? "General";
endpoint.description = action.description ?? action.summary;
endpoint.description = action.description?.trim() ?? action.summary?.trim();

const parameters =
action.parameters?.filter(
Expand Down
43 changes: 43 additions & 0 deletions packages/generator/src/openapi/templates/repositoryAction.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{{#*inline "get"}}
return this.callApi(api => api.path(`{{{path}}}`)
{{~#if isResponseArray~}}
.getEntities({{responseType}}{{#if action.queryParam}}, query{{/if}})
{{~else~}}
.getEntity({{responseType}}{{#if action.queryParam}}, query{{/if}})
{{~/if~}}
);
{{/inline}}
{{#*inline "post"}}
// TODO {{action.method}} {{path}}
return this.callApi(api => api.path(`{{{path}}}`).postEntity(payload{{#if response}}, {{responseTypeDeclaration}}{{/if}}));
{{/inline}}
{{#*inline "put"}}
// TODO {{action.method}} {{path}}
return this.callApi(api => api.path(`{{{path}}}`).putEntity(payload{{#if response}}, {{responseTypeDeclaration}}{{/if}}));
{{/inline}}
{{#*inline "patch"}}
// TODO {{action.method}} {{path}}
return this.callApi(api => api.path(`{{{path}}}`).patchEntity(payload{{#if response}}, {{responseTypeDeclaration}}{{/if}}));
{{/inline}}
{{#*inline "delete"}}
// TODO {{action.method}} {{path}}
return this.callApi(api => api.path(`{{{path}}}`).delete({{#if action.requestBody}}payload{{/if}}));
{{/inline}}
{{#*inline "option"}}
// TODO {{action.method}} {{path}}
{{/inline}}
{{#*inline "head"}}
// TODO {{action.method}} {{path}}
{{/inline}}
{{#*inline "trace"}}
// TODO {{action.method}} {{path}}
{{/inline}}
{{#if action.description}}
/** {{action.description}} */
{{/if}}
{{camelCase action.name}}({{#each parameters}}{{{this}}}{{#unless @last}}, {{/unless}}{{/each}}) {
{{> (lookup action "method")}}
{{#each responses}}
// HTTP {{statusCode}}: {{returnType}}{{#unless returnType}}EMPTY{{/unless}}
{{/each}}
}
13 changes: 13 additions & 0 deletions packages/generator/src/openapi/templates/repositoryFile.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import RepositoryBase from "./repositoryBase";
{{#each imports}}
{{{this}}}
{{/each}}

export default class {{repositoryName}} extends RepositoryBase {
{{#each actions}}
{{{this}}}
{{#unless @last}}

{{/unless}}
{{/each}}
}
159 changes: 47 additions & 112 deletions packages/generator/src/openapi/writers/repositoryWriter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/* eslint-disable @typescript-eslint/tslint/config */
import camelCase from "lodash/camelCase";
import uniq from "lodash/uniq";
import { ClassDeclaration, CodeBlockWriter, Directory, SourceFile } from "ts-morph";
import { CodeBlockWriter, Directory, SourceFile } from "ts-morph";
import GeneratorBase from "../../generatorBase";
import { pascalCase } from "../../helpers";
import { entityGeneratedHeader } from "../../messages.json";
import Endpoint from "../models/endpoint";
import TypeReference from "../models/typeReference";

Expand All @@ -13,7 +12,11 @@ export interface RepositoryWriterConfig {
}

export default class RepositoryWriter {
constructor(private parentDirectory: Directory, private config: RepositoryWriterConfig) {}
constructor(
private parentDirectory: Directory,
private config: RepositoryWriterConfig,
private templates: Record<"repositoryAction" | "repositoryFile", Handlebars.TemplateDelegate>
) {}

write(repositoryBaseName: string, actions: Endpoint[]) {
const repositoryName = `${pascalCase(repositoryBaseName)}Repository`;
Expand All @@ -24,77 +27,67 @@ export default class RepositoryWriter {
}

const existingFile = this.parentDirectory.getSourceFile(fileName);
const actualFile = existingFile
return existingFile
? this.updateFile(existingFile, repositoryName, actions)
: this.createFile(fileName, repositoryName, actions);

actualFile.formatText();
return actualFile;
}

private updateFile(file: SourceFile, repositoryName: string, actions: Endpoint[]) {
const currentClass = file.getClassOrThrow(pascalCase(repositoryName));
this.updateClass(currentClass, actions);

return file;
}

private createFile(fileName: string, repositoryName: string, actions: Endpoint[]) {
return this.parentDirectory.createSourceFile(
fileName,
writer => {
writer.writeLine(entityGeneratedHeader);
writer.writeLine(`import RepositoryBase from "./repositoryBase";`);
this.writeEntityImports(writer, actions);

writer.blankLineIfLastNot();
writer.writeLine(`export default class ${repositoryName} extends RepositoryBase {`);

for (const action of actions) {
this.writeAction(writer, action);
}

writer.writeLine("}");
},
{ overwrite: true }
);
}
const repository = file.getClassOrThrow(pascalCase(repositoryName));

private updateClass(repository: ClassDeclaration, actions: Endpoint[]) {
for (const action of actions) {
const method = repository.getMethod(camelCase(action.name));

if (!method) {
repository.addMember(writer => {
this.writeAction(writer, action);
});
repository.addMember(this.getActionContent(action));
}
}

return file;
}

private writeEntityImports(writer: CodeBlockWriter, actions: Endpoint[]) {
const entitiesToImport = uniq(
private createFile(fileName: string, repositoryName: string, actions: Endpoint[]) {
const result = this.templates.repositoryFile({
repositoryName,
imports: this.getRequiredImports(actions),
actions: () => actions.map(x => this.getActionContent(x)),
});

return this.parentDirectory.createSourceFile(fileName, result, { overwrite: true });
}

private getRequiredImports(actions: Endpoint[]) {
return uniq(
actions
.flatMap(action => [action.queryParam, action.requestBody?.typeReference, getMainResponse(action)?.typeReference])
.filter((x): x is TypeReference => !!x && x.isImportRequired)
);

entitiesToImport.forEach(entity => {
).map(entity => {
const name = entity.getTypeName();
writer.writeLine(`import ${name} from "${this.config.entitiesRelativePath}/${camelCase(name)}";`);
return `import ${name} from "${this.config.entitiesRelativePath}/${camelCase(name)}";`;
});
}

private writeAction(writer: CodeBlockWriter, action: Endpoint) {
writer.conditionalWriteLine(!!action.description, `/** ${action.description} */`);
writer.write(camelCase(action.name));
writer.write("(");

const parameters = Array.from(this.getMethodParameters(action));
writer.write(parameters.join(", "));

writer.write(")");
writer.block(() => this.writeMethodBody(writer, action));
private getActionContent(action: Endpoint) {
const mainResponse = getMainResponse(action);

const context = {
action,
path: getPathStringWithPlaceholders(action),
parameters: this.getMethodParameters(action),
requestContentType: action.requestBody?.contentType,
response: mainResponse,
isResponseArray: mainResponse?.typeReference.isArray,
responseType: mainResponse?.typeReference.getTypeName(),
responseTypeDeclaration: mainResponse?.typeReference.getTypeDeclaration(),
responses:
action.responses &&
Object.entries(action.responses).map(([statusCode, resultType]) => ({
statusCode,
returnType: resultType?.typeReference.getTypeDeclaration(),
})),
};

return this.templates.repositoryAction(context);
}

private *getMethodParameters(action: Endpoint) {
Expand All @@ -110,64 +103,6 @@ export default class RepositoryWriter {
yield `payload: ${action.requestBody.typeReference.getTypeDeclaration()}`;
}
}

private writeMethodBody(writer: CodeBlockWriter, action: Endpoint) {
const response = getMainResponse(action);

switch (action.method) {
case "get":
// const isPaged = isPagedListAction(action);

this.writeApiCall(writer, action, () => {
if (response) {
const queryParameterText = action.queryParam ? ", query" : "";
if (response.typeReference.isArray) {
writer.write(`.getEntities(${response.typeReference.getTypeName()}${queryParameterText})`);
} else {
writer.write(`.getEntity(${response.typeReference.getTypeName()}${queryParameterText})`);
}
}
});
break;

case "post":
case "put":
case "patch":
const isPayloadFormsData = action.requestBody?.contentType === "multipart/form-data";
if (isPayloadFormsData) {
writer.writeLine(`entityToFormData(payload);`);
}

this.writeApiCall(writer, action, () => {
if (action.requestBody) {
writer.write(`.${action.method}${isPayloadFormsData ? "Data" : "Entity"}(payload`);
writer.conditionalWrite(!!response, `, ${response?.typeReference.getTypeDeclaration()}`);
writer.write(`)`);
} else {
writer.write(`.${action.method}({})`);
}
});
break;

default:
writer.writeLine(`// ${action.method} ${action.path}`);
break;
}

if (action.responses) {
for (const [result, resultType] of Object.entries(action.responses)) {
writer.writeLine(`// ${result}: ${resultType?.typeReference.getTypeDeclaration()}`);
}
}
}

private writeApiCall(writer: CodeBlockWriter, action: Endpoint, additionalContent: () => void) {
const path = getPathStringWithPlaceholders(action);
writer.write(`return this.callApi(api => api.path(${path})`);
additionalContent();
writer.write(`);`);
writer.newLine();
}
}

// function isPagedListAction(action: Endpoint) {
Expand Down Expand Up @@ -198,5 +133,5 @@ function getPathStringWithPlaceholders(action: Endpoint) {
path = path.replace(`{${param.externalName || param.name}}`, "${" + param.name + "}");
}

return "`" + path + "`";
return path;
}

0 comments on commit a2cc61b

Please sign in to comment.