Skip to content

Commit

Permalink
feat(type-safe-api): generate handler tests (#640)
Browse files Browse the repository at this point in the history
* feat(type-safe-api): generate handler tests for java
* feat(type-safe-api): generate handler tests for python
* feat(type-safe-api): generate handler tests for typescript

Additionally add lombok builder to generated Java input classes for easier construction in tests, and update default pattern for using the logger in Java handlers

Fixes #570
  • Loading branch information
cogwirrel authored Nov 8, 2023
1 parent dea3c1f commit 09335a2
Show file tree
Hide file tree
Showing 39 changed files with 1,036 additions and 388 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,15 @@ This will give you generated lambda handler stubs which look like the following:
package com.generated.api.myapijavahandlers.handlers;

import com.generated.api.myapijavaruntime.runtime.api.interceptors.DefaultInterceptors;
import com.generated.api.myapijavaruntime.runtime.api.interceptors.powertools.LoggingInterceptor;
import com.generated.api.myapijavaruntime.runtime.api.handlers.Interceptor;
import com.generated.api.myapijavaruntime.runtime.api.handlers.say_hello.SayHello;
import com.generated.api.myapijavaruntime.runtime.api.handlers.say_hello.SayHelloInput;
import com.generated.api.myapijavaruntime.runtime.api.handlers.say_hello.SayHello500Response;
import com.generated.api.myapijavaruntime.runtime.api.handlers.say_hello.SayHelloRequestInput;
import com.generated.api.myapijavaruntime.runtime.api.handlers.say_hello.SayHelloResponse;
import com.generated.api.myapijavaruntime.runtime.model.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.List;

Expand All @@ -169,6 +170,11 @@ This will give you generated lambda handler stubs which look like the following:
*/
private final List<Interceptor<SayHelloInput>> interceptors = DefaultInterceptors.all();

/**
* Use the logger to log requests. The LoggingInterceptor sets up all loggers to include lambda context values in your logs.
*/
private final Logger log = LogManager.getLogger(SayHelloHandler.class);

/**
* Return the interceptors for this handler.
* You can also use the @Interceptors annotation on the class to add interceptors
Expand All @@ -195,7 +201,7 @@ This will give you generated lambda handler stubs which look like the following:
*/
@Override
public SayHelloResponse handle(final SayHelloRequestInput request) {
LoggingInterceptor.getLogger(request).info("Start SayHello Operationnn");
log.info("Start SayHello Operation");

// TODO: Implement SayHello Operation. `input` contains the request input.
SayHelloInput input = request.getInput();
Expand Down Expand Up @@ -247,6 +253,8 @@ This will give you generated lambda handler stubs which look like the following:

You can implement your lambda handlers in any of the supported languages, or mix and match languages for different operations if you prefer.

An example unit test will also be generated for each handler. These unit tests are only generated when the corresponding handler is initially generated, so you can safely delete the generated test if you do not want it.

## Function CDK Constructs

As well as generating lambda handler stubs, when you use the `@handler` Smithy trait or `x-handler` OpenAPI vendor extension, your generated CDK infrastructure project will include lambda function CDK constructs with preconfigured paths to your handler distributables. This allows you to quickly add lambda integrations to your API:
Expand Down
10 changes: 6 additions & 4 deletions packages/type-safe-api/scripts/type-safe-api/generators/generate
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ generator_dir=''
additional_properties=''
openapi_normalizer=''
src_dir='src'
tst_dir='test'
extra_vendor_extensions=''
generate_alias_as_model=''
while [[ "$#" -gt 0 ]]; do case $1 in
Expand All @@ -20,6 +21,7 @@ while [[ "$#" -gt 0 ]]; do case $1 in
--additional-properties) additional_properties="$2"; shift;;
--openapi-normalizer) openapi_normalizer="$2"; shift;;
--src-dir) src_dir="$2"; shift;;
--tst-dir) tst_dir="$2"; shift;;
--extra-vendor-extensions) extra_vendor_extensions="$2"; shift;;
--generate-alias-as-model) generate_alias_as_model='true'; ;;
esac; shift; done
Expand Down Expand Up @@ -55,8 +57,9 @@ run_command ts-node pre-process-spec.ts \
--outputSpecPath="$processed_spec_path" \
--extraVendorExtensions="$extra_vendor_extensions"

# 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
# Support special placeholders of {{src}} and {{tst}} in config.yaml to ensure our custom templates get written to the correct folder
sed 's|{{src}}|'"$src_dir"'|g' config.yaml > config.tmp.yaml
sed 's|{{tst}}|'"$tst_dir"'|g' config.tmp.yaml > config.final.yaml

# Copy the openapitools.json config into the working directory so it's honoured by the generator cli
cp $working_dir/$output_path/openapitools.json .
Expand Down Expand Up @@ -106,8 +109,7 @@ fi
# Post processing
cp $script_dir/post-process.ts .
run_command ts-node post-process.ts \
--outputPath="$working_dir/$output_path" \
--srcDir="$src_dir"
--outputPath="$working_dir/$output_path"

# Clean up empty directories left over by openapi generator
log "Cleaning up empty directories"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ files:
handlers.handlebars:
destinationFilename: {{src}}/__all_handlers.java
templateType: SupportingFiles
tests.handlebars:
destinationFilename: {{tst}}/__all_tests.java
templateType: SupportingFiles
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
{{#startsWith vendorExtensions.x-handler.language 'java'}}
###TSAPI_WRITE_FILE###
{
"id": "{{operationIdCamelCase}}Handler",
"dir": ".",
"name": "{{operationIdCamelCase}}Handler",
"ext": ".java",
Expand All @@ -15,14 +16,15 @@
###/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.interceptors.DefaultInterceptors;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.interceptors.powertools.LoggingInterceptor;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.handlers.Interceptor;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.handlers.{{operationIdSnakeCase}}.{{operationIdCamelCase}};
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.handlers.{{operationIdSnakeCase}}.{{operationIdCamelCase}}Input;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.handlers.{{operationIdSnakeCase}}.{{operationIdCamelCase}}500Response;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.handlers.{{operationIdSnakeCase}}.{{operationIdCamelCase}}RequestInput;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.handlers.{{operationIdSnakeCase}}.{{operationIdCamelCase}}Response;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.model.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.List;

Expand All @@ -36,6 +38,11 @@ public class {{operationIdCamelCase}}Handler extends {{operationIdCamelCase}} {
*/
private final List<Interceptor<{{operationIdCamelCase}}Input>> interceptors = DefaultInterceptors.all();

/**
* Use the logger to log requests. The LoggingInterceptor sets up all loggers to include lambda context values in your logs.
*/
private final Logger log = LogManager.getLogger({{operationIdCamelCase}}Handler.class);

/**
* Return the interceptors for this handler.
* You can also use the @Interceptors annotation on the class to add interceptors
Expand All @@ -62,7 +69,7 @@ public class {{operationIdCamelCase}}Handler extends {{operationIdCamelCase}} {
*/
@Override
public {{operationIdCamelCase}}Response handle(final {{operationIdCamelCase}}RequestInput request) {
LoggingInterceptor.getLogger(request).info("Start {{operationIdCamelCase}} Operation");
log.info("Start {{operationIdCamelCase}} Operation");

// TODO: Implement {{operationIdCamelCase}} Operation. `input` contains the request input.
{{operationIdCamelCase}}Input input = request.getInput();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
###TSAPI_SPLIT_FILE###
{{#apiInfo ~}}
{{#apis ~}}
{{#operations ~}}
{{#operation ~}}
{{#if vendorExtensions.x-handler}}
{{#startsWith vendorExtensions.x-handler.language 'java'}}
###TSAPI_WRITE_FILE###
{
"id": "{{operationIdCamelCase}}HandlerTest",
"dir": ".",
"name": "{{operationIdCamelCase}}HandlerTest",
"ext": ".java",
"generateConditionallyId": "{{operationIdCamelCase}}Handler"
}
###/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.{{operationIdSnakeCase}}.{{operationIdCamelCase}}Input;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.handlers.{{operationIdSnakeCase}}.{{operationIdCamelCase}}500Response;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.handlers.{{operationIdSnakeCase}}.{{operationIdCamelCase}}RequestInput;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.handlers.{{operationIdSnakeCase}}.{{operationIdCamelCase}}RequestParameters;
import {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-package}}{{/apis.0}}{{/apiInfo}}.api.handlers.{{operationIdSnakeCase}}.{{operationIdCamelCase}}Response;
import org.junit.jupiter.api.Test;

import java.util.HashMap;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* Tests for {{operationIdCamelCase}}Handler
*/
public class {{operationIdCamelCase}}HandlerTest {
@Test
public void shouldReturnNotImplementedError() {
// TODO: Update the test as appropriate when you implement your handler
{{operationIdCamelCase}}Response response = new {{operationIdCamelCase}}Handler().handle({{operationIdCamelCase}}RequestInput.builder()
.interceptorContext(new HashMap<>())
.input({{operationIdCamelCase}}Input.builder()
.requestParameters({{operationIdCamelCase}}RequestParameters.builder()
// Add request parameters here...
.build())
// If the request has a body you can add it here...
.build())
.build());
assertEquals(500, response.getStatusCode());

// Cast to the particular type of response returned to access the typed response body
assertEquals("Not Implemented!", (({{operationIdCamelCase}}500Response) response).getTypedBody().getMessage());
}
}

{{~/startsWith}}
{{~/if}}
{{~/operation}}
{{~/operations}}
{{~/apis}}
{{~/apiInfo}}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ files:
destinationFilename: {{src}}/api/operation_config/OperationLookup.java
templateType: SupportingFiles
handlers.mustache:
destinationFilename: {{src}}/api/handlers/__handlers.java
destinationFilename: {{src}}/__handlers.java
templateType: SupportingFiles
interceptors.mustache:
destinationFilename: {{src}}/api/interceptors/__interceptors.java
destinationFilename: {{src}}/__interceptors.java
templateType: SupportingFiles
Original file line number Diff line number Diff line change
Expand Up @@ -861,9 +861,9 @@ public class {{operationIdCamelCase}}{{code}}Response extends RuntimeException i
}
}

private String body;
{{#dataType}}private {{#isPrimitiveType}}String{{/isPrimitiveType}}{{^isPrimitiveType}}{{.}}{{/isPrimitiveType}} typedBody;{{/dataType}}
private Map<String, String> headers;
private final String body;
{{#dataType}}private final {{#isPrimitiveType}}String{{/isPrimitiveType}}{{^isPrimitiveType}}{{.}}{{/isPrimitiveType}} typedBody;{{/dataType}}
private final Map<String, String> headers;

private {{operationIdCamelCase}}{{code}}Response({{#dataType}}final {{#isPrimitiveType}}String{{/isPrimitiveType}}{{^isPrimitiveType}}{{.}}{{/isPrimitiveType}} body, {{/dataType}}final Map<String, String> headers) {
{{#dataType}}this.typedBody = body;{{/dataType}}
Expand Down Expand Up @@ -946,10 +946,12 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
/**
* Query, path and header parameters for the {{operationIdCamelCase}} operation
*/
@lombok.Builder
@lombok.AllArgsConstructor
public class {{operationIdCamelCase}}RequestParameters {
{{#allParams}}
{{^isBodyParam}}
private {{^required}}Optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}};
private final {{^required}}Optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}};
{{/isBodyParam}}
{{/allParams}}

Expand Down Expand Up @@ -1022,6 +1024,8 @@ import java.io.IOException;
/**
* Input for the {{nickname}} operation
*/
@lombok.Builder
@lombok.AllArgsConstructor
public class {{operationIdCamelCase}}Input {
static {
// JSON has a static instance of Gson which is instantiated lazily the first time it is initialised.
Expand All @@ -1031,9 +1035,9 @@ public class {{operationIdCamelCase}}Input {
}
}

private {{operationIdCamelCase}}RequestParameters requestParameters;
private final {{operationIdCamelCase}}RequestParameters requestParameters;
{{#bodyParam}}
private {{#isModel}}{{dataType}}{{/isModel}}{{^isModel}}String{{/isModel}} body;
private final {{#isModel}}{{dataType}}{{/isModel}}{{^isModel}}String{{/isModel}} body;
{{/bodyParam}}

public {{operationIdCamelCase}}Input(final APIGatewayProxyRequestEvent event) {
Expand Down Expand Up @@ -1104,18 +1108,13 @@ import com.amazonaws.services.lambda.runtime.Context;
/**
* Full request input for the {{nickname}} operation, including the raw API Gateway event
*/
@lombok.Builder
@lombok.AllArgsConstructor
public class {{operationIdCamelCase}}RequestInput implements RequestInput<{{operationIdCamelCase}}Input> {
private APIGatewayProxyRequestEvent event;
private Context context;
private Map<String, Object> interceptorContext;
private {{operationIdCamelCase}}Input input;

public {{operationIdCamelCase}}RequestInput(final APIGatewayProxyRequestEvent event, final Context context, final Map<String, Object> interceptorContext, final {{operationIdCamelCase}}Input input) {
this.event = event;
this.context = context;
this.interceptorContext = interceptorContext;
this.input = input;
}
private final APIGatewayProxyRequestEvent event;
private final Context context;
private final Map<String, Object> interceptorContext;
private final {{operationIdCamelCase}}Input input;

/**
* Returns the typed request input, with path, query and body parameters
Expand Down
Loading

0 comments on commit 09335a2

Please sign in to comment.