Skip to content

Commit

Permalink
feat(type-safe-api): generate lambda handler stubs based on model tra…
Browse files Browse the repository at this point in the history
…it (#537)

This change adds a new configuration option `handlers` which allows users to opt in to generating
lambda handler projects for java, python, and/or typescript, ready-configured with packaging tasks
for use in a lambda function. Handler stubs are generated for operations annotated with the @handler
trait in Smithy (or the x-handler vendor extension in OpenAPI), only generating the handler stubs
for the chosen language for each operation. Documentation to come once this feature has been further
fleshed out.
  • Loading branch information
cogwirrel committed Aug 16, 2023
1 parent c32ff63 commit 16633b6
Show file tree
Hide file tree
Showing 43 changed files with 11,269 additions and 248 deletions.
18 changes: 17 additions & 1 deletion packages/type-safe-api/scripts/generators/generate
Expand Up @@ -51,6 +51,8 @@ install_packages \
@types/node@20.1.5 \
ts-node@10.9.1 \
ts-command-line-args@2.4.2 \
lodash@4.17.21 \
@types/lodash@4.14.197 \
@apidevtools/swagger-parser@10.1.0 \
openapi-types@12.1.0

Expand All @@ -59,7 +61,11 @@ install_packages \
log "preprocess spec :: $spec_path"
processed_spec_path="$tmp_dir/.preprocessed-api.json"
cp $script_dir/pre-process-spec.ts .
run_command ts-node pre-process-spec.ts --specPath="$working_dir/$spec_path" --outputSpecPath="$processed_spec_path" --extraVendorExtensions="$extra_vendor_extensions" ${smithy_json_path:+"--smithyJsonPath=$working_dir/$smithy_json_path"}
run_command ts-node pre-process-spec.ts \
--specPath="$working_dir/$spec_path" \
--outputSpecPath="$processed_spec_path" \
--extraVendorExtensions="$extra_vendor_extensions" \
${smithy_json_path:+"--smithyJsonPath=$working_dir/$smithy_json_path"}

# Support a special placeholder of {{src}} in config.yaml to ensure our custom templates get written to the correct folder
sed 's|{{src}}|'"$src_dir"'|g' config.yaml > config.final.yaml
Expand Down Expand Up @@ -109,6 +115,16 @@ if [ -f "$handlebars_ignore_file" ]; then
fi
fi

# Post processing
cp $script_dir/post-process.ts .
run_command ts-node post-process.ts \
--outputPath="$working_dir/$output_path" \
--srcDir="$src_dir"

# Clean up empty directories left over by openapi generator
log "Cleaning up empty directories"
find "$working_dir/$output_path" -type d -empty -print -delete

echo "$generator_dir ($generator) OpenAPI generation done!"

# Clean up
Expand Down
@@ -0,0 +1,4 @@
files:
handlers.handlebars:
destinationFilename: {{src}}/__all_handlers.java
templateType: SupportingFiles
@@ -0,0 +1,46 @@
###TSAPI_SPLIT_FILE###
{{#apiInfo ~}}
{{#apis ~}}
{{#operations ~}}
{{#operation ~}}
{{#if vendorExtensions.x-handler}}
{{#startsWith vendorExtensions.x-handler.language 'java'}}
###TSAPI_WRITE_FILE###
{
"dir": ".",
"name": "{{operationIdCamelCase}}Handler",
"ext": ".java",
"overwrite": false,
"kebabCaseFileName": false
}
###/TSAPI_WRITE_FILE###package {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-handlers-package}}{{/apis.0}}{{/apiInfo}};

import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.Handlers.{{operationIdCamelCase}};
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.Handlers.{{operationIdCamelCase}}500Response;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.Handlers.{{operationIdCamelCase}}RequestInput;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.Handlers.{{operationIdCamelCase}}Response;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.model.*;

/**
* Entry point for the AWS Lambda handler for the {{operationIdCamelCase}} operation.
* The {{operationIdCamelCase}} class manages marshalling inputs and outputs.
*/
public class {{operationIdCamelCase}}Handler extends {{operationIdCamelCase}} {
/**
* Type-safe handler for the {{operationIdCamelCase}} operation
*/
@Override
public {{operationIdCamelCase}}Response handle({{operationIdCamelCase}}RequestInput input) {
// TODO: Implement {{operationIdCamelCase}} Operation
return {{operationIdCamelCase}}500Response.of(InternalFailureErrorResponseContent.builder()
.message("Not Implemented!")
.build());
}
}

{{~/startsWith}}
{{~/if}}
{{~/operation}}
{{~/operations}}
{{~/apis}}
{{~/apiInfo}}
109 changes: 109 additions & 0 deletions packages/type-safe-api/scripts/generators/post-process.ts
@@ -0,0 +1,109 @@
/*! Copyright [Amazon.com](http://amazon.com/), Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0 */
import * as fs from "fs";
import * as path from "path";
import kebabCase from "lodash/kebabCase";
import { parse } from "ts-command-line-args";

// Used to split OpenAPI generated files into multiple files in order to work around
// the restrictions around file naming and splitting in OpenAPI generator
const TSAPI_SPLIT_FILE_HEADER = "###TSAPI_SPLIT_FILE###";
const TSAPI_WRITE_FILE_START = "###TSAPI_WRITE_FILE###";
const TSAPI_WRITE_FILE_END = "###/TSAPI_WRITE_FILE###";

interface Arguments {
/**
* Path to the directory containing output files
*/
readonly outputPath: string;
/**
* Path to the source directory relative to the output directory
*/
readonly srcDir: string;
}

interface WriteFileConfig {
readonly dir: string;
readonly name: string;
readonly ext: string;
readonly overwrite?: boolean;
readonly kebabCaseFileName?: boolean;
}

void (async () => {
const args = parse<Arguments>({
outputPath: { type: String },
srcDir: { type: String },
});

// OpenAPI generator writes a manifest called FILES which lists the files it generated.
const openApiGeneratedFilesManifestPath = path.join(
args.outputPath,
".openapi-generator",
"FILES"
);

// Read the file paths from the manifest
const generatedFiles = fs
.readFileSync(openApiGeneratedFilesManifestPath, { encoding: "utf-8" })
.split("\n")
.filter((x) => x);

const additionalGeneratedFiles: string[] = [];

// Loop over generated files
generatedFiles.forEach((generatedFile) => {
const filePath = path.join(args.outputPath, generatedFile);

if (fs.existsSync(filePath)) {
const contents = fs.readFileSync(filePath, "utf-8");

if (contents.startsWith(TSAPI_SPLIT_FILE_HEADER)) {
// Split by the start template
contents
.split(TSAPI_WRITE_FILE_START)
.filter((t) => t.includes(TSAPI_WRITE_FILE_END))
.forEach((destinationFileTemplate) => {
// Split by the end template to receive the file path, and contents
const [configString, newFileContents] =
destinationFileTemplate.split(TSAPI_WRITE_FILE_END);
const config = JSON.parse(configString) as WriteFileConfig;

const newFileName = `${
config.kebabCaseFileName ? kebabCase(config.name) : config.name
}${config.ext}`;
const relativeNewFileDir = path.join(args.srcDir, config.dir);
const relativeNewFilePath = path.join(
relativeNewFileDir,
newFileName
);
const newFilePath = path.join(args.outputPath, relativeNewFilePath);

// Write to the instructed file path (relative to the src dir)
if (!fs.existsSync(newFilePath) || config.overwrite) {
// Create it's containing directory if needed
fs.mkdirSync(path.join(args.outputPath, relativeNewFileDir), {
recursive: true,
});
fs.writeFileSync(newFilePath, newFileContents);

// Overwritten files are added to the manifest so that they can be cleaned up
// by clean-openapi-generated-code
if (config.overwrite) {
additionalGeneratedFiles.push(relativeNewFilePath);
}
}
});

// Delete the original file
fs.rmSync(filePath);
}
}
});

// Update the manifest with any overwritten files
fs.writeFileSync(
openApiGeneratedFilesManifestPath,
[...generatedFiles, ...additionalGeneratedFiles].join("\n")
);
})();
8 changes: 6 additions & 2 deletions packages/type-safe-api/scripts/generators/pre-process-spec.ts
@@ -1,7 +1,6 @@
/*! Copyright [Amazon.com](http://amazon.com/), Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0 */
import * as fs from "fs";
import * as path from "path";
import SwaggerParser from "@apidevtools/swagger-parser";
import { parse } from "ts-command-line-args";

Expand Down Expand Up @@ -76,9 +75,14 @@ void (async () => {
Object.entries(operation.traits).forEach(([traitId, value]) => {
// By default, we use x-<fully_qualified_trait_id> for the vendor extension, but for extensions we support
// directly from OpenAPI we apply a mapping (rather than repeat ourselves in the mustache templates).
const vendorExtension =
let vendorExtension =
TRAIT_TO_SUPPORTED_OPENAPI_VENDOR_EXTENSION[traitId] ??
`x-${traitId}`;
// Special case for the handler trait where it's defined as part of the user's smithy model, so the namespace
// can be any namespace the user defines
if (traitId.endsWith("#handler")) {
vendorExtension = "x-handler";
}
spec.paths[operation.path][operation.method][vendorExtension] = value;
});
}
Expand Down
@@ -0,0 +1,4 @@
files:
handlers.handlebars:
destinationFilename: {{src}}/__all_handlers.py
templateType: SupportingFiles
@@ -0,0 +1,43 @@
###TSAPI_SPLIT_FILE###
{{#apiInfo ~}}
{{#apis ~}}
{{#operations ~}}
{{#operation ~}}
{{#if vendorExtensions.x-handler}}
{{#startsWith vendorExtensions.x-handler.language 'python'}}
###TSAPI_WRITE_FILE###
{
"dir": ".",
"name": "{{operationId}}",
"ext": ".py",
"overwrite": false
}
###/TSAPI_WRITE_FILE###from {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-module-name}}{{/apis.0}}{{/apiInfo}}.models import *
from {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-module-name}}{{/apis.0}}{{/apiInfo}}.apis.tags.default_api_operation_config import (
{{operationId}}_handler, {{operationIdCamelCase}}Request, {{operationIdCamelCase}}OperationResponses, ApiResponse
)


def {{operationId}}(input: {{operationIdCamelCase}}Request, **kwargs) -> {{operationIdCamelCase}}OperationResponses:
"""
Type-safe handler for the {{operationIdCamelCase}} operation
"""
# TODO: Implement {{operationIdCamelCase}} Operation
return ApiResponse(
status_code=500,
body=InternalFailureErrorResponseContent(
message="Not Implemented!"),
headers={}
)


# Entry point for the AWS Lambda handler for the {{operationIdCamelCase}} operation.
# The {{operationId}}_handler method wraps the type-safe handler and manages marshalling inputs and outputs
handler = {{operationId}}_handler({{operationId}})

{{~/startsWith}}
{{~/if}}
{{~/operation}}
{{~/operations}}
{{~/apis}}
{{~/apiInfo}}
@@ -0,0 +1,4 @@
files:
handlers.handlebars:
destinationFilename: {{src}}/__all_handlers.ts
templateType: SupportingFiles
@@ -0,0 +1,45 @@
###TSAPI_SPLIT_FILE###
{{#apiInfo ~}}
{{#apis ~}}
{{#operations ~}}
{{#operation ~}}
{{#if vendorExtensions.x-handler}}
{{#startsWith vendorExtensions.x-handler.language 'typescript'}}
###TSAPI_WRITE_FILE###
{
"dir": ".",
"name": "{{nickname}}",
"ext": ".ts",
"overwrite": false,
"kebabCaseFileName": true
}
###/TSAPI_WRITE_FILE###import {
{{nickname}}Handler,
{{operationIdCamelCase}}ChainedHandlerFunction,
} from "{{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package-name}}{{/apis.0}}{{/apiInfo}}";

/**
* Type-safe handler for the {{operationIdCamelCase}} operation
*/
export const {{nickname}}: {{operationIdCamelCase}}ChainedHandlerFunction = async ({ input }) => {
// TODO: Implement {{operationIdCamelCase}} Operation
return {
statusCode: 500,
body: {
message: "Not Implemented!",
},
};
};

/**
* Entry point for the AWS Lambda handler for the {{operationIdCamelCase}} operation.
* The {{nickname}}Handler method wraps the type-safe handler and manages marshalling inputs and outputs
*/
export const handler = {{nickname}}Handler({{nickname}});

{{~/startsWith}}
{{~/if}}
{{~/operation}}
{{~/operations}}
{{~/apis}}
{{~/apiInfo}}
Expand Up @@ -10,9 +10,14 @@ import { MockResponseDataGenerationOptions } from "../../types";
*/
export enum OtherGenerators {
DOCS = "docs",
// Infrastructure
TYPESCRIPT_CDK_INFRASTRUCTURE = "typescript-cdk-infrastructure",
PYTHON_CDK_INFRASTRUCTURE = "python-cdk-infrastructure",
JAVA_CDK_INFRASTRUCTURE = "java-cdk-infrastructure",
// Handlers
TYPESCRIPT_LAMBDA_HANDLERS = "typescript-lambda-handlers",
PYTHON_LAMBDA_HANDLERS = "python-lambda-handlers",
JAVA_LAMBDA_HANDLERS = "java-lambda-handlers",
}

export enum TypeSafeApiScript {
Expand Down

0 comments on commit 16633b6

Please sign in to comment.