Skip to content

Commit

Permalink
feat(type-safe-api): improve performance of markdown, plantuml and ht…
Browse files Browse the repository at this point in the history
…ml-redoc docs generation (#836)

Remove openapi generator for markdown and plantuml in favour of our own templates with the new
codegen.

Generate html redoc docs by calling `@redocly/cli` directly with `npx` rather than using the
wrapper script.
  • Loading branch information
cogwirrel authored Oct 4, 2024
1 parent 9c92cca commit 345eb6f
Show file tree
Hide file tree
Showing 20 changed files with 332 additions and 293 deletions.
3 changes: 0 additions & 3 deletions .projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions packages/pdk/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions packages/type-safe-api/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

This file was deleted.

Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
###TSAPI_WRITE_FILE###
{
"id": "readme",
"dir": ".",
"name": "README",
"ext": ".md",
"overwrite": true
}
###/TSAPI_WRITE_FILE#### Documentation for <%- info.title %>

<a name="documentation-for-api-endpoints"></a>
## Documentation for API Endpoints

| Class | Method | HTTP request | Description |
|------------ | ------------- | ------------- | -------------|
<%_ allOperations.forEach(operation => { _%>
| *<%- operation.service %>Api* | [**<%- operation.name %>**](Apis/<%- operation.service %>Api.md#<%- operation.name %>) | **<%- operation.method %>** <%- operation.path %> | <%- operation.description || '' %> |
<%_ }); _%>

<a name="documentation-for-models"></a>
## Documentation for Models

<%_ models.forEach(model => { _%>
- [<%- model.name %>](./Models/<%- model.name %>.md)
<%_ }); _%>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<%_ services.forEach(service => { _%>
###TSAPI_WRITE_FILE###
{
"id": "service-<%- service.name %>",
"dir": "Apis",
"name": "<%- service.className %>",
"ext": ".md",
"overwrite": true
}
###/TSAPI_WRITE_FILE#### <%- service.className %>
<%- service.description || '' %>
| Method | HTTP request | Description |
|------------- | ------------- | -------------|
<%_ service.operations.forEach(operation => { _%>
| [**<%- operation.name %>**](<%- operation.service %>Api.md#<%- operation.name %>) | **<%- operation.method %>** <%- operation.path %> | <%- operation.description || '' %> |
<%_ }); _%>
<%_ service.operations.forEach(operation => { _%>
<a name="<%- operation.name %>"></a>
# **<%- operation.name %>**
<%_ const result = operation.results[0] _%>
> <% if (result && result.type !== 'void') { %><%- result.javaType %> <% } %><%- operation.name %>(<% operation.parameters.forEach((param, i) => { %><%- param.prop %><% if (i < operation.parameters.length - 1) { %>, <% } %><% }); %>)
<%_ if (operation.description) { _%>
<%- operation.description %>
<%_ } _%>
### Parameters
<%_ if (operation.parameters.length === 0) { _%>
This endpoint does not need any parameters.
<%_ } else { _%>
|Name | Type | Description | Notes |
|------------- | ------------- | ------------- | -------------|
<%_ operation.parameters.forEach((param) => { _%>
| **<%- param.prop %>** | <% if (param.isPrimitive || param.export === "array" || param.export === "dictionary") { %>**<%- param.javaType %>**<% } else { %>[**<%- param.type %>**](../Models/<%- param.type %>.md)<% } %>| <%- param.description || '' %> |<% if (!param.isRequired) { %> [optional]<% } %><% if (param.defaultValue) { %> [default to <%- param.defaultValue %>]<% } %><% if (param.enum.length > 0) { %> [enum: <% param.enum.forEach((e, i) => { %><%- e.value %><% if (i < param.enum.length - 1) { %>, <% } %><% }); %><% } %> |
<%_ }); _%>
<%_ } _%>
### Return type
<%_ if (result && result.type !== 'void') { _%>
<% if (result.isPrimitive || result.export === "array" || result.export === "dictionary") { %>**<%- result.javaType %>**<% } else { %>[**<%- result.type %>**](../Models/<%- result.type %>.md)<% } %>
<%_ } else { _%>
null (empty response body)
<%_ } _%>
### HTTP request headers
- **Content-Type**: <% if (operation.parametersBody && operation.parametersBody.mediaTypes && operation.parametersBody.mediaTypes.length > 0) { %><%- operation.parametersBody.mediaTypes.join(', ') %><% } else { %>Not defined<% } %>
- **Accept**: <% if (result && result.mediaTypes && result.mediaTypes.length > 0) { %><%- result.mediaTypes.join(', ') %><% } else { %>Not defined<% } %>
<%_ }); _%>
<%_ }); _%>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<%_ models.forEach(model => { _%>
###TSAPI_WRITE_FILE###
{
"id": "model-<%- model.name %>",
"dir": "Models",
"name": "<%- model.name %>",
"ext": ".md",
"overwrite": true
}
###/TSAPI_WRITE_FILE#### <%- model.name %>
## Properties
| Name | Type | Description | Notes |
|------------ | ------------- | ------------- | -------------|
<%_ model.resolvedProperties.forEach(property => { _%>
| **<%- property.name %>** | <% if (property.isPrimitive || property.export === "array" || property.export === "dictionary") { %>**<%- property.javaType %>**<% } else { %>[**<%- property.type %>**](<%- property.type %>.md)<% } %> | <%- property.description || '' %> | <% if (!property.isRequired) { %>[optional] <% } %><% if (property.isReadOnly) { %>[readonly] <% } %><% if (property.defaultValue) { %>[default to <%- property.defaultValue %>]<% } %> |
<%_ }); _%>
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
<%_ }); _%>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
###TSAPI_WRITE_FILE###
{
"id": "schemas",
"dir": ".",
"name": "schemas",
"ext": ".plantuml",
"overwrite": true
}
###/TSAPI_WRITE_FILE###@startuml

title <%- info.title %> Schemas Diagram

<%_ models.forEach((model) => { _%>
entity <%- model.name %> {
<%_ model.resolvedProperties.forEach(property => { _%>
<% if (property.isRequired) { %>* <% } %><%- property.name %>: <%- property.javaType %>
<%_ }); _%>
}
<%_ }); _%>

<%_ models.forEach((model) => { _%>
<%_ model.resolvedProperties.forEach((property) => { _%>
<%_ if (!property.isPrimitive) { _%>
<%- model.name %> -- <% if (["array", "dictionary"].includes(property.export)) { %>"0..*" <% } %><%- property.type %> : <%- property.name %>
<%_ } _%>
<%_ }); _%>
<%_ }); _%>

@enduml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ interface Arguments {
* Location to write the generated code to
*/
readonly outputPath: string;
/**
* Print the data passed to the ejs templates
*/
readonly printData?: boolean;
}

interface WriteFileConfig {
Expand Down Expand Up @@ -240,12 +244,58 @@ const toTypeScriptType = (property: parseOpenapi.Model): string => {
}
};

const toJavaPrimitive = (property: parseOpenapi.Model): string => {
if (property.type === "string" && ["date", "date-time"].includes(property.format ?? '')) {
return "Date";
} else if (property.type === "binary") {
return "byte[]";
} else if (property.type === "number") {
switch(property.format) {
case "int32":
return "Integer";
case "int64":
return "BigInteger";
case "float":
return "Float";
case "double":
return "Double";
default:
break;
}

if ((property as any).openapiType === "integer") {
return "Integer";
}
return "BigDecimal";
} else if (property.type === "boolean") {
return "Boolean";
} else if (property.type === "string") {
return "String";
}
return property.type;
};

const toJavaType = (property: parseOpenapi.Model): string => {
switch (property.export) {
case "generic":
case "reference":
return toJavaPrimitive(property);
case "array":
return `${property.uniqueItems ? 'Set' : 'List'}<${property.link ? toTypeScriptType(property.link) : property.type}>`;
case "dictionary":
return `Map<String, ${property.link ? toTypeScriptType(property.link) : property.type}>`;
default:
return property.type;
}
};

/**
* Mutates the given model to add language specific types and names
*/
const mutateModelWithAdditionalTypes = (model: parseOpenapi.Model) => {
(model as any).typescriptName = model.name;
(model as any).typescriptType = toTypeScriptType(model);
(model as any).javaType = toJavaType(model);
(model as any).isPrimitive = PRIMITIVE_TYPES.has(model.type);

// Trim any surrounding quotes from name
Expand All @@ -259,6 +309,7 @@ const mutateWithOpenapiSchemaProperties = (spec: OpenAPIV3.Document, model: pars
(model as any).isShort = schema.format === "int32";
(model as any).isLong = schema.format === "int64";
(model as any).deprecated = !!schema.deprecated;
(model as any).openapiType = schema.type;

visited.add(model);

Expand Down Expand Up @@ -414,6 +465,9 @@ const buildData = (inSpec: OpenAPIV3.Document, metadata: any) => {
// When there's no content, we set the type to 'void'
if (!specResponse.content) {
response.type = 'void';
} else {
// Add the response media types
(response as any).mediaTypes = Object.keys(specResponse.content);
}
}
});
Expand Down Expand Up @@ -443,6 +497,7 @@ const buildData = (inSpec: OpenAPIV3.Document, metadata: any) => {
if (parameter.in === "body") {
// Parameter name for the body is it's type in camelCase
parameter.name = parameter.export === "reference" ? _camelCase(parameter.type) : "body";
parameter.prop = "body";

// The request body is not in the "parameters" section of the openapi spec so we won't have added the schema
// properties above. Find it here.
Expand Down Expand Up @@ -565,6 +620,7 @@ export default async (argv: string[], rootScriptDir: string) => {
metadata: { type: String, optional: true },
templateDirs: { type: String, multiple: true },
outputPath: { type: String },
printData: { type: Boolean, optional: true },
}, { argv });

const spec = (await SwaggerParser.bundle(args.specPath)) as any;
Expand All @@ -585,6 +641,10 @@ export default async (argv: string[], rootScriptDir: string) => {
// Build data
const data = buildData(spec, JSON.parse(args.metadata ?? '{}'));

if (args.printData) {
console.log(JSON.stringify(data, null, 2));
}

// Read all .ejs files in each template directory
const templates = args.templateDirs.flatMap(t => fs.readdirSync(resolveTemplateDir(rootScriptDir, t), {
recursive: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import { Project, ProjectOptions, Task } from "projen";
import { GeneratedHtmlRedocDocumentationOptions } from "../../types";
import { OpenApiToolsJsonFile } from "../components/open-api-tools-json-file";
import { TypeSafeApiCommandEnvironment } from "../components/type-safe-api-command-environment";
import {
buildTypeSafeApiExecCommand,
TypeSafeApiScript,
} from "../components/utils";

export interface GeneratedHtmlRedocDocumentationProjectOptions
extends ProjectOptions,
Expand All @@ -32,10 +28,7 @@ export class GeneratedHtmlRedocDocumentationProject extends Project {

this.generateTask = this.addTask("generate");
this.generateTask.exec(
buildTypeSafeApiExecCommand(
TypeSafeApiScript.GENERATE_HTML_REDOC_DOCS,
`--spec-path ${options.specPath} --output-path .`
)
`npx --yes @redocly/[email protected] build-docs "${options.specPath}" --output ./index.html`
);
this.compileTask.spawn(this.generateTask);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
/*! Copyright [Amazon.com](http://amazon.com/), Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0 */
import { Project, ProjectOptions, Task } from "projen";
import { DocumentationFormat } from "../../languages";
import { GeneratedMarkdownDocumentationOptions } from "../../types";
import { OpenApiToolsJsonFile } from "../components/open-api-tools-json-file";
import { TypeSafeApiCommandEnvironment } from "../components/type-safe-api-command-environment";
import {
buildInvokeOpenApiGeneratorCommandArgs,
buildCodegenCommandArgs,
buildTypeSafeApiExecCommand,
OtherGenerators,
TypeSafeApiScript,
} from "../components/utils";

Expand Down Expand Up @@ -36,11 +34,10 @@ export class GeneratedMarkdownDocumentationProject extends Project {
this.generateTask = this.addTask("generate");
this.generateTask.exec(
buildTypeSafeApiExecCommand(
TypeSafeApiScript.GENERATE,
buildInvokeOpenApiGeneratorCommandArgs({
generator: DocumentationFormat.MARKDOWN,
TypeSafeApiScript.GENERATE_NEXT,
buildCodegenCommandArgs({
specPath: options.specPath,
generatorDirectory: OtherGenerators.DOCS,
templateDirs: ["docs/templates/markdown"],
})
)
);
Expand Down
Loading

0 comments on commit 345eb6f

Please sign in to comment.