1 Introduction
Ratpack is a set of Java libraries that facilitate fast, efficient, evolvable and well tested HTTP applications.
It is built on the highly performant and efficient Netty event-driven networking engine.
Ratpack focuses on allowing HTTP applications to be efficient, modular, adaptive to new requirements and technologies, and well-tested over time.
1.1 Compared to…
This section contrasts Ratpack with other similar types of technologies to help explain what Ratpack is and what it isn’t.
If you disagree with description given of a particular technology/framework below, please edit this documentation on GitHub. Moreover, if you think that another comparison is warranted please request it.
1.1.1 Netty
Ratpack is built on top of Netty. Netty is a low level, extremely performant, general networking engine. It is extensible, well documented and very stable. It provides HTTP support out of the box, along with other common protocols, but importantly can also be used for custom protocols.
Netty is more performant than Ratpack, as it is at a lower level of abstraction. It provides practically no support for structuring an application beyond the networking protocol concerns. That is, to build a non trivial HTTP application in pure Netty would require writing a lot of the kind of support Ratpack provides. While Netty is more performant, Ratpack does not add significant overhead. Performance is a key concern for Ratpack.
Ratpack does not completely abstract over Netty. You can access Netty through Ratpack’s API and potentially use it directly. This could be used to implement support for custom, non HTTP oriented, networking protocols in your application.
1.1.2 Vert.x
Ratpack is similar to Vert.x in some respects:
- Built on Netty
- Not based on J2EE or Servlets
- Non blocking
A key difference between Vert.x and Ratpack is that Vert.x is a container, where Ratpack is a set of libraries (Vert.x can be embedded, but is typically not). As such, it attempts to address a broader set of concerns than Ratpack.
Vert.x applications are a composed set of “verticles” communicating via an unstructured JSON (in terms of schema) distributed bus. Vert.x provides support for many different programming languages, even making it easy to compose an application out of verticles of different languages.
Vert.x makes certain irreversible decisions about how applications should be written and architected. It provides horizontal scaling, a message bus, development time support and more. In contrast Ratpack is less prescriptive, giving you more freedom (and responsibility) to build your app from the ground up with the technologies you choose.
Vert.x has a plugin system and a healthy ecosystem of plugins, generally collaborating around the message bus. The “Architecture” chapter of this manual discusses why Ratpack does not have a plugin mechanism, and why it doesn’t need one.
Vert.x supports many protocols like Netty. Its API is closer to Netty’s, and completely encapsulates it. As such, it is (not significantly) more performant than Ratpack and more suitable for lower level networking.
Ratpack is more focussed on HTTP applications than Vert.x. Ratpack provides more support for structuring, composing, evolving and testing request handling logic than Vert.x. A more apt comparison than Ratpack and Vert.x would be a HTTP oriented framework built on top of Vert.x, such as Yoke.
Another key difference is that the Vert.x API is callback based. Ratpack use the concept of a “promise” instead (see the “Async” chapter for more info). Moreover, Ratpack’s handler pipeline is designed to support composition of asynchronous functions that process requests (called handlers) without callbacks. Both Vert.x and Ratpack integrate with RxJava for composition of generic asynchronous functions through add-ons. See RxVertx, and the “RxJava” chapter of this manual.
Architecturally Vert.x integrates application components via its own implementation of the Actor model (i.e. via message passing on its own event bus). There is no equivalent Ratpack feature. You are free to choose your own approach for higher level composition.
1.1.3 RxNetty
https://github.com/Netflix/RxNetty
RxNetty is the integration of RxJava and Netty. It is slightly more than a “reactive” layer over Netty. It is a young project.
A lot of the comparison between Ratpack and Netty given previously is applicable to comparing Ratpack and RxNetty. Most users of Ratpack will never need to access Netty’s API. Ratpack’s API is a different “shape” to Netty’s as it is more tightly focussed, therefore comparing the Ratpack and RxNetty APIs is not that useful.
Ratpack provides an RxJava integration library that can be used to bridge Ratpack promises and RxJava observables. The use of RxJava is recommended for all non trivial Ratpack applications.
1.1.4 Grails
Grails is a full stack, fully featured, MVC based web development framework (in the tradition of Ruby on Rails). It provides many deeply integrated features such as multi-store persistence (GORM), a view technology (GSP), a proprietary build system, testing support and has a vast ecosystem of plugins.
Grails does much more than Ratpack does. It is a framework and has very strong opinions about how to write and structure applications. It is based on the Servlet architecture and leverages the Spring Framework.
Grails is Groovy based, both in terms of internal implementation and language of choice for adopters. Ratpack is implemented in 100% Java, but provides a small adapter layer for Groovy. Ratpack is not a build system and makes no restrictions on language of choice for adopters other than providing functional Java APIs (which can easily be used from other JVM languages that bridge to Java).
Ratpack is more performant than Grails. However, Grails provides much more functionality.
1.1.5 Spark
Spark is a micro web framework. It is in the tradition of Ruby’s Sinatra.
At this point the question has to be asked about the relationship between Sinatra and Ratpack.
Ratpack was originally a Groovy based clone of Sinatra. It has since transformed from this and could no longer reasonably be considered to be in the tradition of Sinatra clones.
Spark is ultimately very simple and less ambitious than Ratpack. Spark is a blocking framework and does not enjoy the performance of Netty. It is Servlet based, but encapsulates the Servlet API.
Ratpack aims to scale to very large applications, both in terms of throughput and complexity. It achieves performance through its use of Netty (and non-blocking) and supports large applications through its coherent and non invasive API.
1.1.6 Dropwizard
https://dropwizard.github.io/dropwizard
TBD.
1.2 How to read the documentation
The canonical reference documentation for Ratpack is the Javadoc. This manual acts as a starting point for different aspects of Ratpack and as a mechanism to pull together larger ideas and patterns. The manual frequently introduces topics at a high level and links through to the Javadoc for detailed API information. Much work goes in to making the Javadoc (hopefully) useful.
1.3 Project Status
Ratpack is a new project, but stands on the shoulders of well established technologies and is built by developers with substantial experience in web application frameworks and tooling. It is currently pre 1.0 but under very active development.
No API compatibility guarantees are provided before the 1.0 release, from which point on the API will be strictly semantically versioned.
While it is new, it has received many man hours of development time.
2 Setup
Ratpack is purely a runtime. There is no installable package. To build Ratpack applications, you can use any JVM build tool. The Ratpack project provides specific support for Gradle through plugins, but any could be used.
Ratpack is published as a set of library JARs. The ratpack-core
library is the only strictly required library. Others such as ratpack-groovy
, ratpack-guice
, ratpack-jackson
, ratpack-test
etc. are optional.
With Ratpack on the classpath, you can use the API described in the next chapter to launch the application.
2.1 Using the Gradle plugin(s)
First, install Gradle if you haven’t already. On Mac OS X, if you have Homebrew installed, you can simply run brew install gradle
.
2.1.1 Using the Gradle Java plugin
Create a build.gradle
file with the following contents:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.10"
}
}
apply plugin: "io.ratpack.ratpack-java"
apply plugin: "idea"
repositories {
jcenter()
}
dependencies {
runtime "org.slf4j:slf4j-simple:1.7.5"
}
Create directories src/ratpack
and src/main/java
.
If desired, run gradle idea
to generate project files for IntelliJ and open the project.
Create a src/ratpack/ratpack.properties
file with the following contents:
handlerFactory=AppHandlerFactory
Create a src/main/java/AppHandlerFactory.java
with the following contents:
import ratpack.handling.Context;
import ratpack.handling.Handler;
import ratpack.launch.HandlerFactory;
import ratpack.launch.LaunchConfig;
import static ratpack.handling.Handlers.*;
public class AppHandlerFactory implements HandlerFactory {
@Override
public Handler create(LaunchConfig launchConfig) throws Exception {
return chain(
path("foo", new Handler() {
@Override
public void handle(Context context) {
context.render("from the foo handler");
}
}),
path("bar", new Handler() {
@Override
public void handle(Context context) throws Exception {
context.render("from the bar handler");
}
})
);
}
}
Run the project by running gradle run
, or create a distribution archive by running gradle distZip
.
For further information on using Ratpack with Gradle, please the Gradle chapter.
2.1.2 Using the Gradle Groovy plugin
Create a build.gradle
file with the following contents:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.10"
}
}
apply plugin: "io.ratpack.ratpack-groovy"
apply plugin: "idea"
repositories {
jcenter()
}
dependencies {
runtime "org.slf4j:slf4j-simple:1.7.5"
}
Create directories src/ratpack
and src/main/groovy
.
If desired, run gradle idea
to generate project files for IntelliJ and open the project.
Create a src/ratpack/ratpack.groovy
file with the following contents:
import static ratpack.groovy.Groovy.ratpack
ratpack {
handlers {
get("foo") {
render "from the foo handler"
}
get("bar") {
render "from the bar handler"
}
}
}
Run the project by running gradle run
, or create a distribution archive by running gradle distZip
.
For further information on using Ratpack with Gradle and Groovy, please the Gradle and Groovy chapters.
2.2 Using Lazybones project templates
Lazybones is a command line tool that allows you to generate a project structure for any framework based on pre-defined templates.
Ratpack’s Lazybones templates can be found on Bintray in the ratpack/lazybones repository. Templates are published with each Ratpack release and template versions are aligned with Ratpack release versions. There are different types of Ratpack templates available, listed in the repository. See the description of each for details.
See the Lazybones documentation for help with installing Lazybones.
Lazybones commands are in the format…
lazybones create <ratpack template> <ratpack version> <app name>
With Lazybones installed, creating a new Ratpack application is as easy as…
lazybones create ratpack my-ratpack-app
cd my-ratpack-app
./gradlew run
This will use the latest available version of Ratpack. If a specific version is required…
lazybones create ratpack x.x.x my-ratpack-app
cd my-ratpack-app
./gradlew run
Where x.x.x
is a valid template version. See the Bintray template repository for all available template versions.
3 Architecture
This chapter describes Ratpack applications at a high level.
3.1 Strongly typed
Ratpack is strongly typed. Beyond being implemented in Java, a strongly typed language, its API embraces types. For example, the notion of a Registry
is used extensively in Ratpack. A Registry
can be thought of as a map that uses types as keys.
This may be of most interest to Ratpack users implementing their applications in Groovy. Ratpack’s Groovy adapter uses the latest Groovy features to fully support static typing, while maintaining an idiomatic, concise, Groovy API.
3.2 Non blocking
Ratpack at its core is an event based (i.e. non-blocking) HTTP IO engine, and an API that makes it easy to structure response logic. Being non blocking imposes a different style of API than “traditional” blocking Java APIs in that the API must be asynchronous.
Ratpack aims to significantly simplify this style of programming for HTTP applications. It provides support for structuring asynchronous code (see the “Asynchronous & Non Blocking” chapter), and uses an innovative approach for structuring request processing into a self building, asynchronously traversed, graph of functions (it’s nowhere near as complicated to use as that may sound).
3.3 The parts
In the following section, “quotes” are used to denote key Ratpack terms and concepts.
A Ratpack application begins with a “launch configuration”, which as you would assume provides the configuration that is needed to start the application. One key piece of configuration provided by the “launch configuration” is the “handler factory”, which given the “launch configuration” creates a “handler”. A Ratpack “server” can be built and started solely from a “launch configuration”. The “server” once started, starts listening for requests. See the “Launching” chapter for more detail on this aspect.
The “handler” provided by the “handler factory” provided by the “launch configuration” is asked to respond to each request. A handler can do one of three things:
- Respond to the request
- Delegate to the “next” handler
- “Insert” handlers and immediately delegate to them
All request processing logic is simply the composition of handlers (see the Handlers
chapter for more detail on this aspect). Importantly, the processing is not bound to a thread and can be completed asynchronously. The “handler” API supports this asynchronous composition.
Handlers operate on a “context”. A “context” represents the state of request processing at that particular point in the handler graph. One of its key functions is to act as a “registry”, that can be used to retrieve objects by type. This allows handlers to retrieve strategy objects (typically just objects implementing key interfaces) from the “context” by public types. As handlers insert other handlers into the handler graph, they can contribute to the context registry. This allows handlers to contribute code (as strategy objects) to downstream handlers. See the “Context” chapter for more detail, and the following section for how this context registry is used in practice.
This has been a high level, abstract, description of a Ratpack application. It is likely unclear exactly how all this translates to real code. The rest of this manual, and the accompanying API reference will provide the detail.
3.4 Plugins and extensibility through the Registry
Ratpack has no notion of plugins. However, add-on integration with Google Guice facilitates a kind of plugin system through Guice modules. Guice is a dependency injection container. Guice modules define objects to be part of the dependency injection container. Guice modules can act as plugins by providing implementations of key Ratpack interfaces, that are used by handlers. When using the Guice integration, all of the objects known to Guice (typically through Guice modules) are obtainable via the “context registry”. That is, handlers can retrieve them by type.
To see why this is useful, we will use the requirement of rendering an object as JSON to the response. The “context” object given to a “handler”?has a render(Object) method. The implementation of this method simply searches the context registry for an implementation of Renderer
that can render objects of the given type. Because objects available to Guice are available through the registry, they may be used for rendering. Therefore, adding a Guice module with a Renderer
implementation for the desired type will allow it to be integrated into request processing. This is no different in concept to plain dependency injection.
While we have used the Guice integration in the above example, this approach is not tied to Guice (Guice is not part of Ratpack’s core API). Another dependency injection container (such as Spring) could easily be used, or no container at all. Any source of objects can be adapted to Ratpack’s Registry
interface (there is also a builder).
3.5 Services & business logic
Ratpack has no opinion on how you structure your code that isn’t related to request handling (i.e. business logic). We’ll use the term “service” as a catch all for an object that performs some kind of business logic.
Handlers can of course freely use whatever services they need to. There are two main patterns for access services from handlers:
- Provide the service to the handler when it is constructed
- Retrieve the service from the context registry
TODO - add chapter that elaborates on when to use which one of these approaches
3.5.1 Asynchronous APIs in services
One aspect of Ratpack that is relevant to non request handling code is the use of the ExecControl
for performing asynchronous operations. This allows services to perform async operations, using Ratpack’s event loop, and use Ratpack’s Promise
API.
An exec control can be obtained from the [execution controller attached to the launch config](api/ratpack/launch/LaunchConfig.html#getExecController())
4 Launching
This chapter introduces how to launch a Ratpack application, and the associated launch time configuration.
4.1 Launch configuration
To launch a Ratpack application, you must create a LaunchConfig
. This can then be used with the RatpackServerBuilder
method to create a RatpackServer
that can then be started.
Note that one of the LaunchConfig
object’s responsibilities is to provide a HandlerFactory
implementation. This factory is responsible for creating the handler that is effectively the Ratpack application. See the chapter on handlers for more details.
One option for building a LaunchConfig
is to use the LaunchConfigBuilder
. Another option is to use LaunchConfigs
which is able to build a launch config from system properties and a properties file.
4.2 RatpackMain
The RatpackMain
class is a convenient entry point (i.e. main class) that bootstraps a Ratpack application. When building with Gradle, this is the default main class.
The minimum requirement for launching a RatpackMain
based application, is to ensure there is a ratpack.properties
file on the JVM classpath that indicates the application HandlerFactory
…
handlerFactory=org.my.HandlerFactory
All aspects of the LaunchConfig can be specified in this properties file. Aspects can also be environmentally overridden by JVM system properties. See RatpackMain
for details.
5 Handlers
This chapter introduces handlers, which are the fundamental components of a Ratpack application.
5.1 What is a handler?
Conceptually, a handler (Handler
) is just a function that acts on a handling context (Context
).
The “hello world” handler looks like this…
import ratpack.handling.Handler;
import ratpack.handling.Context;
public class HelloWorld implements Handler {
public void handle(Context context) {
context.getResponse().send("Hello world!");
}
}
As we saw in the previous chapter, one of the mandatory launch config properties is the HandlerFactory
implementation that provides the primary handler. The handler that this factory creates is effectively the application.
This may seem limiting, until we recognise that a handler does not have to be an endpoint (i.e. it can do other things than generate a HTTP response). Handlers can also delegate to other handlers in a number of ways, serving more of a routing function. The fact that there is no framework level (i.e. type) distinction between a routing step and an endpoint offers much flexibility. The implication is that any kind of custom request processing pipeline can be built by composing handlers. This compositional approach is the canonical example of Ratpack’s philosophy of being a toolkit instead of a magical framework.
The rest of this chapter discusses aspects of handlers that are beyond HTTP level concerns (e.g. reading headers, sending responses etc.), which is addressed in the HTTP chapter.
5.2 Handler delegation
If a handler is not going to generate a response, it must delegate to another handler. It can either insert one or more handlers, or simply defer to the next handler.
Consider a handler that routes to one of two different handlers based on the request path. This can be implemented as…
import ratpack.handling.Handler;
import ratpack.handling.Context;
public class FooHandler implements Handler {
public void handle(Context context) {
context.getResponse().send("foo");
}
}
public class BarHandler implements Handler {
public void handle(Context context) {
context.getResponse().send("bar");
}
}
public class Router implements Handler {
private final Handler fooHandler = new FooHandler();
private final Handler barHandler = new BarHandler();
public void handle(Context context) {
String path = context.getRequest().getPath();
if (path.equals("foo")) {
context.insert(fooHandler);
} else if (path.equals("bar")) {
context.insert(barHandler);
} else {
context.next();
}
}
}
The key to delegation is the context.insert()
method that passes control to one or more linked handlers. The context.next()
method passes control to the next linked handler.
Consider the following…
import ratpack.handling.Handler;
import ratpack.handling.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PrintThenNextHandler implements Handler {
private final String message;
private final static Logger LOGGER = LoggerFactory.getLogger(PrintThenNextHandler.class);
public PrintThenNextHandler(String message) {
this.message = message;
}
public void handle(Context context) {
LOGGER.info(message);
context.next();
}
}
public class Application implements Handler {
public void handle(Context context) {
context.insert(
new PrintThenNextHandler("a"),
new PrintThenNextHandler("b"),
new PrintThenNextHandler("c")
);
}
}
Given that Application
is the primary handler (i.e. the one returned by the launch config’s HandlerFactory
), when this application receives a request the following will be written to System.out
…
a
b
c
And then what? What happens when the “c” handler delegates to its next? The last handler is always an internal handler that issues a HTTP 404 client error (via context.clientError(404)
which is discussed later).
Consider that inserted handlers can themselves insert more handlers…
import ratpack.handling.Handler;
import ratpack.handling.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PrintThenInsertOrNextHandler implements Handler {
private final String message;
private final Handler[] handlers;
private final static Logger LOGGER = LoggerFactory.getLogger(PrintThenInsertOrNextHandler.class);
public PrintThenInsertOrNextHandler(String message, Handler... handlers) {
this.message = message;
this.handlers = handlers;
}
public void handle(Context context) {
LOGGER.info(message);
if (handlers.length == 0) {
context.next();
} else {
context.insert(handlers);
}
}
}
public class Application implements Handler {
public void handle(Context context) {
context.insert(
new PrintThenInsertOrNextHandler("a",
new PrintThenInsertOrNextHandler("a.1"),
new PrintThenInsertOrNextHandler("a.2"),
),
new PrintThenInsertOrNextHandler("b",
new PrintThenInsertOrNextHandler("b.1",
new PrintThenInsertOrNextHandler("b.1.1")
),
),
new PrintThenInsertOrNextHandler("c")
);
}
}
This would write the following to System.out
…
a
a.1
a.2
b
b.1
b.1.1
c
This demonstrates how the next handler of the handler that inserts the handlers becomes the next handler of the last of the inserted handlers. You might need to read that sentence more than once.
You should be able to see a certain nesting capability emerge. This is important for composibility, and also for scoping which will be important when considering the registry context later in the chapter.
It would be natural at this point to think that it looks like a lot of work to build a handler structure for a typical web application (i.e. one that dispatches requests matching certain request paths to endpoints). Read on.
5.3 Building handler chains
Ratpack provides a suite of routing type handlers out of the box that make it easy to compose dispatch logic. These are available via the static methods of the Handlers
class.
For example, the path(String, Handler)
method can be used for path based routing.
import ratpack.handling.Handler;
import ratpack.handling.Context;
import ratpack.launch.LaunchConfig;
import ratpack.launch.HandlerFactory;
import static ratpack.handling.Handlers.path;
import static ratpack.handling.Handlers.get;
import static ratpack.handling.Handlers.chain;
public class SomeHandler implements Handler {
public void handle(Context context) {
// do some application work
}
}
public class Application implements HandlerFactory {
public Handler create(LaunchConfig launchConfig) {
return path("foo/bar", chain(get(), new SomeHandler()));
}
}
Here we have a HandlerFactory
that can be used when launching an app (see previous chapter). For this “application”:
- a GET request to
/foo/bar
would be routed to theSomeHandler
- a non-GET request to
/foo/bar
would produce a HTTP 405 (method not allowed) - anything else would produce a HTTP 404
This is easier than doing it all yourself, but we can do better. We can use the chain()
method and the Chain
DSL.
import ratpack.handling.Handler;
import ratpack.launch.HandlerFactory;
import ratpack.launch.LaunchConfig;
import static ratpack.handling.Handlers.chain;
public class Application implements HandlerFactory {
public Handler create(LaunchConfig launchConfig) throws Exception {
return chain(launchConfig, (chain) -> chain
.prefix("api", (api) -> api
.delete("someResource", context -> {
// delete the resource
}))
.assets("public")
.get("foo/bar", ctx -> {
// do stuff
})
);
}
}
The chain DSL is built on the existing delegation methods that have been presented so far. It is merely syntactic sugar. The Groovy version of this DSL is extremely sweet…
import ratpack.handling.Handler
import ratpack.launch.LaunchConfig
import ratpack.launch.HandlerFactory
import static ratpack.groovy.Groovy.chain
class Application implements HandlerFactory {
Handler create(LaunchConfig launchConfig) {
chain(launchConfig) {
prefix("api") {
delete("someResource") {
// delete the resource
}
}
assets("public")
get("foo/bar") {
// do stuff
}
}
}
}
See the chapter on Groovy for more information on using Groovy with Ratpack.
6 Context
The Context
type is at the core of Ratpack.
It provides:
- Access the HTTP
Request
andResponse
- Delegation and flow control (via the
next()
andinsert()
methods) - Access to contextual objects
- Convenience for common handler operations
For working directly with the request/response, see the HTTP chapter.
For information about delegation, see the Handlers chapter.
6.1 Contextual objects
The context is a registry. It provides access to via-type-lookup of objects that were made available upstream in the handler pipeline. This is the mechanism for inter-handler collaboration in Ratpack.
Consider the following example:
import ratpack.test.embed.EmbeddedApp;
import static ratpack.registry.Registries.just;
public class Example {
public static interface Person {
String getId();
String getStatus();
String getAge();
}
public static class PersonImpl implements Person {
private final String id;
private final String status;
private final String age;
public PersonImpl(String id, String status, String age) {
this.id = id;
this.status = status;
this.age = age;
}
@Override
public String getId() {
return id;
}
@Override
public String getStatus() {
return status;
}
@Override
public String getAge() {
return age;
}
}
public static void main(String... args) {
EmbeddedApp
.fromChain(chain -> chain
.prefix("person/:id", (personChain) -> personChain
.handler(context -> {
String id = context.getPathTokens().get("id"); // (1)
Person person = new PersonImpl(id, "example-status", "example-age");
context.next(just(Person.class, person)); // (2)
})
.get("status", context -> {
Person person = context.get(Person.class); // (3)
context.render("person " + person.getId() + " status: " + person.getStatus());
})
.get("age", context -> {
Person person = context.get(Person.class); // (4)
context.render("person " + person.getId() + " age: " + person.getAge());
}))
)
.test(httpClient -> {
assert httpClient.get("person/10/status").getBody().getText().equals("person 10 status: example-status");
assert httpClient.get("person/6/age").getBody().getText().equals("person 6 age: example-age");
});
}
}
At (2)
we are pushing the Person
instance into the registry for the downstream handlers to use and at (3)
and (4)
how they retrieve it. We are decoupling the creation detail from the usage, and avoiding duplicating the creation code in the status
and age
handlers. The benefit of avoiding duplication is obvious. What’s slightly more subtle is that the decoupling makes testing easier when the downstream handlers are not implemented as anonymous classes (see the Testing chapter for for information).
At (1)
we are also using contextual objects. The prefix()
chain method binds on a request path, potentially capturing tokens. If the binding is successful, a PathBinding
object is registered with the context that describes the binding result. This includes any path tokens that were captured as part of the binding. In the case above, we are capturing the second path component as the id
. The getPathTokens()
method on a context is literally shorthand for get(PathBinding.class).getPathTokens()
on the same context. This is another example of using the context object mechanism for inter-handler communication.
Another example of using contextual objects is the shorthand for accessing files from the file system. Consider the following script, which makes use of the context’s file
method to retrieve a static asset from the file system:
import static ratpack.groovy.Groovy.ratpack
ratpack {
handlers {
get {
def f = file('../')
render f ?: "null-value"
}
}
}
In the above example, the context’s file()
method is being called to retrieve a java.io.File
instance for the provided path. The context’s file()
method is a shorthand to retrieve the FileSystemBinding
object from the registry, and literally is a shorthand to get(FileSystemBinding.class).file(path/to/file)
. The context will always resolve file assets relative to the application root, so in the case where an absolute path is provided, it should be noted that the path to the asset will be prefixed by the path in which the application exists. For example, if your application exists in /home/ratpack/app
and your handler uses the file
method to resolve /etc/passwd
, then the actual path that is resolved will be /home/ratpack/app/etc/passwd
. In the case where a file cannot be resolved from within the application’s root, the file()
method may return a null value, which is demonstrated in the above example. The developer is responsible for handling scenarios where accessing a file may return a null object.
6.1.1 Partitioning
The context object mechanism supports partitioning application logic by providing different objects to different partitions. This is because objects registered with context are implicitly scoped, depending on how they were registered. Objects registered with the next()
methods are available to all downstream handlers that were part of the same insertion (i.e. context.insert()
including and nested insertions. Objects registered with the insert()
methods are available to the inserted handlers and nested insertions.
A typical use for this is using different error handling strategies for different parts of your application.
import ratpack.error.ServerErrorHandler;
import ratpack.test.embed.EmbeddedApp;
public class Example {
public static void main(String... args) {
EmbeddedApp.fromChain(chain -> chain
.prefix("api", api -> api
.register(r -> r.add(ServerErrorHandler.class, (context, throwable) ->
context.render("api error: " + throwable.getMessage())
)
)
.handler(context -> {
throw new Exception("in api - " + context.getRequest().getPath());
})
)
.register(r -> r.add(ServerErrorHandler.class, (context, throwable) ->
context.render("app error: " + throwable.getMessage())
)
)
.handler(context -> {
throw new Exception("in app - " + context.getRequest().getPath());
})
).test(httpClient -> {
assert httpClient.get("api/foo").getBody().getText().equals("api error: in api - api/foo");
assert httpClient.get("bar").getBody().getText().equals("app error: in app - bar");
});
}
}
7 Basic HTTP
This chapter introduces how to deal with basic HTTP concerns such as parsing requests, rendering responses, content negotiation file uploads etc.
7.1 Request & Response
The context object that a handler operates on provides the (getRequest()
& getResponse()
methods for accessing the Request
and Response
respectively. These objects provide more or less what you would expect.
For example, they both provide a getHeaders()
method that returns an model of the HTTP headers sent with the request and a model of the HTTP headers that are to be sent with the response. The Request
exposes other metadata attributes such as the HTTP method, the URI and a key/value model of the query string parameters among other things.
7.2 Reading the request
TODO introduce parsers generally
7.2.1 Forms
Ratpack provides a parser for Form
objects in the core. This can be used for reading POST’d (or PUT’d etc. for that matter) forms, both URL encoded and multi part (including file uploads).
Here’s an example of using this from Java…
import ratpack.handling.Handler;
import ratpack.handling.Context;
import ratpack.form.Form;
import ratpack.form.UploadedFile;
public class MyHandler implements Handler {
public void handle(Context context) {
Form form = context.parse(Form.class);
// Get the first attribute sent with name “foo”
String foo = form.get("foo");
// Get all attributes sent with name “bar”
List<String> bar = form.getAll("bar");
// Get the file uploaded with name “myFile”
UploadedFile myFile = form.file("myFile");
// Send back a response …
}
}
See Form
and UploadedFile
for more information and examples.
7.3 Sending a response
TODO introduce status methods
TODO introduce send methods
TODO introduce renderers
TODO introduce sendFile methods (pointing to use of render(file(?path?)))
instead.
TODO introduce beforeSend method and the ResponseMetaData interface.
Cookies
TODO introduce getCookies() on request and response
TODO introduce Request#oneCookie()
TODO introduce Response#expireCookie()
7.5 Sessions
TODO introduce ratpack-sessions library
8 Asynchronous & Non Blocking
Ratpack is designed for “asynchronous” & “non blocking” request processing. Its internal IO (e.g. HTTP request and response transfer) is all performed in a non blocking fashion (thanks to Netty). This approach yields higher throughput, lower resource usage, and importantly, more predictable behaviour under load. This programming model has become increasingly popular of late due to the Node.js platform. Ratpack is built on the same non blocking, event driven, model as Node.js.
Asynchronous programming is notoriously tricky. One of Ratpack’s key value propositions is that it provides constructs and abstractions to tame the asynchronous beast, yielding better performance while keeping implementation simple.
8.1 Comparison to blocking frameworks & containers
The Java Servlet API, that underpins most JVM web frameworks and containers, along with the majority of the JDK is fundamentally based on a synchronous programming model. Most JVM programmers are very familiar and comfortable with this programming model. In this model, when IO needs to be performed the calling thread will simply sleep until the operation is complete and the result is available. This model requires a reasonably large pool of threads. In a web application context, this usually means that each request is bound to a thread from the large pool and that the application can process ?X? number of parallel requests, where ?X? is the size of the thread pool.
Version 3.0 of the Servlet API does facilitate asynchronous request processing. However, retrofitting asynchronous support as an opt-in option is a different proposition to a completely asynchronous approach. Ratpack is asynchronous from the ground up.
The benefit of this model is that synchronous programming is unarguably “simpler”. The drawback of this model, opposed to a non blocking model, is that it demands greater resource usage and yields lower throughput. In order to serve more requests in parallel, the size of the thread pool has to be increased. This creates more contention for compute resources and more cycles are lost to managing the scheduling of these threads, not to mention the increased memory consumption. Modern operating systems, and the JVM, are very good at managing this contention; however, it is still a scaling bottleneck. Moreover, it demands greater resource allocation, which is a serious consideration for modern pay-for-what-you-use deployment environments.
The asynchronous, non blocking, model does not require a large thread pool. This is possible because threads are never blocked waiting for IO. If IO needs to be performed, the calling thread registers a callback of some sort that will be invoked when the IO is done. This allows the thread to be used for other processing while the IO is occurring. Under this model, the thread pool is sized according to the number of processing cores available. Since the threads are always busy with computation, there is no point in having more threads.
Many Java APIs (
InputStream
,JDBC
, etc.) are predicated on a blocking IO model. Ratpack provides a mechanism for using such API while minimizing the blocking cost (discussed below).
Ratpack is fundamentally asynchronous in two key ways…
- HTTP IO is event driven / non blocking (thanks to Netty)
- Request handling is organised as a pipeline of asynchronous functions
The HTTP IO being event driven is largely transparent when using Ratpack. Netty just does its thing.
The second point is the key characteristic of Ratpack. It does not expect your code to be synchronous. Many web frameworks that have opt in asynchronous support have serious constraints and gotchas that become apparent when trying to perform complex (i.e. real world) async operations. Ratpack is asynchronous from the ground up. Moreover, it provides constructs and abstractions that facilitate complex asynchronous processing.
8.2 Performing blocking operations (e.g. IO)
Most applications are going to have to perform some kind of blocking IO. Many Java APIs do not offer asynchronous options (e.g. JDBC). Ratpack provides a simple mechanism for executing blocking operations in a separate thread pool. This avoids blocking request processing (i.e. compute) threads (which is a good thing), but does incur some overhead due to thread contention. If you have to use blocking IO APIs there is unfortunately no other option.
Let’s consider a contrived data store API. It is conceivable that communication with the actual data store requires IO (or if it is in memory, then its access requires waiting on one or more locks which has the same blocking effect). The API methods cannot be called on a request processing thread because they will block. Instead, we need to use the “blocking” API…
import ratpack.handling.InjectionHandler;
import ratpack.handling.Context;
import ratpack.func.Action;
import ratpack.test.UnitTest;
import ratpack.test.handling.HandlingResult;
import java.util.concurrent.Callable;
import java.util.Collections;
import java.io.IOException;
public class Example {
// Some API that performs blocking operations
public static interface Datastore {
int deleteOlderThan(int days) throws IOException;
}
// A handler that uses the API
public static class DeletingHandler extends InjectionHandler {
void handle(final Context context, final Datastore datastore) {
final int days = context.getPathTokens().asInt("days");
context
.blocking(() -> datastore.deleteOlderThan(days))
.then(i -> context.render(i + " records deleted"));
}
}
// Unit test
public static void main(String... args) throws Exception {
HandlingResult result = UnitTest.handle(new DeletingHandler(), fixture -> fixture
.pathBinding(Collections.singletonMap("days", "10"))
.registry(r -> r.add(Datastore.class, new Datastore() {
public int deleteOlderThan(int days) throws IOException {
return days;
}
}))
);
assert result.rendered(String.class).equals("10 records deleted");
}
}
The Callable
submitted as the blocking operation is executed asynchronously (i.e. the blocking()
method returns instantly), in a separate thread pool. The result that it returns will processed back on a request processing (i.e. compute) thread.
See the Context#blocking(Callable) method for more details.
8.3 Performing async operations
The Context#promise(Action<Fulfiller<T>>) for integrating with async APIs. It is essentially a mechanism for adapting 3rd party APIs to Ratpack’s promise type.
import ratpack.test.embed.EmbeddedApp;
public class Example {
public static void main(String... args) {
EmbeddedApp.fromHandler(ctx ->
ctx.promise((f) ->
new Thread(() -> f.success("hello world")).start()
).then(ctx::render)
).test(httpClient -> {
assert httpClient.getText().equals("hello world");
});
}
}
It is important to note that the promise is always fulfilled on a compute thread managed by Ratpack. When the “fulfiller” is invoked from a non Ratpack thread (perhaps it’s a thread managed by the 3rd party async API) the promise subscriber will be invoked on a Ratpack thread.
8.4 Async composition and avoiding callback hell
One of the challenges of asynchronous programming lies in composition. Non trivial asynchronous programming can quickly descend into a phenomenon known as “callback hell”, which is the term used to describe the incomprehensibility of many layers of nested callbacks.
Elegantly and cleanly composing async operations together into complex workflows is an area of rapid innovation at this time. Ratpack does not attempt to provide a framework for asynchronous composition. Instead, it aims to integrate and provide adapters to specialised tools for this task. An example of this approach is Ratpack’s integration with RxJava.
In general, integration is a matter of adapting Ratpack’s Promise
type with the composition primitive of the target framework.
9 Streams
Ratpack supports streaming data in a variety of ways. This chapter outlines the fundamentals of working with data streams in Ratpack and different ways to stream data.
9.1 The Reactive Streams API
Generally, streaming in Ratpack is based around the emerging Reactive Streams API standard.
From the Reactive Streams site:
Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure on the JVM.
Ratpack uses the Reactive Streams API, opposed to a proprietary API, to allow users to choose their reactive toolkit of choice. Reactive toolkits such as RxJava and Reactor will support bridging to the Reactive Streams API in the near future. However, it is not required to use a specialist reactive library if your needs are modest. Ratpack provides some useful utilities for dealing with streams via its Streams
class.
9.1.1 Back pressure
A key tenant of the Reactive Streams API is support for flow control via back pressure. This allows stream subscribers, which in the case of a HTTP server app is usually the HTTP client, to communicate to the publisher how much data they can handle. In extreme cases, without back pressure a slowly consuming client can exhaust resources on the server as the data producer produces data faster than it is being consumed, potentially filling up in memory buffers. Back pressure allows the data producer to match its rate of production with what the client can handle.
For more info on the importance of back pressure, please see the documentation from the Reactive Streams project.
Streaming a response always occurs via the Response.sendStream()
method. See the documentation for this method for more precise semantics of what back pressure means when streaming data.
9.2 Chunked transfer encoding
Ratpack supports chunked transfer encoding for arbitrary data streams by way of the ResponseChunks
renderable type.
9.3 Server-sent events
Ratpack supports server-sent events for streaming data to, primarily Javascript based, clients by way of the ServerSentEvents
renderable type.
9.4 Websockets
Ratpack supports streaming data over websockets by way of the WebSockets.websocketBroadcast()
method.
Ratpack also supports bidirectional websocket communication via the other websocket-opening methods of the WebSockets
class.
10 Testing Ratpack applications
Ratpack provides testing support, both at the functional and unit level. The ratpack-test
library contains the core support, and ratpack-test-groovy
provides convenient Groovy extensions.
Note: The ratpack
and ratpack-groovy
Gradle plugins auto configure these libraries to be added to the test classpath.
10.1 Unit testing
The primary integration point between Ratpack and your code is the Handler
contract. Ratpack provides the UnitTest.handle()
and GroovyUnitTest.handle()
for contriving the invocation of a handler. As the routing of requests is also implemented via handlers in Ratpack, this can also be used to test the routing.
Ratpack doesn’t couple to any particular test framework. It only provides some utilities that you can use from whatever test framework you choose. However, we strongly recommend using the Groovy based Spock Framework, even if your application is implemented in Java.
There is no special setup or teardown required for unit testing Ratpack applications, as by definition they require no server infrastructure.
10.2 Functional testing
Functional testing for Ratpack is built up around the ApplicationUnderTest.
This interface provides the address of the running application. Implementations of this interface will take care of starting the server for you.
10.2.1 TestHttpClient
Ratpack provides TestHttpClient in ratpack-test-groovy
, this is a client that makes it very simple to test status codes and responses.
Note below we use @Delegate so we just need to call get()
in the when block instead of client.get()
.
import ratpack.groovy.test.LocalScriptApplicationUnderTest
import ratpack.test.http.TestHttpClient
import ratpack.test.http.TestHttpClients
import ratpack.test.ApplicationUnderTest
class SiteSmokeSpec {
LocalScriptApplicationUnderTest aut = new LocalScriptApplicationUnderTest()
@Delegate TestHttpClient client = TestHttpClients.testHttpClient(aut)
def "Check Site Index"() {
get("index.html")
assert response.statusCode == 200
assert response.body.text.contains('<title>Ratpack: A toolkit for JVM web applications</title>')
}
def "Check Site Root"() {
get()
assert response.statusCode == 200
assert response.body.text.contains('<title>Ratpack: A toolkit for JVM web applications</title>')
}
def cleanup() {
aut.stop()
}
}
10.2.2 Geb
Geb can also be used we just need to set up the correct base URL and make sure the test app is shut down.
To set the correct base URL we will use the ServerBackedApplicationUnderTest instance to get the address and give that to the Geb browser instance.
For shutting down the app we will call stop in the cleanup function.
An example of a Geb based test is available here.
10.2.3 Application subsets, modules and extensions
The ratpack.test.embed
package (provided by the ratpack-test
library) provides the EmbeddedApp
type. These can be used to functionally test features in isolation, by creating small Ratpack apps within test cases and is actually the basis for how Ratpack itself is tested. This approach is most commonly used for functionally testing reusable Ratpack components (e.g. Guice modules) but can also be used for functionally testing a subset of an application.
The ratpack.groovy.test.embed
package (provided by the ratpack-groovy-test
library) provides the GroovyEmbeddedApp
Groovy specialization. This is the preferred implementation to use as it provides Guice support, flexibility and is easy to use.
The EmbeddedApp
type extends the ApplicationUnderTest
type. This makes them convenient to use with the TestHttpClient
mechanism provided by the ratpack-groovy-test
library.
11 Google Guice integration
The ratpack-guice
jar provides integration with Google Guice for modularisation and dependency injection. It is typically used in most Ratpack applications, but it is not a mandatory dependency. However, the ratpack-groovy
jar does depend on the Guice integration.
See the Guice package API documentation for detailed usage information.
11.1 Modules
Guice provides the concept of a module, which is a kind of recipe for providing objects. See Guice’s “Getting Started” documentation for details.
The ratpack-guice
library provides the BindingsSpec
type for specifying the bindings for the application.
11.2 Dependency injected handlers
The Guice integration gives you a means of decoupling the components of your application. You can factor out functionality into standalone (i.e. non Handler
) objects and use these objects from your handlers. This makes your code more maintainable and more testable. This is the standard “Dependency Injection” or “Inversion of Control” pattern.
The Guice class provides static handler()
factory methods for creating root handlers that are the basis of the application. These methods (the commonly used ones) expose Chain
instance that can be used to build the application’s handler chain. The instance exposed by these methods provide a registry (via the getRegistry()
) method that can be used to construct dependency injected handler instances.
See the documentation of the Guice class for example code.
11.3 Modules as plugins
Ratpack does not have a formal plugin system. However, reusable functionality can be packaged as Guice modules.
For example, that ratpack-jackson
library provides the JacksonModule
class which is a Guice module. To integrate Jackson into your Guice backed Ratpack application (e.g. for serializing objects as JSON), you simply use this module.
In Groovy script application this is as easy as:
import ratpack.jackson.JacksonModule
import static ratpack.jackson.Jackson.json
import static ratpack.groovy.Groovy.ratpack
ratpack {
bindings {
add new JacksonModule()
}
handlers {
get("some-json") {
render json(user: 1) // will render '{user: 1}'
}
}
}
See the Guice package API documentation for more info on registering modules.
See the Jackson package API documentation for more info on using the Jackson integration.
11.4 Guice and the context registry
TODO guice backed registry impl
This offers an alternative to dependency injected handlers, as objects can just be retrieved on demand from the context.
More usefully, this means that Ratpack infrastructure can be integrated via Guice modules. For example, an implementation of the ServerErrorHandler
can be provided by a Guice module. Because Guice bound objects are integrated into the context registry lookup mechanism, this implementation will participate in the error handling infrastructure.
This is true of all Ratpack infrastructure that works via context registry lookup, such as Renderer implementations for example.
12 Groovy
Groovy is an alternative JVM programming language. It has a strong synergy with Java and many language and library features that make it a compelling programming environment. Ratpack provides strong integration with Groovy via the ratpack-groovy
and ratpack-groovy-test
libraries. Writing Ratpack applications in Groovy generally leads to less code through Groovy’s concise syntax compared to Java and a more productive and enjoyable development experience. To be clear though, Ratpack applications do not need to be written in Groovy.
Groovy is commonly known as a dynamic language. However, Groovy 2.0 added full static typing and static compilation as an option. Ratpack’s Groovy support is strictly designed to fully support “static Groovy” and also leverages the newest features of Groovy to avoid introducing boilerplate code to achieve this goal. Said another way, Ratpack’s Groovy support does not use any dynamic language features and has a strongly typed API.
TODO: find decent links describing static Groovy and use above
12.1 Prerequisites
If you are new to Groovy, you may want to research the following foundational Groovy topics before proceeding:
- Closures
- The
def
keyword
TODO: what else should be in this list? Also, links needed
something else
12.2 Ratpack Groovy API
TODO: explain that Groovy API wraps Java API and mirrors it in corresponding ratpack.groovy.*
12.2.1 @DelegatesTo
@DelegatesTo
is aimed at documenting code and providing the IDE and static type checker/compiler with more type information at compile-time. Applying the annotation is particularly interesting for DSL authors.
Let’s consider the following Ratpack code snippet:
ratpack {
handlers {
get {
render "Hello world!"
}
}
}
This code is also known as being written in Ratpack’s “GroovyChain DSL”. It is essentially the same as
ratpack({
handlers({
get({
render("Hello world!")
})
})
})
The methods ratpack
, handlers
, get
and render
are called. In fact there is no restriction to call each method only once, for example, the code could have had multiple method invocations of get
, responding to different request URIs:
ratpack {
handlers {
get {
render "Hello world!"
}
get('foo') {
render "bar"
}
}
}
Notice how the calls to ratpack
, handlers
and get
build a hierarchy. But how is it actually transformed to calls to Groovy/Java objects?
That’s exactly where delegation comes into play. Groovy allows to change the target of method calls inside Closure
code blocks. Let’s have a look at a very basic example:
class Settings {
String host
Integer port
def host(String h) { this.host = h }
def port(Integer p) { this.port = p }
}
Settings settings(Closure dsl) {
def p = new Settings()
def code = dsl.clone() // better use: dsl.rehydrate(p, this, this)
code.delegate = p
code()
return p
}
// our DSL starts here and returns a Settings instance
Settings config = settings {
port 1234
host 'localhost'
}
assert config.host == 'localhost'
assert config.port == 1234
The code in the settings
DSL block calls methods which do not exist in the current lexical scope. At runtime, once the delegate
property is set though, Groovy additionally resolves the method against the given delegate object, in our case, the Settings
instance.
Delegation is commonly used in Groovy DSLs, as it is the case with the Ratpack DSL, to decouple DSL code from underlying objects.
This technique bears a problem for code completion in IDEs or the static type checker/compiler that has been added in Groovy 2. Running the following code in groovyConsole
class Settings {
String host
Integer port
def host(String h) { this.host = h }
def port(Integer p) { this.port = p }
}
Settings settings(Closure dsl) {
def p = new Settings()
def code = dsl.clone() // better use: dsl.rehydrate(p, this, this)
code.delegate = p
code()
return p
}
@groovy.transform.TypeChecked
void createConfig() {
Settings config = settings {
port 1234
host 'localhost'
}
assert config.host == 'localhost'
assert config.port == 1234
}
gives
[Static type checking] - Cannot find matching method ConsoleScript23#port(int). Please check if the declared type is right and if the method exists.
at line: 20, column: 7
[Static type checking] - Cannot find matching method ConsoleScript23#host(java.lang.String). Please check if the declared type is right and if the method exists.
at line: 21, column: 7
The type checker misses information about the delegate type Settings
at compile-time.
This is the point where @DelegatesTo
finally comes into play. It is exactly used in cases where this type information needs to be specified for Closure
method parameters:
// ...
// let's tell the compiler we're delegating to the Settings class
Settings settings(@DelegatesTo(Settings) Closure dsl) {
def p = new Settings()
def code = dsl.clone() // better use: dsl.rehydrate(p, this, this)
code.delegate = p
code()
return p
}
// ...
Ratpack uses @DelegatesTo
wherever Closure
method parameters are used. This does not only serve better code completion or the static type checker, but also for documentation purposes.
12.3 GroovyRatpackMain
The ratpack-groovy
library provides the GroovyRatpackMain
application entry point that bootstraps the Groovy support. This extends from RatpackMain
, but forces the handler factory to be an instance of GroovyScriptFileHandlerFactory.
12.4 ratpack.groovy script
TODO: introduce DSL used in this file, discuss reloading when in development mode
12.5 handlers {} DSL
TODO: introduce the
GroovyChain
DSL, and closures as handlers
12.6 GroovyChainAction
TODO: Point out the existence of GroovyChainAction
12.7 Testing
Groovy comes with built-in support for writing tests. Besides integrated support for JUnit, the programming language comes with features proven to be very valuable for test-driven development. One of them is the extended assert
keyword on which we’ll have a look at in the next section.
If you’re looking for Ratpack-specific test support documentation, there is a dedicated Ratpack testing guide section in this manual.
12.7.1 Power assertions
Writing tests means formulating assumptions by using assertions. In Java this can be done by using the assert
keyword that has been added in J2SE 1.4. Assertion statements in Java are disabled by default.
Groovy comes with a powerful variant of assert
also known as power assertion statement. Groovy’s power assert
differs from the Java version in its output given once the boolean expression validates to false
:
def x = 1
assert x == 2
// Output:
//
// Assertion failed:
// assert x == 2
// | |
// 1 false
The java.lang.AssertionError
contains an extended version of the original assertion error message. Its output shows all variable and expression values from the outer to the inner most expression.
Due to its expressional power, it has become common sense in the Groovy community to write test cases with the assert
statement instead of any assert*
methods provided by the testing library of choice.
The assert
statement is only one of the many useful Groovy features for writing tests. If you’re looking for a comprehensive guide of all Groovy language testing features, please consult the Groovy testing guide.
12.7.2 JUnit 3 support
Groovy comes with embedded JUnit 3 support and a custom TestCase
base class: groovy.util.GroovyTestCase
. GroovyTestCase
extends TestCase
and adds useful utility methods.
One example is the shouldFail
method family. Each shouldFail
method takes a Closure
and executes it. If an exception is thrown and the code block fails, shouldFail
catches the exception and the test method does not fail:
import groovy.util.GroovyTestCase
class ListTest extends GroovyTestCase {
void testInvalidIndexAccess1() {
def numbers = [1,2,3,4]
shouldFail {
numbers.get(4)
}
}
}
shouldFail
in fact returns the catched exception which allows for asserting on exception messages or types:
import groovy.util.GroovyTestCase
class ListTest extends GroovyTestCase {
void testInvalidIndexAccess1() {
def numbers = [1,2,3,4]
def msg = shouldFail {
numbers.get(4)
}
assert msg == 'Index: 4, Size: 4'
}
}
In addition there is a variant of the shouldFail
method that comes with a java.lang.Class
parameter before the Closure
parameter:
import groovy.util.GroovyTestCase
class ListTest extends GroovyTestCase {
void testInvalidIndexAccess1() {
def numbers = [1,2,3,4]
def msg = shouldFail(IndexOutOfBoundsException) {
numbers.get(4)
}
assert msg == 'Index: 4, Size: 4'
}
}
If the thrown exception would be another type, shouldFail
would rethrow the exception letting the test method fail
A complete overview of allGroovyTestCase
methods can be found in the JavaDoc documentation.
12.7.3 JUnit 4 support
Groovy comes with embedded JUnit 4 support. As of Groovy 2.3.0, the groovy.test.GroovyAssert
class can be seen as complement to Groovy’s GroovyTestCase
JUnit 3 base class. GroovyAssert
extends org.junit.Assert
by adding static utility methods for writing JUnit 4 tests in Groovy.
import static groovy.test.GroovyAssert.shouldFail
class ListTest {
void testInvalidIndexAccess1() {
def numbers = [1,2,3,4]
shouldFail {
numbers.get(4)
}
}
}
A complete overview of all GroovyAssert
methods can be found in the JavaDoc documentation.
12.7.4 Spock
Spock is a testing and specification framework for Java and Groovy applications. What makes it stand out from the crowd is its beautiful and highly expressive specification DSL. Spock specifications are written as Groovy classes.
Spock can be used for unit, integration or BDD (behavior-driven-development) testing, it doesn’t put itself into a specific category of testing frameworks or libraries.
In the following paragraphs we will have a first look at the anatomy of a Spock specification. It is not ment to be as seen as full blown documentation, it rather should give a pretty good feeling on what Spock is up to.
12.7.4.1 First steps
Spock lets you write specifications that describe features (properties, aspects) exhibited by a system of interest. The “system” can be anything between a single class and an entire application, a more advanced term for it is system under specification. The feature description starts from a specific snapshot of the system and its collaborators, this snapshot is called the feature’s fixture.
Spock specification classes are automatically derived from spock.lang.Specification
. A concrete specification class might consist of fields, fixture methods, features methods and helper methods.
Let’s have a look at a Ratack unit test specification:
import ratpack.groovy.test.LocalScriptApplicationUnderTest
import ratpack.test.http.TestHttpClient
import ratpack.test.http.TestHttpClients
import ratpack.test.ApplicationUnderTest
class SiteSpec {
LocalScriptApplicationUnderTest aut = new LocalScriptApplicationUnderTest()
@Delegate TestHttpClient client = TestHttpClients.testHttpClient(aut)
def "Check Site Index"() {
when:
get("index.html")
then:
response.statusCode == 200
response.body.text.contains('<title>Ratpack: A toolkit for JVM web applications</title>')
}
}
Spock feature specifications are defined as methods inside a spock.lang.Specification
class. They describe the feature by using a String literal instead of a method name. The specification above uses "Check Site Index"
to test the outcome of a request to index.html
.
The feature specification uses the when
and then
blocks. The when
block creates a so-called stimulus and is a companion of the then
block which describes the response to the stimulus. Notice how we can also leave out the assert
statements in the then
block. Spock will interpret Boolean expressions there correctly. The setup
block could have been used to configure local variables visible inside the feature method only.
12.7.4.2 More on Spock
Spock provides more advanced features like data tables or mocking we didn’t have a look at in this section. Feel free to consult the Spock GitHub page for more documentation.
13 RxJava
The excellent RxJava can be used in Ratpack applications to elegantly compose asynchronous operations.
The ratpack-rx
JAR provides with RxRatpack
class that provides static methods for adapting Ratpack promises to RxJava’s Observable.
The ratpack-rx
module as of 0.9.10 is built against (and depends on) RxJava 1.0.0-rc.4.
13.1 Initialization
The RxRatpack.initialize()
must be called to fully enable the integration. This method only needs to be called once for the JVM’s lifetime.
13.2 Observing Ratpack
The integration is based on the RxRatpack.observe()
and RxRatpack.observeEach()
static methods. These methods adapt Ratpack’s promise type into an observable, which can then be used with all of the observable operators that RxJava offers.
For example, blocking operations can be easily observed.
import ratpack.exec.Promise;
import ratpack.test.handling.HandlingResult;
import static ratpack.rx.RxRatpack.observe;
import static ratpack.test.UnitTest.requestFixture;
public class Example {
public static void main(String... args) throws Exception {
HandlingResult result = requestFixture().handle(context -> {
Promise<String> promise = context.blocking(() -> "hello world");
observe(promise).map(String::toUpperCase).subscribe(context::render);
});
assert result.rendered(String.class).equals("HELLO WORLD");
}
}
13.3 Implicit error handling
A key feature of the RxJava integration is the implicit error handling. All observable sequences have an implicit default error handling strategy of forwarding the exception to the execution context error handler. In practice, this means that error handlers rarely need to be defined for observable sequences.
import ratpack.error.ServerErrorHandler;
import ratpack.rx.RxRatpack;
import ratpack.test.UnitTest;
import ratpack.test.handling.HandlingResult;
import rx.Observable;
public class Example {
public static void main(String... args) throws Exception {
RxRatpack.initialize(); // must be called once per JVM
HandlingResult result = UnitTest.requestFixture().handleChain(chain -> {
chain.register(registry ->
registry.add(ServerErrorHandler.class, (context, throwable) ->
context.render("caught by error handler: " + throwable.getMessage())
)
);
chain.get(ctx -> Observable.<String>error(new Exception("!")).subscribe((s) -> {}));
});
assert result.rendered(String.class).equals("caught by error handler: !");
}
}
In this case, the throwable thrown during the blocking operation will be forwarded to the current ServerErrorHandler
, which will probably render an error page to the response. If the subscriber does implement an error handling strategy, it will be used instead of the implicit error handler.
The implicit error handling applies to all observables that are created on Ratpack managed threads. It is not restricted to observables that are backed by Ratpack promises.
14 Coda Hale Metrics
The ratpack-codahale-metrics
jar provides integration with Coda Hale’s Metrics library.
Coda Hale Metrics is one of the best metrics libraries out there for the JVM. It provides a toolkit of metric types and metric reporters that will give a deep insight into your application’s performance, whether that be at development time or real time in production. It allows you to easily capture statistics like the number of requests served or response times, and also more generic information like the state of your internal collections, queues or how many times some portion of code is being executed. By measuring your code you will know exactly what your code is doing when it runs and be able to make informed optimization decisions.
Ratpack’s integration with Coda Hale Metrics means that many of these key metrics are captured already for you, simply by registering the Guice module. Should you require further insight then Ratpack also makes it easy for you to capture additional metrics using the library’s many metric types and then report all of these metrics to your required output using the library’s metric reporters.
See CodaHaleMetricsModule
for detailed usage information.
14.1 Built-in metrics
Ratpack provides built-in metric collectors for key metrics. When metrics are enabled within your application using CodaHaleMetricsModule.metrics()
, the built-in metric collectors will automatically be enabled too.
Ratpack has built-in metric collectors for:
- Request timing
- Background operation timing
Ratpack also has support for Coda Hale Metric’s JVM instrumentation. See CodaHaleMetricsModule.jvmMetrics()
for usage information.
14.2 Custom metrics
Ratpack enables you to capture your own application metrics in two ways:
- Obtaining the
MetricRegistry
via dependency injection or context registry lookup and registering your own metrics with it. - Add metrics annotations to your Guice injected classes.
See CodaHaleMetricsModule.metrics()
for more details.
14.3 Reporting metrics
Ratpack supports metric reporters for the following outputs:
For an example of how to consume real-time metrics with websockets, see the example-books project.
14.4 Health checks
Health checks verify that your application components or responsibilities are performing as expected.
For detailed information on how to create your own application health checks and how to run them, see CodaHaleMetricsModule.healthChecks()
.
15 Building with Gradle
The recommended way to build Ratpack applications is to use the Gradle Build System, by way of the Gradle plugins provided by the Ratpack project.
Ratpack is purely a runtime toolkit and not also a development time tool like Ruby on Rails and Grails. This means that you can use whatever you like to build a Ratpack app. The provided Gradle plugins merely provide convenience and are not fundamental to Ratpack development.
15.1 Setup
The first requirement is to apply the Gradle plugin to your Gradle project…
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.10"
}
}
apply plugin: "io.ratpack.ratpack-java"
repositories {
jcenter()
}
Or for a Groovy based Ratpack project…
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.10"
}
}
apply plugin: "io.ratpack.ratpack-groovy"
repositories {
jcenter()
}
The 'io.ratpack.ratpack-java'
plugin applies the core Gradle 'java'
plugin. The 'io.ratpack.ratpack-groovy'
plugin applies the core Gradle 'groovy'
plugin. This means that you can start adding code and dependencies to your app like a standard Gradle based project (e.g. putting source in src/main/[groovy|java]
). Note that the 'io.ratpack.ratpack-groovy'
plugin implicitly applies the 'io.ratpack.ratpack-java'
plugin.
15.2 Ratpack dependencies
To depend on a Ratpack extension library, simply add it as a regular compile dependency…
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.10"
}
}
apply plugin: "io.ratpack.ratpack-groovy"
repositories {
jcenter()
}
dependencies {
compile ratpack.dependency("jackson")
}
Using ratpack.dependency("jackson")
is equivalent to "io.ratpack:ratpack-jackson:?version of ratpack-gradle dependency?"
. This is the recommended way to add dependencies that are part of the core distribution.
The 'io.ratpack.ratpack-java'
plugin adds the following implicit dependencies:
ratpack-core
- compileratpack-test
- testCompile
The 'io.ratpack.ratpack-groovy'
plugin adds the following implicit dependencies:
ratpack-groovy
- compile (depends onratpack-core
)ratpack-groovy-test
- testCompile (depends onratpack-test
)
The available libraries can be browsed via Bintray. All Ratpack jars are published to both Bintray’s JCenter and Maven Central.
15.3 The ‘application’ plugin
Both the 'ratpack-java'
and 'ratpack-groovy'
plugins also apply the core Gradle 'application'
plugin. This plugin provides the ability to create a standalone executable distribution of your software. This is the preferred deployment format for Ratpack applications.
The 'application'
plugin requires the main class (i.e. entry point) of your application to be specified. This is preconfigured by the 'ratpack-java'
and 'ratpack-groovy'
plugins to be the RatpackMain
and GroovyRatpackMain
respectively. This can be changed if you wish to use a custom entry point (consult the 'application'
plugin documentation).
15.4 The ‘shadow’ plugin
Both the 'ratpack-java'
and 'ratpack-groovy'
plugins ship with integration support for the 3rd party 'shadow'
plugin. This plugin provides the ability to create a self-contained “fat-jar” that includes your ratpack application and any compile and runtime dependencies.
The plugins react to the application of the 'shadow'
plugin and configure additional task dependencies. They do not apply the 'shadow'
plugin and, for compatibility reasons, do not ship with a version of the 'shadow'
as a dependency.
To use the 'shadow'
integration, you will need to include the dependency in your project and apply the plugin.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.10"
classpath 'com.github.jengelman.gradle.plugins:shadow:1.0.2'
}
}
apply plugin: "io.ratpack.ratpack-java"
apply plugin: 'com.github.johnrengelman.shadow'
repositories {
jcenter()
}
The latest version of the 'shadow'
plugin can be found on the project’s Github page.
You can now have the build generate the fat-jar, by running…
./gradlew shadowJar
15.5 The base dir
The src/ratpack
directory in the Gradle project effectively becomes the base dir of your Ratpack application. That is, these are the files that are visible to your application (e.g. static files to serve).
This directory will be included in the distribution built by the 'application'
plugin as the app
directory. This directory will be added to the classpath when starting the application, and will also be the JVM working directory.
15.5.1 ratpack.properties and launch configuration
It is a good idea to immediately put a (potentially empty) ratpack.properties
file in the src/ratpack
directory. When the application is launched, this file contributes to the application LaunchConfig
.
For example, to configure the maximum request size that the application will accept, add the following to the src/ratpack/ratpack.properties
file…
ratpack.maxContentLength=1024
See Launching for more information about specifying the effective LaunchConfig
for the application.
15.5.2 The ‘ratpack.groovy’ script
The 'ratpack-groovy'
plugin expects the main application definition to be located at either src/ratpack/ratpack.groovy
or src/ratpack/Ratpack.groovy
. This file should not go in to src/main/groovy
.
See Groovy for more information about the contents of this file.
15.5.3 Generated files
Your build may generate files to be served or otherwise used at runtime. The best approach is to have the tasks that generate these files generate into a subdirectory of src/ratpack
. The Ratpack Gradle plugins add a special task named 'prepareBaseDir
’ that you should make depend on your generation task.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.10"
}
}
apply plugin: "io.ratpack.ratpack-java"
repositories {
jcenter()
}
task generateDocs(type: Copy) {
from "src/documentation"
into "src/ratpack/documentation"
expand version: project.version
}
prepareBaseDir {
dependsOn generateDocs
}
// Ensure that 'clean' removes the files generated by 'generateDocs'
clean {
delete generateDocs
}
Making 'prepareBaseDir'
depend on your generation task ensures that it is invoked whenever the application is run or assembled.
15.6 Running the application
The 'application'
plugin provides the 'run'
task for starting the Ratpack application. This is a task of the core Gradle JavaExec
type. The 'ratpack-java'
plugin configures this 'run'
task to start the process in src/ratpack
and to launch with the system property 'ratpack.development'
set to true
(which enables development time code reloading).
If you wish to set extra system properties for development time execution, you can configure this task…
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.10"
}
}
apply plugin: "io.ratpack.ratpack-java"
repositories {
jcenter()
}
run {
systemProperty "ratpack.other.dbPassword", "secret"
}
15.6.1 Running with the ‘shadow’ plugin
If applied to the project, the 'shadow'
plugin provides the 'runShadow'
task for starting the Ratpack application from the fat-jar. Like the 'run'
task, this is a task of the core Gradle JavaExec
type. The 'shadow'
plugin configure this 'runShadow'
task to start the process using the java -jar <path/to/shadow-jar.jar>
command.
Class reloading is not supported through the 'runShadow'
task because the application is being run from the packaged jar file.
Extra system properties or JVM options can be configured on this task…
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.10"
classpath "com.github.jengelman.gradle.plugins:shadow:1.0.2"
}
}
apply plugin: "io.ratpack.ratpack-java"
apply plugin: "com.github.johnrengelman.shadow"
repositories {
jcenter()
}
runShadow {
systemProperty "ratpack.other.dbPassword", "secret"
}
15.7 Class reloading via SpringLoaded
With a little extra configuration, you can enable reloading of changed classes at development time without restarting the server. This is achieved by leveraging SpringLoaded, by Pivotal. To use SpringLoaded in your Ratpack project, you need to add a dependency on the SpringLoaded agent.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.10"
}
}
apply plugin: "io.ratpack.ratpack-java"
repositories {
jcenter()
maven { url "https://repo.spring.io/repo" } // for springloaded
}
dependencies {
springloaded "org.springframework:springloaded:1.2.1.RELEASE"
}
Reloading is now enabled for your application. SpringLoaded will detect changed class files while your application is running and patch the code in memory.
An effective workflow is to open to terminal windows. In the first, execute…
./gradlew run
In the second, run the following after making a code change…
./gradlew classes
If you’d like to have Gradle automatically compile changes as they happen, you can use the Gradle Watch plugin.
Note: You do not need SpringLoaded support for reloading changes to the src/ratpack/Ratpack.groovy
file when using 'ratpack-groovy'
, nor do you need to have Gradle recompile the code. The reloading of this file is handled at runtime in reloadable mode.
15.8 IntelliJ IDEA support
The 'ratpack-java'
Gradle plugin integrates with the core 'idea'
Gradle plugin. A “Run Configuration” is automatically created, making it easy to start your application from within IDEA. The run configuration mimics the configuration of the 'run'
Gradle task, including integration with SpringLoaded.
To use the integration, you need to apply the 'idea'
plugin to your build.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.10"
}
}
apply plugin: "io.ratpack.ratpack-java"
apply plugin: "idea"
repositories {
jcenter()
}
You can now have the build generate metadata that allows the project to be opened in IDEA, by running…
./gradlew idea
This will generate a ?project name?.ipr
file, which can be opened with IDEA. Once the project is opened, you will see a “Run Configuration” named “Ratpack Run” that can be used to start the application.
15.8.1 Reloading
If you have configured your build to use SpringLoaded, it will also be used by IDEA. However, IDEA will not automatically recompile code while there is an active run configuration. This means that after making a code change (to anything other than src/ratpack/Ratpack.groovy
) you need to click “Make Project” in the “Build”?menu (or use the corresponding key shortcut).
16 Deploying to Heroku
Heroku is a scalable polyglot cloud application platform. It allows you to focus on writing applications in the language of your choice, and then easily deploy them to the cloud without having to manually manage servers, load balancing, log aggregation, etc. Heroku does not have, nor does it need, any special Ratpack support above its generic support for JVM applications. Heroku is a rich platform, with many add-ons such as Postgres, Redis, Memcache, RabbitMQ, New Relic, etc. It is a compelling option for serving Ratpack applications.
Deployments to Heroku are typically in source form. Deploying is as simple as performing a Git push at the end of your CI pipeline. Many popular cloud CI tools such as drone.io and Travis-CI (among others) have convenient support for pushing to Heroku.
It is recommended to read the Heroku Quickstart and Buildpack documentation if you are new to Heroku. The rest of this chapter outlines the requirements and necessary configuration for deploying Ratpack applications to Heroku.
16.1 Gradle based builds
Ratpack applications can be built by any build system, but the Ratpack team recommends Gradle. Heroku has native support for Gradle via the Gradle buildpack, which works well with the Ratpack Gradle plugins.
All Gradle projects should use the Gradle Wrapper. If the wrapper scripts are present in your project, Heroku will detect that your project is built with Gradle.
16.1.1 Java Version
Heroku allows the required Java version to be set via system.properties
file in the root of the source tree.
java.runtime.version=1.7
At the time of writing, this file is required as Heroku defaults to Java 6 and Ratpack requires Java 7.
16.1.2 Building
The Gradle buildpack will invoke ./gradlew stage
. The Ratpack Gradle plugins do not add a stage
task to your build, so you need to add it yourself and make it build your application. The simplest way to do this is make the stage
task depend on the installApp
task which is added by the Ratpack Gradle plugins.
A minimalistic build.gradle
looks like this:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.10"
}
}
apply plugin: "io.ratpack.ratpack-java"
task stage {
dependsOn installApp
}
This will build your application and install it into the directory build/install/?project name?
.
16.1.2.1 Setting the project name
By default, Gradle uses the project’s directory name as the project’s name. In Heroku (and some CI servers), the project is built in a temporary directory with a randomly assigned name. To ensure that the project uses a consistent name, add a declaration to settings.gradle
in the root of your project:
rootProject.name = "?project name?"
This is a good practice for any Gradle project.
16.1.3 Running (Procfile)
The Procfile
lives at the root of your application and specifies the command that Heroku should use to start your application. In this file, add a declaration that Heroku should start a web
process by executing the launch script created by the build.
web: build/install/?project name?/bin/?project name?
16.1.4 Configuration
There are several ways to configure the environment for applications deployed to Heroku. You may want to use these mechanisms to set environment variables and/or JVM system properties to configure your application.
The application entry points that are used when using the ratpack
and ratpack-groovy
Gradle plugins support using JVM system properties to contribute to the LaunchConfig
(see the launching chapter chapter for more detail). The starter scripts created by the Ratpack Gradle plugins, support the standard JAVA_OPTS
environment variable and an app specific ?PROJECT_NAME?_OPTS
environment variable. If your application name was foo-Bar
, then the environment variable would be named FOO_BAR_OPTS
.
One way to bring this all together is to launch your application via env
:
web: env "FOO_BAR_OPTS=-Dratpack.other.dbPassword=secret" build/install/?project name?/bin/?project name?
It is generally preferable to not use JAVA_OPTS
as Heroku sets this to useful defaults for the platform.
Another approach is to use config vars. The benefit of setting the environment via the Procfile is that this information is in your versioned source tree. The benefit of using config vars is that they are only available to those with permissions to view/change them with Heroku. It is possible to combine both approaches by setting config vars for values that should be secret (like passwords) and referencing them in your Procfile.
web: env "FOO_BAR_OPTS=-Dratpack.other.dbPassword=$SECRET_DB_PASSWORD" build/install/?project name?/bin/?project name?
Now it is easy to see which properties and environment variables are set in the source tree, but sensitive values are only visible via the Heroku management tools.
16.2 Other build tools and binary deployments
The Ratpack project does not provide any “official”?integration with other build tools. However, it is quite possible to use whatever tool you like to build a Ratpack application for Heroku or even to deploy in binary form.
Once you have a compiled Ratpack application in the Heroku environment (either through building with another build tool or by binary deployment), you can simply start the application by using java
directly.
web: java ratpack.groovy.launch.GroovyRatpackMain
See the launching chapter chapter for more detail on starting Ratpack applications.
16.3 General Considerations
16.3.1 Port and public address
You may want to consider setting -Dratpack.publicAddress
to the public name of your application so that application redirects work as expected. See redirect()
for more details.
Heroku assigns each application an ephemeral port number, made available by the PORT
environment variable. The RatpackMain
entry point implicitly supports this environment variable if the ratpack.port
system property is not set.
17 Logging
Ratpack uses SLF4J for logging, which allows you to easily bind your favorite logging library at compile time.
Library options include:
- No-Op - discards all logs (default)
- Log4J
- Logback - native SLF4J implementation with “zero memory and computational overhead”
- Java Util Logging
- Simple - logs messages at INFO level and higher to System.err
- Jakarta Commons Logging
Simply add one logging library as a dependency and use slf4j syntax to log. If you are currently using another logging library, SLF4J provides a migration tool to automate the transition.
Examples for Java and Groovy are below and more details can be found in the SLF4J manual.
17.1 Java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogExample {
private final static Logger LOGGER = LoggerFactory.getLogger(LogExample.class);
public void log() {
LOGGER.info("Start logging");
LOGGER.warn("Logging with a {} or {}", "parameter", "two");
LOGGER.error("Log an exception", new Exception("Example exception"));
LOGGER.info("Stop logging");
}
}
17.2 Groovy
import groovy.util.logging.Slf4j
@Slf4j
class LogExample {
void log() {
log.info "Start logging"
log.warn "Logging with a {} or {}", "parameter", "two"
log.debug "Detailed information"
log.info "Stop logging"
}
}
18 The Ratpack project
18.1 Credits
Ratpack has been made possible by the following people.
18.1.1 Active project members
The following people are currently actively contributing their time to Ratpack.
18.1.2 Contributors
The following people have provided significant contributions.
- Tim Berglund
- Peter Ledbrook
- Marco Vermeulen
- Paul Fairless
- Tom?s Lin
- Kevin Marquardsen
- Stefano Gualdi
- Ben Navetta
- Bobby Warner
- Jim White
- Dimitris Zavaliadis
- Craig Burke
- Andrey Adamovich
- Tim Yates
- Brett Wooldridge
- Roy Schumacher
- Chip McCormick
- ?lvaro S?nchez-Mariscal
- Cihat Keser
- C?dric Champeau
- Kevin Greene
- David Tiselius
- John Engelman
- Andre Steingress
- Gareth Davis
- Lari Hotari
- Massimo Lusetti
- Rob Zienert
- Tom Duncan
18.1.3 Past project members
The following people have contributed their time to Ratpack in the past, but are no longer actively working on it.
18.2 About this manual
18.2.1 Resources
18.2.1.1 Libraries
- Prism by Lea Verou
- Normalize.css by Nicolas Gallagher
- Modernizr
- Breakpoint by Mason Wendell and Sam Richard
18.2.1.2 Fonts
Web fonts are served by Google Web Fonts.
- Merriweather and Merriweather Sans by Eben Sorkin
- Engagement by Astigmatic One Eye Typographic Institute
18.2.1.3 Images
- Hat designed by Adhara Garcia from The Noun Project
- Microphone designed by Cris Dobbins from The Noun Project
- Whisky designed by Anthony Lui from The Noun Project
- Retro starburst 3 from Velma’s Retro Clip Art and Images (last known URL: http://free-retro-graphics.com/2011/02/retro-starburst-3/)
- Jagged Edge social media icons from Geek Fairy