Skip to content

Commit

Permalink
feat(open-api-gateway): java support for router for using single lamb…
Browse files Browse the repository at this point in the history
…da function for all operations (#242)

Adds a HandlerRouter class to the generated Java client which can be extended and used as the lambda
entrypoint for all operations. The router requires you to define a method which returns a handler
for every operation. The router can also be decorated with interceptors to apply to all operations.

fix #238
  • Loading branch information
cogwirrel committed Dec 9, 2022
1 parent 60cb977 commit f4f5401
Show file tree
Hide file tree
Showing 15 changed files with 2,212 additions and 300 deletions.
29 changes: 29 additions & 0 deletions packages/open-api-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,35 @@ public class SayHelloHandler extends SayHello {
}
```

##### Handler Router

The lambda handler wrappers can be used in isolation as handler methods for separate lambdas. If you would like to use a single lambda function to serve all requests, you can do so by extending the `HandlerRouter` class.

```java
import com.generated.api.myapijava.client.api.Handlers.SayGoodbye;
import com.generated.api.myapijava.client.api.Handlers.HandlerRouter;
import com.generated.api.myapijava.client.api.Handlers.Interceptors;
import com.generated.api.myapijava.client.api.Handlers.SayHello;

import java.util.Arrays;
import java.util.List;

// Interceptors defined here apply to all operations
@Interceptors({ TimingInterceptor.class })
public class ApiHandlerRouter extends HandlerRouter {
// You must implement a method to return a handler for every operation
@Override
public SayHello sayHello() {
return new SayHelloHandler();
}

@Override
public SayGoodbye sayGoodbye() {
return new SayGoodbyeHandler();
}
}
```

### Interceptors

The lambda handler wrappers allow you to pass in a _chain_ of handler functions to handle the request. This allows you to implement middleware / interceptors for handling requests. Each handler function may choose whether or not to continue the handler chain by invoking `chain.next`.
Expand Down
3 changes: 3 additions & 0 deletions packages/open-api-gateway/scripts/generators/java/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ files:
operationConfig.mustache:
destinationFilename: /OperationConfig.java
templateType: API
operations.mustache:
destinationFilename: /Operations.java
templateType: API
operationLookup.mustache:
destinationFilename: /OperationLookup.java
templateType: API
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,25 @@ public class Handlers {
}
}

private static String concatMethodAndPath(final String method, final String path) {
return String.format("%s||%s", method.toLowerCase(), path);
}

private static <T, I> List<Interceptor<I>> getAnnotationInterceptors(Class<T> clazz) {
// Support specifying simple interceptors via the @Interceptors({ MyInterceptor.class, MyOtherInterceptor.class }) format
return clazz.isAnnotationPresent(Interceptors.class)
? Arrays.stream(clazz.getAnnotation(Interceptors.class).value()).map(c -> {
try {
return (Interceptor<I>) c.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(String.format(
"Cannot create instance of interceptor %s. Please ensure it has a public constructor " +
"with no arguments, or override the getInterceptors method instead of using the annotation", c.getSimpleName()), e);
}
}).collect(Collectors.toList())
: new ArrayList<>();
}

/**
* Represents an HTTP response from an api operation
*/
Expand Down Expand Up @@ -435,21 +454,18 @@ public class Handlers {

@Override
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent event, final Context context) {
return this.handleRequestWithAdditionalInterceptors(event, context, new ArrayList<>());
}

public APIGatewayProxyResponseEvent handleRequestWithAdditionalInterceptors(final APIGatewayProxyRequestEvent event, final Context context, final List<Interceptor<{{operationIdCamelCase}}Input>> additionalInterceptors) {
final Map<String, Object> interceptorContext = new HashMap<>();
// Support specifying simple interceptors via the @Interceptors({ MyInterceptor.class, MyOtherInterceptor.class }) format
List<Interceptor<{{operationIdCamelCase}}Input>> interceptors = this.getClass().isAnnotationPresent(Interceptors.class)
? Arrays.stream(this.getClass().getAnnotation(Interceptors.class).value()).map(clazz -> {
try {
return (Interceptor<{{operationIdCamelCase}}Input>) clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(String.format(
"Cannot create instance of interceptor %s. Please ensure it has a public constructor " +
"with no arguments, or override the getInterceptors method instead of using the annotation", clazz.getSimpleName()), e);
}
}).collect(Collectors.toList())
: new ArrayList<>();
List<Interceptor<{{operationIdCamelCase}}Input>> interceptors = new ArrayList<>();
interceptors.addAll(additionalInterceptors);

List<Interceptor<{{operationIdCamelCase}}Input>> annotationInterceptors = getAnnotationInterceptors(this.getClass());

interceptors.addAll(annotationInterceptors);
interceptors.addAll(this.getInterceptors());

final HandlerChain chain = buildHandlerChain(interceptors, new HandlerChain<{{operationIdCamelCase}}Input>() {
Expand Down Expand Up @@ -497,4 +513,85 @@ public class Handlers {
{{/operation}}
{{/operations}}
public static abstract class HandlerRouter implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
{{#operations}}
{{#operation}}
private static final String {{nickname}}MethodAndPath = concatMethodAndPath("{{httpMethod}}", "{{path}}");
{{/operation}}
{{/operations}}
{{#operations}}
{{#operation}}
private final {{operationIdCamelCase}} constructed{{operationIdCamelCase}};
{{/operation}}
{{/operations}}
{{#operations}}
{{#operation}}
/**
* This method must return your implementation of the {{operationIdCamelCase}} operation
*/
public abstract {{operationIdCamelCase}} {{nickname}}();
{{/operation}}
{{/operations}}
private static enum Route {
{{#operations}}
{{#operation}}
{{nickname}}Route,
{{/operation}}
{{/operations}}
}
/**
* Map of method and path to the route to map to
*/
private final Map<String, Route> routes = new HashMap<>();
public HandlerRouter() {
{{#operations}}
{{#operation}}
this.routes.put({{nickname}}MethodAndPath, Route.{{nickname}}Route);
{{/operation}}
{{/operations}}
// Handlers are all constructed in the router's constructor such that lambda behaviour remains consistent;
// ie resources created in the constructor remain in memory between invocations.
// https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html
{{#operations}}
{{#operation}}
this.constructed{{operationIdCamelCase}} = this.{{nickname}}();
{{/operation}}
{{/operations}}
}

/**
* For more complex interceptors that require instantiation with parameters, you may override this method to
* return a list of instantiated interceptors. For simple interceptors with no need for constructor arguments,
* prefer the @Interceptors annotation.
*/
public <T> List<Interceptor<T>> getInterceptors() {
return Collections.emptyList();
}

@Override
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent event, final Context context) {
String method = event.getRequestContext().getHttpMethod();
String path = event.getRequestContext().getResourcePath();
String methodAndPath = concatMethodAndPath(method, path);
Route route = this.routes.get(methodAndPath);
switch (route) {
{{#operations}}
{{#operation}}
case {{nickname}}Route:
List<Interceptor<{{operationIdCamelCase}}Input>> {{nickname}}Interceptors = getAnnotationInterceptors(this.getClass());
{{nickname}}Interceptors.addAll(this.getInterceptors());
return this.constructed{{operationIdCamelCase}}.handleRequestWithAdditionalInterceptors(event, context, {{nickname}}Interceptors);
{{/operation}}
{{/operations}}
default:
throw new RuntimeException(String.format("No registered handler for method {} and path {}", method, path));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package {{package}};

{{>generatedAnnotation}}
public class Operations {
/**
* Returns an OperationConfig Builder with all values populated with the given value.
* You can override specific values on the builder if you like.
* Make sure you call `.build()` at the end to construct the OperationConfig.
*/
public static <T> OperationConfig.OperationConfigBuilder<T> all(final T value) {
return OperationConfig.<T>builder()
{{#operations}}
{{#operation}}
.{{nickname}}(value)
{{/operation}}
{{/operations}}
;
}
}

0 comments on commit f4f5401

Please sign in to comment.