diff --git a/build.gradle b/build.gradle index 55a7605..52e3a25 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,9 @@ dependencies { 'org.codehaus.groovy:groovy-all:2.4.7', 'com.amazonaws:aws-lambda-java-core:1.1.0', 'com.amazonaws:aws-lambda-java-log4j:1.0.0', - 'com.h2database:h2' + 'com.h2database:h2', + 'io.vertx:vertx-core:3.4.1', + 'io.vertx:vertx-lang-groovy:3.4.1' ) // Use JUnit test framework diff --git a/src/main/groovy/com/serverless/LambdaHandler.groovy b/src/main/groovy/com/serverless/LambdaHandler.groovy index 934d90a..06476ec 100644 --- a/src/main/groovy/com/serverless/LambdaHandler.groovy +++ b/src/main/groovy/com/serverless/LambdaHandler.groovy @@ -2,16 +2,19 @@ package com.serverless import com.amazonaws.services.lambda.runtime.Context import com.amazonaws.services.lambda.runtime.RequestHandler -import com.serverless.service.DispatcherService import com.serverless.lambda.Request import com.serverless.lambda.Response import groovy.transform.Memoized import groovy.util.logging.Log4j +import io.vertx.core.eventbus.EventBus import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.context.ApplicationContext import org.springframework.context.annotation.ComponentScan +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + @SpringBootApplication @ComponentScan(basePackages = "com.serverless") @Log4j @@ -23,13 +26,22 @@ class LambdaHandler implements RequestHandler { @Memoized ApplicationContext getApplicationContext(String[] args = []) { + System.setProperty('vertx.disableFileCPResolving', 'true') return SpringApplication.run(LambdaHandler.class, args) } @Override Response handleRequest(Map input, Context context) { - final Request request = new Request(input, context) - DispatcherService dispatcher = getApplicationContext().getBean(DispatcherService.class) - return dispatcher.dispatch(request) + final Request request = new Request(context.awsRequestId, input) + def eventBus = applicationContext.getBean(EventBus.class) + CompletableFuture future = new CompletableFuture<>() + eventBus.send(request.methodResource(), request, { ar -> + if (ar.succeeded()) { + assert ar.result().body() instanceof Response + log.info "Received reply: ${ar.result().body()}" + future.complete(ar.result().body()) + } + }) + future.get(5, TimeUnit.SECONDS) } } diff --git a/src/main/groovy/com/serverless/handler/CreateUser.groovy b/src/main/groovy/com/serverless/handler/CreateUser.groovy index eb4f254..8e634e2 100644 --- a/src/main/groovy/com/serverless/handler/CreateUser.groovy +++ b/src/main/groovy/com/serverless/handler/CreateUser.groovy @@ -5,8 +5,8 @@ import com.serverless.lambda.Response import com.serverless.persistence.entity.User import com.serverless.persistence.repository.UserRepository import groovy.json.JsonOutput -import groovy.transform.CompileStatic import groovy.util.logging.Log4j +import io.vertx.core.eventbus.Message import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @@ -14,33 +14,35 @@ import javax.annotation.PostConstruct @Component @Log4j -@CompileStatic -class CreateUser implements Handler { +class CreateUser implements MessageHandler { @Autowired private UserRepository userRepository + @PostConstruct + void init() { + // Sample Data + userRepository.save(new User('Michael', 'Jordan')) + userRepository.save(new User('Scottie', 'Pippen')) + userRepository.save(new User('Phil', 'Jackson')) + } + @Override - boolean route(Request request) { - request.resourcePath() == '/users/create' && request.httpMethod() == 'POST' + String getRouteChannel() { + return 'POST:/users/create' } @Override - Response respond(final Request request) { + void handle(final Message message) { + final Request request = message.body() def firstName = request.queryString('firstName') def lastName = request.queryString('lastName') def user = new User(firstName, lastName) userRepository.save(user) - Response.builder() + def response = Response.builder() .statusCode(201) .body(JsonOutput.prettyPrint(JsonOutput.toJson(user))) .build() - } - - @PostConstruct - void init() { - userRepository.save(new User('Michael', 'Jordan')) - userRepository.save(new User('Scottie', 'Pippen')) - userRepository.save(new User('Phil', 'Jackson')) + message.reply(response) } } diff --git a/src/main/groovy/com/serverless/handler/FindUsersByLastName.groovy b/src/main/groovy/com/serverless/handler/FindUsersByLastName.groovy index 7b030fa..15b4567 100644 --- a/src/main/groovy/com/serverless/handler/FindUsersByLastName.groovy +++ b/src/main/groovy/com/serverless/handler/FindUsersByLastName.groovy @@ -1,34 +1,35 @@ package com.serverless.handler - import com.serverless.lambda.Request import com.serverless.lambda.Response import com.serverless.persistence.repository.UserRepository import groovy.json.JsonOutput -import groovy.transform.CompileStatic import groovy.util.logging.Log4j + +import io.vertx.core.eventbus.Message import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @Component @Log4j -@CompileStatic -class FindUsersByLastName implements Handler { +class FindUsersByLastName implements MessageHandler { @Autowired private UserRepository userRepository @Override - boolean route(final Request request) { - request.resourcePath() == '/users/{lastName}' && request.httpMethod() == 'GET' + String getRouteChannel() { + return 'GET:/users/{lastName}' } @Override - Response respond(final Request request) { + void handle(final Message message) { + def request = message.body() def customers = userRepository.findByLastName(request.pathParameter('lastName')) - Response.builder() + def response = Response.builder() .statusCode(200) .body(JsonOutput.prettyPrint(JsonOutput.toJson(customers))) .build() + message.reply(response) } } diff --git a/src/main/groovy/com/serverless/handler/GetUsers.groovy b/src/main/groovy/com/serverless/handler/GetUsers.groovy index 5ca07a0..8b71168 100644 --- a/src/main/groovy/com/serverless/handler/GetUsers.groovy +++ b/src/main/groovy/com/serverless/handler/GetUsers.groovy @@ -1,34 +1,33 @@ package com.serverless.handler - import com.serverless.lambda.Request import com.serverless.lambda.Response import com.serverless.persistence.repository.UserRepository import groovy.json.JsonOutput -import groovy.transform.CompileStatic import groovy.util.logging.Log4j +import io.vertx.core.eventbus.Message import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @Component @Log4j -@CompileStatic -class GetUsers implements Handler { +class GetUsers implements MessageHandler { @Autowired private UserRepository userRepository @Override - boolean route(final Request request) { - request.resourcePath() == '/users' && request.httpMethod() == 'GET' + String getRouteChannel() { + return 'GET:/users' } @Override - Response respond(final Request request) { + void handle(final Message message) { def allCustomers = userRepository.findAll() - Response.builder() + def response = Response.builder() .statusCode(200) .body(JsonOutput.prettyPrint(JsonOutput.toJson(allCustomers))) .build() + message.reply(response) } } diff --git a/src/main/groovy/com/serverless/handler/Handler.groovy b/src/main/groovy/com/serverless/handler/Handler.groovy deleted file mode 100644 index 55d465d..0000000 --- a/src/main/groovy/com/serverless/handler/Handler.groovy +++ /dev/null @@ -1,9 +0,0 @@ -package com.serverless.handler - -import com.serverless.lambda.Request -import com.serverless.lambda.Response - -interface Handler { - boolean route(Request request) - Response respond(Request request) -} \ No newline at end of file diff --git a/src/main/groovy/com/serverless/handler/MessageHandler.groovy b/src/main/groovy/com/serverless/handler/MessageHandler.groovy new file mode 100644 index 0000000..b97d00d --- /dev/null +++ b/src/main/groovy/com/serverless/handler/MessageHandler.groovy @@ -0,0 +1,10 @@ +package com.serverless.handler + +import com.serverless.lambda.Request +import io.vertx.core.Handler +import io.vertx.core.eventbus.Message + + +interface MessageHandler extends Handler> { + String getRouteChannel() +} \ No newline at end of file diff --git a/src/main/groovy/com/serverless/lambda/Request.groovy b/src/main/groovy/com/serverless/lambda/Request.groovy index 9fad90c..26ea2b9 100644 --- a/src/main/groovy/com/serverless/lambda/Request.groovy +++ b/src/main/groovy/com/serverless/lambda/Request.groovy @@ -1,20 +1,20 @@ package com.serverless.lambda -import com.amazonaws.services.lambda.runtime.Context +import com.serverless.trait.ToJson import groovy.transform.ToString @ToString(includePackage = false) -class Request { - private final Map input - private final Context context +class Request implements ToJson { + final Map input + final String requestId - Request(final Map input, final Context context) { + Request(final String requestId, final Map input) { this.input = input - this.context = context + this.requestId = requestId } String requestId() { - context?.awsRequestId + requestId } String resourcePath() { @@ -32,4 +32,8 @@ class Request { String pathParameter(String name) { input?.pathParameters?."${name}"?.trim() } + + String methodResource() { + "${httpMethod()}:${resourcePath()}" + } } diff --git a/src/main/groovy/com/serverless/lambda/Response.groovy b/src/main/groovy/com/serverless/lambda/Response.groovy index ba26ea3..1a0c466 100644 --- a/src/main/groovy/com/serverless/lambda/Response.groovy +++ b/src/main/groovy/com/serverless/lambda/Response.groovy @@ -1,5 +1,6 @@ package com.serverless.lambda +import com.serverless.trait.ToJson import groovy.transform.CompileStatic import groovy.transform.ToString import groovy.transform.builder.Builder @@ -7,7 +8,7 @@ import groovy.transform.builder.Builder @Builder @ToString(includePackage = false) @CompileStatic -class Response { +class Response implements ToJson { int statusCode String body private Map headers = [:] diff --git a/src/main/groovy/com/serverless/persistence/entity/User.groovy b/src/main/groovy/com/serverless/persistence/entity/User.groovy index adc190e..27994f8 100644 --- a/src/main/groovy/com/serverless/persistence/entity/User.groovy +++ b/src/main/groovy/com/serverless/persistence/entity/User.groovy @@ -19,7 +19,7 @@ class User { String firstName String lastName - User(){} + User() {} User(String firstName, String lastName) { this.firstName = firstName diff --git a/src/main/groovy/com/serverless/service/DispatcherService.groovy b/src/main/groovy/com/serverless/service/DispatcherService.groovy deleted file mode 100644 index 5d7db60..0000000 --- a/src/main/groovy/com/serverless/service/DispatcherService.groovy +++ /dev/null @@ -1,35 +0,0 @@ -package com.serverless.service - - -import com.serverless.handler.Handler -import com.serverless.lambda.Request -import com.serverless.lambda.Response -import groovy.transform.CompileStatic -import groovy.util.logging.Log4j -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.stereotype.Component - -@Component -@CompileStatic -@Log4j -class DispatcherService { - - @Autowired - List handlers - - Response dispatch(Request request) { - log.info("request: ${request}") - try { - def handlers = handlers?.findAll { it.route(request) } - if (handlers?.size() != 1) { - throw new RuntimeException('Unable to resolve unique handler for route/request') - } - final Response response = handlers.first().respond(request) - response.addHeader('X-Request-Id', request.requestId()) - log.info("response: ${response}") - return response - } catch (e) { - return Response.builder().statusCode(500).body(e.getMessage()).build() - } - } -} diff --git a/src/main/groovy/com/serverless/trait/ToJson.groovy b/src/main/groovy/com/serverless/trait/ToJson.groovy new file mode 100644 index 0000000..d507b1f --- /dev/null +++ b/src/main/groovy/com/serverless/trait/ToJson.groovy @@ -0,0 +1,11 @@ +package com.serverless.trait + +import groovy.json.JsonOutput +import groovy.transform.CompileStatic + +@CompileStatic +trait ToJson { + String asJson() { + return JsonOutput.prettyPrint(JsonOutput.toJson(this)) + } +} \ No newline at end of file diff --git a/src/main/groovy/com/serverless/vertx/RequestCodec.groovy b/src/main/groovy/com/serverless/vertx/RequestCodec.groovy new file mode 100644 index 0000000..4184178 --- /dev/null +++ b/src/main/groovy/com/serverless/vertx/RequestCodec.groovy @@ -0,0 +1,38 @@ +package com.serverless.vertx + +import com.serverless.lambda.Request +import groovy.json.JsonSlurper +import io.vertx.core.buffer.Buffer +import io.vertx.core.eventbus.MessageCodec +import org.springframework.stereotype.Component + +@Component +class RequestCodec implements MessageCodec { + + @Override + void encodeToWire(Buffer buffer, Request request) { + buffer.appendString(request.asJson()) + } + + @Override + Request decodeFromWire(int pos, Buffer buffer) { + String jsonString = buffer.getString(0, buffer.length()) + Map json = JsonSlurper.newInstance().parseText(jsonString) + return new Request(json.requestId, json.input) + } + + @Override + Request transform(Request request) { + request + } + + @Override + String name() { + this.getClass().getSimpleName() + } + + @Override + byte systemCodecID() { + -1 + } +} diff --git a/src/main/groovy/com/serverless/vertx/ResponseCodec.groovy b/src/main/groovy/com/serverless/vertx/ResponseCodec.groovy new file mode 100644 index 0000000..2f64aa2 --- /dev/null +++ b/src/main/groovy/com/serverless/vertx/ResponseCodec.groovy @@ -0,0 +1,38 @@ +package com.serverless.vertx + +import com.serverless.lambda.Response +import groovy.json.JsonSlurper +import io.vertx.core.buffer.Buffer +import io.vertx.core.eventbus.MessageCodec +import org.springframework.stereotype.Component + +@Component +class ResponseCodec implements MessageCodec { + + @Override + void encodeToWire(Buffer buffer, Response response) { + buffer.appendString(response.asJson()) + } + + @Override + Response decodeFromWire(int pos, Buffer buffer) { + String jsonString = buffer.getString(0, buffer.length()) + Map json = JsonSlurper.newInstance().parseText(jsonString) + Response.builder().statusCode(json?.statusCode).body(json.body).headers(json.headers).build() + } + + @Override + Response transform(Response response) { + response + } + + @Override + String name() { + this.getClass().getSimpleName() + } + + @Override + byte systemCodecID() { + return -1 + } +} diff --git a/src/main/groovy/com/serverless/vertx/VertxConfig.groovy b/src/main/groovy/com/serverless/vertx/VertxConfig.groovy new file mode 100644 index 0000000..f0bb5b1 --- /dev/null +++ b/src/main/groovy/com/serverless/vertx/VertxConfig.groovy @@ -0,0 +1,31 @@ +package com.serverless.vertx + +import com.serverless.handler.MessageHandler +import com.serverless.lambda.Request +import com.serverless.lambda.Response +import io.vertx.core.Vertx +import io.vertx.core.eventbus.EventBus +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class VertxConfig { + + @Bean + EventBus eventBus(final List handlers, + final RequestCodec requestCodec, + final ResponseCodec responseCodec) { + def eventBus = Vertx.vertx().eventBus() + + // Register Codecs + eventBus.registerDefaultCodec(Request.class, requestCodec) + eventBus.registerDefaultCodec(Response.class, responseCodec) + + // Register Handlers + handlers?.each { + eventBus.consumer(it.getRouteChannel(), it) + } + + eventBus + } +} diff --git a/src/test/java/com/serverless/LambdaHandlerTest.groovy b/src/test/java/com/serverless/LambdaHandlerTest.groovy new file mode 100644 index 0000000..950a763 --- /dev/null +++ b/src/test/java/com/serverless/LambdaHandlerTest.groovy @@ -0,0 +1,33 @@ +package com.serverless + +import com.amazonaws.services.lambda.runtime.Context +import groovy.util.logging.Log4j +import org.junit.Test +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.ComponentScan + +@SpringBootApplication +@ComponentScan(basePackages = "com.serverless") +@Log4j +class LambdaHandlerTest { + @Test + void handleRequest() throws Exception { + + LambdaHandler mainHandler = new LambdaHandler() + + def input + def context = {} as Context + context.metaClass.getAwsRequestId = { UUID.randomUUID().toString() } + + + input = [httpMethod: 'GET', resource: '/users/{lastName}', pathParameters: [lastName: 'Jackson']] + mainHandler.handleRequest(input, context) + + input = [httpMethod : 'POST', resource: '/users/create', + queryStringParameters: [firstName: 'Rowell', lastName: 'Belen']] + mainHandler.handleRequest(input, context) + + input = [httpMethod: 'GET', resource: '/users'] + mainHandler.handleRequest(input, context) + } +} \ No newline at end of file