diff --git a/README.md b/README.md index 041a6d3..7644624 100644 --- a/README.md +++ b/README.md @@ -1,165 +1,212 @@ ![Alt logo](https://cdn.rawgit.com/noboomu/proteus/master/src/main/resources/io/sinistral/proteus/proteus-logo.svg) -An extremely lightweight, flexible, and fast [Swagger](http://swagger.io/) first REST API framework atop [Undertow](http://undertow.io). -A great deal of inspiration came from working with the following excellent projects: [Play](http://playframework.com), [Jooby](http://jooby.org), and [light-4j](https://github.com/networknt/light-4j). - -### Motivation - - Several years of working with the [Play](http://playframework.com) framework convinced us there had to be a better way. - - We faced a Goldilocks Crisis with the existing alternatives: [Jooby](http://jooby.org) did too much, [light-4j](https://github.com/networknt/light-4j) didn't do quite enough. - - We needed a framework that enabled us to write clean MVC REST controllers that created Swagger docs we could plug directly into the existing [codegen](https://github.com/swagger-api/swagger-codegen) solutions. - - We needed a framework with minimal overhead and performance at or near that of raw [Undertow](http://undertow.io). - -### Built With - - [Undertow](http://undertow.io) (server) - - [Guice](https://github.com/google/guice) (di) - - [Java Runtime Compiler](https://github.com/OpenHFT/Java-Runtime-Compiler) (runtime generated class compilation) - - [javapoet](https://github.com/square/javapoet) (runtime class generation) - - [Jackson](https://github.com/FasterXML/jackson-dataformat-xml) (xml) - - [jsoniter](http://jsoniter.com/) (json) - - [Logback](https://logback.qos.ch/) (logging) - - [Typesafe Config](https://github.com/typesafehub/config) (config) - - [Swagger](http://swagger.io/) (annotations and swagger spec) - - [JAX-RS](http://docs.oracle.com/javaee/6/api/javax/ws/rs/package-summary.html) (annotations only) +* An extremely lightweight, flexible, and fast [Swagger](http://swagger.io/) first REST API framework atop [Undertow](http://undertow.io). +* A great deal of inspiration came from working with the following excellent projects: [Play](http://playframework.com), [Jooby](http://jooby.org), and [light-4j](https://github.com/networknt/light-4j). -### Dependencies -* [JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/index.html) -* [Maven 3](http://maven.apache.org/) +## Motivation +--- +* Several years of working with the [Play](http://playframework.com) framework convinced us there had to be a better way. +* We faced a Goldilocks Crisis with the existing alternatives: [Jooby](http://jooby.org) did too much, [light-4j](https://github.com/networknt/light-4j) didn't do quite enough. +* We needed a framework that enabled us to write clean MVC REST controllers that created Swagger docs we could plug directly into the existing [codegen](https://github.com/swagger-api/swagger-codegen) solutions. +* We needed a framework with minimal overhead and performance at or near that of raw [Undertow](http://undertow.io). + +## Under the Hood +--- +Proteus takes your MVC controller classes and methods decorated with Swagger / JAX-RS annotations and generates native Undertow handler classes at runtime. You can review the generated code by setting the ```io.sinistral.proteus.server``` log level to `DEBUG`. + +## Setup +--- +Parameters are all configured in the `conf/application.conf` file. +Proteus applications generally have a main method that creates an instance of `io.sinistral.proteus.ProteusApplication`. +Prior to calling `start` on the `ProteusApplication` instance: +* Register `Service` classes via `addService` +* Register `Module` classes via `addModule` +* Register classes annotated with `io.swagger.annotations.Api` via `addController` + +Out of the box you get a [Swagger UI](https://github.com/swagger-api/swagger-ui) at `/swagger` and [Redoc](https://github.com/Rebilly/ReDoc) at `/swagger/redoc`. -### Setup - - We are very impressed by what Jooby has done with server configuration. - - Parameters are all configured in the ```conf/application.conf``` file. - - Proteus applications generally have a main method that creates an instance of ```io.sinistral.proteus.ProteusApplication``` - - Prior to calling ```start``` on the ```ProteusApplication``` instance: - - Register ```Service``` classes via ```addService``` - - Register ```Module``` classes via ```addModule``` - - Register classes annotated with ```io.swagger.annotations.Api``` via ```addController``` - ```java - public class ExampleApplication extends ProteusApplication +##### Example Application Class + +```java +public class ExampleApplication extends ProteusApplication +{ + public static void main( String[] args ) { - public static void main( String[] args ) - { - ExampleApplication app = new ExampleApplication(); - app.addService(io.sinistral.proteus.services.SwaggerService.class); - app.addService(io.sinistral.proteus.services.AssetsService.class); - app.addController(Benchmarks.class); - app.start(); - } + ExampleApplication app = new ExampleApplication(); + app.addService(io.sinistral.proteus.services.SwaggerService.class); + app.addService(io.sinistral.proteus.services.AssetsService.class); + app.addController(Examples.class); + app.start(); } - ``` - -### Under the Hood -- Proteus takes your MVC controller classes and methods decorated with Swagger / JAX-RS annotations and generates native Undertow handler classes at runtime. -- You can review the generated code by setting the ```io.sinistral.proteus.server``` log level to ```DEBUG```. - -### API Endpoints -- Classes with API endpoints respect standard Swagger / JAX-RS annotations: - ```java - @Api(tags="benchmarks") - @Path("/benchmarks") - @Produces((MediaType.APPLICATION_JSON)) - @Consumes((MediaType.MEDIA_TYPE_WILDCARD)) - ``` -- Individual endpoints also respect standard Swagger / JAX-RS annotations: - ```java - @GET - @Path("/plaintext") - @Produces((MediaType.TEXT_PLAIN)) - @ApiOperation(value = "Plaintext endpoint", httpMethod = "GET" ) - public void plaintext(HttpServerExchange exchange) - { - response("Hello, World!").contentType(PLAINTEXT_TYPE).send(exchange); - } - ``` -##### HttpServerExchange: Total Control - - For total control and maximum performance the raw ```HttpServerExchange``` can be passed to the endpoint method. - - Methods that take an ```HttpServerExchange``` as an argument should not return a value. - - In this case the method takes on full responsibility for completing the exchange. +} +``` +##### Example Controller Class +```java +import java.nio.ByteBuffer; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import io.swagger.annotations.*; +import io.sinistral.proteus.server.*; +import java.util.*; +import com.google.inject.Singleton; +import com.jsoniter.output.JsonStream; +import io.undertow.server.HttpServerExchange; +import static io.sinistral.proteus.server.ServerResponse.response; -##### ```ServerResponse``` -- The static method ```io.sinistral.proteus.server.ServerResponse.response``` helps create ```ServerResponse``` instances that are the preferred return type for endpoint methods. -- For methods that should return a String or ByteBuffer to the client users can create responses like this: - ```java +@Api(tags="examples") +@Path("/examples") +@Produces((MediaType.APPLICATION_JSON)) +@Consumes((MediaType.WILDCARD)) +@Singleton +public class Examples +{ @GET - @Path("/plaintext2") + @Path("/echo") @Produces((MediaType.TEXT_PLAIN)) - @ApiOperation(value = "Serve a plaintext message using a ServerResponse", httpMethod = "GET" ) - public io.sinistral.proteus.server.ServerResponse plaintext2(String message) - { - return io.sinistral.proteus.server.ServerResponse.response("Hello, World!").contentType(PLAINTEXT_TYPE); - } - ``` -- By default, passing a String to the response helper will convert it into a ByteBuffer. -- For other types of responses the following demonstrates the preferred style: - ```java - @GET - @Path("/world") - @Produces((MediaType.APPLICATION_JSON)) - @ApiOperation(value = "Return a world JSON object", httpMethod = "GET", response=World.class ) - public io.sinistral.proteus.server.ServerResponse getWorld(Integer id, Integer randomNumber ) + @ApiOperation(value = "Echo a message", httpMethod = "GET", response=String.class ) + public ServerResponse echo(String message) { - return io.sinistral.proteus.server.ServerResponse.response(new World(id,randomNumber)); + return response("Hello, World!").contentType(MediaType.TEXT_PLAIN); } - ``` -- The entity can be set separately as well (however, this removes the type checking): - ```java - @GET + + @GET @Path("/world") @Produces((MediaType.APPLICATION_JSON)) - @ApiOperation(value = "Return a world JSON object", httpMethod = "GET", response=World.class ) - public io.sinistral.proteus.server.ServerResponse getWorld(Integer id, Integer randomNumber ) + @ApiOperation(value = "Return a random world instance", httpMethod = "GET", response=World.class ) + public ServerResponse randomWorld(Integer id, Integer randomNumber ) { - return io.sinistral.proteus.server.ServerResponse.response().entity(new World(id,randomNumber)); + return response().entity(new World(id,randomNumber)); } - ``` -- ```CompletableFuture>``` is also supported: - ```java + @GET @Path("/future/user") @ApiOperation(value = "Future user endpoint", httpMethod = "GET" ) - public CompletableFuture> futureUser() + public CompletableFuture> responseFutureUser() { return CompletableFuture.completedFuture(response( new User(123L) ).applicationJson() ); } - ``` -- In this case a handler will be generated with the following source code: - ```java - public void handleRequest(final io.undertow.server.HttpServerExchange exchange) throws java.lang.Exception { - CompletableFuture> response = examplesController.futureUser(); - response.thenAccept( r -> r.applicationJson().send(this,exchange) ) - .exceptionally( ex -> { - throw new java.util.concurrent.CompletionException(ex); - } ); - } - ``` - -##### Endpoint Arguments - - a ```io.sinistral.proteus.server.ServerRequest``` can be added as an endpoint argument if the user wishes to access request properties that are not included in the argument list. - - Proteus is capable of parsing most types of endpoint arguments automatically so long as the type has a ```fromString```, ```valueOf```, or can be deserialized from JSON. - - Multi-part / Form file uploads can be passed to the endpoint methods as a ```java.io.File```, a ```java.nio.Files.Path```, or a ```java.nio.ByteBuffer``` - - Optional arguments are also supported, here is a more complex endpoint demonstrating several argument types: - ```java - @GET - @Path("/response/parameters/complex/{pathLong}") - @ApiOperation(value = "Complex parameters", httpMethod = "GET") - public ServerResponse> complexParameters( - final ServerRequest serverRequest, - @PathParam("pathLong") final Long pathLong, - @QueryParam("optionalQueryString") Optional optionalQueryString, - @QueryParam("optionalQueryLong") Optional optionalQueryLong, - @QueryParam("optionalQueryDate") Optional optionalQueryDate, - @QueryParam("optionalQueryUUID") Optional optionalQueryUUID, - @HeaderParam("optionalHeaderUUID") Optional optionalHeaderUUID, - @QueryParam("optionalQueryEnum") Optional optionalQueryEnum, - @HeaderParam("optionalHeaderString") Optional optionalHeaderString, - @QueryParam("queryUUID") UUID queryUUID, - @HeaderParam("headerString") String headerString, - @QueryParam("queryEnum") User.UserType queryEnum, - @QueryParam("queryIntegerList") List queryIntegerList, - @QueryParam("queryLong") Long queryLong - ) +} +``` + + +## Controller Classes +--- +### Annotations +##### Class +Controller classes respect standard Swagger / JAX-RS annotations: +```java +@Api(tags="benchmarks") +@Path("/benchmarks") +@Produces((MediaType.APPLICATION_JSON)) +@Consumes((MediaType.MEDIA_TYPE_WILDCARD)) +``` +##### Method +Controller class methods also respect standard Swagger / JAX-RS annotations: +```java +@GET +@Path("/plaintext") +@Produces((MediaType.TEXT_PLAIN)) +@ApiOperation(value = "Plaintext endpoint", httpMethod = "GET" ) +public void plaintext(HttpServerExchange exchange) +{ + response("Hello, World!").contentType(PLAINTEXT_TYPE).send(exchange); +} +``` + +### Return Types + +##### HttpServerExchange = Total Control + +For total control and maximum performance the raw `HttpServerExchange` can be passed to the controller method. +Methods that take an `HttpServerExchange` as an argument should __not__ return a value. +In this case the method takes on __full responsibility__ for completing the exchange. + +##### ServerResponse = Convenience +The static method ```io.sinistral.proteus.server.ServerResponse.response``` helps create ```ServerResponse``` instances that are the preferred return type for controller methods. +If the response object's `contentType` is not explicitly set, the `@Produces` annotation is used in combination with the `Accept` headers to determine the `Content-Type`. +For methods that should return a `String` or `ByteBuffer` to the client users can create responses like this: + ```java +@GET +@Path("/hello-world") +@Produces((MediaType.TEXT_PLAIN)) +@ApiOperation(value = "Serve a plaintext message using a ServerResponse") +public ServerResponse plaintext(String message) +{ + return ServerResponse.response("Hello, World!").contentType(PLAINTEXT_TYPE); +} +``` +By default, passing a `String` to the static `ServerResponse.response` helper function will convert it into a `ByteBuffer`. +For other types of responses the following demonstrates the preferred style: +```java +@GET +@Path("/world") +@Produces((MediaType.APPLICATION_JSON)) +@ApiOperation(value = "Return a world JSON object", httpMethod = "GET", response=World.class ) +public io.sinistral.proteus.server.ServerResponse getWorld(Integer id, Integer randomNumber ) +{ + return io.sinistral.proteus.server.ServerResponse.response(new World(id,randomNumber)); +} +``` +The entity can be set separately as well: +> this disables static type checking +```java +@GET +@Path("/world") +@Produces((MediaType.APPLICATION_JSON)) +@ApiOperation(value = "Return a world JSON object", httpMethod = "GET", response=World.class ) +public io.sinistral.proteus.server.ServerResponse getWorld(Integer id, Integer randomNumber ) +{ + return io.sinistral.proteus.server.ServerResponse.response().entity(new World(id,randomNumber)); +} +``` +`CompletableFuture>` can also be used as a response type: +```java +@GET +@Path("/future/user") +@ApiOperation(value = "Future user endpoint", httpMethod = "GET" ) +public CompletableFuture> futureUser() +{ + return CompletableFuture.completedFuture(response( new User(123L) ).applicationJson() ); +} +``` +In this case a handler will be generated with the following source code: +```java +public void handleRequest(final io.undertow.server.HttpServerExchange exchange) throws java.lang.Exception { + CompletableFuture> response = examplesController.futureUser(); + response.thenAccept( r -> r.applicationJson().send(this,exchange) ) + .exceptionally( ex -> { + throw new java.util.concurrent.CompletionException(ex); + } ); +} +``` + +### Arguments +A ```io.sinistral.proteus.server.ServerRequest``` can be added as an endpoint argument if the user wishes to access request properties that are not included in the argument list. +Proteus is capable of parsing most types of endpoint arguments automatically so long as the type has a ```fromString```, ```valueOf```, or can be deserialized from JSON. +Multipart/Form file uploads can be passed to the endpoint methods as a ```java.io.File```, a ```java.nio.Files.Path```, or a ```java.nio.ByteBuffer```. +Optional arguments are also supported, here is a more complex endpoint demonstrating several argument types: +```java +@GET +@Path("/response/parameters/complex/{pathLong}") +@ApiOperation(value = "Complex parameters", httpMethod = "GET") +public ServerResponse> complexParameters( + final ServerRequest serverRequest, + @PathParam("pathLong") final Long pathLong, + @QueryParam("optionalQueryString") Optional optionalQueryString, + @QueryParam("optionalQueryLong") Optional optionalQueryLong, + @QueryParam("optionalQueryDate") Optional optionalQueryDate, + @QueryParam("optionalQueryUUID") Optional optionalQueryUUID, + @HeaderParam("optionalHeaderUUID") Optional optionalHeaderUUID, + @QueryParam("optionalQueryEnum") Optional optionalQueryEnum, + @HeaderParam("optionalHeaderString") Optional optionalHeaderString, + @QueryParam("queryUUID") UUID queryUUID, + @HeaderParam("headerString") String headerString, + @QueryParam("queryEnum") User.UserType queryEnum, + @QueryParam("queryIntegerList") List queryIntegerList, + @QueryParam("queryLong") Long queryLong + ) { - Map responseMap = new HashMap<>(); responseMap.put("optionalQueryString", optionalQueryString.orElse(null)); responseMap.put("optionalQueryLong", optionalQueryLong.orElse(null)); @@ -176,61 +223,83 @@ A great deal of inspiration came from working with the following excellent proje responseMap.put("queryIntegerList", queryIntegerList); return response(responseMap).applicationJson(); } - ``` -### Services -- Proteus comes with two standard services that extend the ```io.sinistral.proteus.services.BaseService``` class. -- ##### AssetsService - - The AssetsService mounts an asset directory at a given path and is configured in your ```application.conf``` file. - - The default configuration: - ``` - assets { - # the base path assets will be server from - path = "/public" - # the directory to load the assets from - dir = "./assets" - cache { - # cache timeout for the assets - time = 500 - } +``` +## Services +--- +Proteus comes with two standard services that extend the ```io.sinistral.proteus.services.BaseService``` class. +##### AssetsService +The AssetsService mounts an asset directory at a given path and is configured in your ```application.conf``` file. +The default configuration: +``` +assets { + # the base path assets will be server from + path = "/public" + # the directory to load the assets from + dir = "./assets" + cache { + # cache timeout for the assets + time = 500 + } +} +``` +##### SwaggerService +The SwaggerService generates a swagger-spec file from your endpoints and serves a swagger-ui and spec. +The service is configured in your ```application.conf``` file. +The default configuration: +``` +swagger { + # the path that has an index.html template and theme css files + resourcePrefix="io/sinistral/proteus/swagger" + # swagger version + swagger: "2.0" + info { + # swagger info title + title = ${application.name} + # swagger info version + version = ${application.version} } - ``` -- ##### SwaggerService - - The SwaggerService generates a swagger-spec file from your endpoints and serves a swagger-ui and spec. - - The service is configured in your ```application.conf``` file. - - The default configuration: - ``` - swagger { - # the path that has an index.html template and theme css files - resourcePrefix="io/sinistral/proteus/swagger" - # swagger version - swagger: "2.0" - info { - # swagger info title - title = ${application.name} - # swagger info version - version = ${application.version} - } - # swagger-ui theme from ostranmes swagger-ui-themes, the following are built-in - # [feeling-blue, flattop, material, monokai, muted, newspaper, outline] - # specifying a different name causes the SwaggerService to search in - # {swagger.resourcePrefix}/themes for a file named "theme-{swagger.theme}.css" - theme="default" - # where the swagger endpoints will be mounted - basePath= ${application.path}"/swagger" - #the name of the spec file - specFilename="swagger.json" - consumes = ["application/json"] - produces = ["application/json"] - # supported schemes - schemes = ["http"] - } - ``` - -### Getting Started - - COMING SOON + # swagger-ui theme from ostranmes swagger-ui-themes, the following are built-in + # [feeling-blue, flattop, material, monokai, muted, newspaper, outline] + # specifying a different name causes the SwaggerService to search in + # {swagger.resourcePrefix}/themes for a file named "theme-{swagger.theme}.css" + theme="default" + # where the swagger endpoints will be mounted + basePath= ${application.path}"/swagger" + #the name of the spec file + specFilename="swagger.json" + consumes = ["application/json"] + produces = ["application/json"] + # supported schemes + schemes = ["http"] + } +``` + +## Getting Started +--- +COMING SOON -### Running +## Examples +--- +COMING SOON + + +### Dependencies +--- +* [JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/index.html) +* [Maven 3](http://maven.apache.org/) - mvn exec:exec + +### Built With +--- + - [Undertow](http://undertow.io) (server) + - [Guice](https://github.com/google/guice) (di) + - [Java Runtime Compiler](https://github.com/OpenHFT/Java-Runtime-Compiler) (runtime generated class compilation) + - [javapoet](https://github.com/square/javapoet) (runtime class generation) + - [Jackson](https://github.com/FasterXML/jackson-dataformat-xml) (xml) + - [jsoniter](http://jsoniter.com/) (json) + - [Logback](https://logback.qos.ch/) (logging) + - [Typesafe Config](https://github.com/typesafehub/config) (config) + - [Swagger](http://swagger.io/) (annotations and swagger spec) + - [JAX-RS](http://docs.oracle.com/javaee/6/api/javax/ws/rs/package-summary.html) (annotations only) diff --git a/src/main/java/io/sinistral/proteus/ProteusApplication.java b/src/main/java/io/sinistral/proteus/ProteusApplication.java index 74b28bd..334fc05 100644 --- a/src/main/java/io/sinistral/proteus/ProteusApplication.java +++ b/src/main/java/io/sinistral/proteus/ProteusApplication.java @@ -86,7 +86,7 @@ public class ProteusApplication @Inject protected Config config; - protected List registeredModules = new ArrayList<>(); + protected List> registeredModules = new ArrayList<>(); protected Injector injector = null; protected ServiceManager serviceManager = null; @@ -129,7 +129,11 @@ public void start() return; } - injector = injector.createChildInjector(registeredModules); + log.info("Configuring modules..."); + + Set modules = registeredModules.stream().map(mc -> injector.getInstance(mc)).collect(Collectors.toSet()); + + injector = injector.createChildInjector(modules); if (rootHandlerClass == null && rootHandler == null) { @@ -291,7 +295,7 @@ public ProteusApplication addController(Class controllerClass) return this; } - public ProteusApplication addModule(Module module) + public ProteusApplication addModule(Class module) { registeredModules.add(module); return this; diff --git a/src/main/resources/io/sinistral/proteus/swagger/index.html b/src/main/resources/io/sinistral/proteus/swagger/index.html index b5c438e..329afc4 100644 --- a/src/main/resources/io/sinistral/proteus/swagger/index.html +++ b/src/main/resources/io/sinistral/proteus/swagger/index.html @@ -5,7 +5,7 @@ {{ title }} - +