Merge pull request #18023 from spray/w/java-side-documentation

Batch of Java-side documentation fills
This commit is contained in:
Konrad Malawski 2015-07-17 17:49:06 +02:00
commit b348691f58
18 changed files with 387 additions and 22 deletions

View file

@ -13,7 +13,7 @@ import akka.http.javadsl.model.*;
import akka.http.javadsl.model.headers.*;
//#import-model
public class ModelSpec {
public class ModelTest {
@Test
public void testConstructRequest() {
//#construct-request

View file

@ -2,7 +2,7 @@
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package docs.http.javadsl;
package docs.http.javadsl.server;
//#high-level-server-example
import akka.actor.ActorSystem;

View file

@ -2,7 +2,7 @@
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
package docs.http.javadsl;
package docs.http.javadsl.server;
import akka.actor.ActorSystem;
import akka.http.impl.util.Util;

View file

@ -2,7 +2,7 @@
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
package docs.http.javadsl;
package docs.http.javadsl.server;
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.StatusCodes;

View file

@ -0,0 +1,33 @@
/*
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
package docs.http.javadsl.server.testkit;
//#simple-app
import akka.http.javadsl.server.*;
import akka.http.javadsl.server.values.Parameters;
public class MyAppService extends HttpApp {
RequestVal<Double> x = Parameters.doubleValue("x");
RequestVal<Double> y = Parameters.doubleValue("y");
public RouteResult add(RequestContext ctx, double x, double y) {
return ctx.complete("x + y = " + (x + y));
}
@Override
public Route createRoute() {
return
route(
get(
pathPrefix("calculator").route(
path("add").route(
handleReflectively(this, "add", x, y)
)
)
)
);
}
}
//#simple-app

View file

@ -0,0 +1,36 @@
/*
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
package docs.http.javadsl.server.testkit;
//#simple-app-testing
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.StatusCodes;
import akka.http.javadsl.testkit.JUnitRouteTest;
import akka.http.javadsl.testkit.TestRoute;
import org.junit.Test;
public class TestkitExampleTest extends JUnitRouteTest {
TestRoute appRoute = testRoute(new MyAppService().createRoute());
@Test
public void testCalculatorAdd() {
// test happy path
appRoute.run(HttpRequest.GET("/calculator/add?x=4.2&y=2.3"))
.assertStatusCode(200)
.assertEntity("x + y = 6.5");
// test responses to potential errors
appRoute.run(HttpRequest.GET("/calculator/add?x=3.2"))
.assertStatusCode(StatusCodes.NOT_FOUND) // 404
.assertEntity("Request is missing required query parameter 'y'");
// test responses to potential errors
appRoute.run(HttpRequest.GET("/calculator/add?x=3.2&y=three"))
.assertStatusCode(StatusCodes.BAD_REQUEST)
.assertEntity("The query parameter 'y' was malformed:\n" +
"'three' is not a valid 64-bit floating point value");
}
}
//#simple-app-testing

View file

@ -13,7 +13,7 @@ Overview
Since akka-http-core provides the central HTTP data structures you will find the following import in quite a
few places around the code base (and probably your own code as well):
.. includecode:: ../code/docs/http/javadsl/ModelSpec.java
.. includecode:: ../code/docs/http/javadsl/ModelTest.java
:include: import-model
This brings all of the most relevant types in scope, mainly:
@ -50,7 +50,7 @@ An ``HttpRequest`` consists of
Here are some examples how to construct an ``HttpRequest``:
.. includecode:: ../code/docs/http/javadsl/ModelSpec.java
.. includecode:: ../code/docs/http/javadsl/ModelTest.java
:include: construct-request
In its basic form ``HttpRequest.create`` creates an empty default GET request without headers which can then be
@ -71,7 +71,7 @@ An ``HttpResponse`` consists of
Here are some examples how to construct an ``HttpResponse``:
.. includecode:: ../code/docs/http/javadsl/ModelSpec.java
.. includecode:: ../code/docs/http/javadsl/ModelTest.java
:include: construct-response
In addition to the simple ``HttpEntities.create`` methods which create an entity from a fixed ``String`` or ``ByteString``
@ -169,7 +169,7 @@ as a ``RawHeader`` (which is essentially a String/String name/value pair).
See these examples of how to deal with headers:
.. includecode:: ../code/docs/http/javadsl/ModelSpec.java
.. includecode:: ../code/docs/http/javadsl/ModelTest.java
:include: headers

View file

@ -91,5 +91,5 @@ Predefined path matchers allow extraction of various types of values:
Here's a collection of path matching examples:
.. includecode:: ../../../code/docs/http/javadsl/PathDirectiveExampleTest.java
.. includecode:: ../../../code/docs/http/javadsl/server/PathDirectiveExampleTest.java
:include: path-examples

View file

@ -102,4 +102,42 @@ static methods. The referenced method must be publicly accessible.
Deferring Result Creation
-------------------------
TODO
Sometimes a handler cannot directly complete the request but needs to do some processing asynchronously. In this case
the completion of a request needs to be deferred until the result has been generated. This is supported by the routing
DSL in two ways: either you can use one of the ``handleWithAsyncN`` methods passing an ``AsyncHandlerN`` which
returns a ``Future<RouteResult>``, i.e. an eventual ``RouteResult``, or you can also use a regular handler as shown
above and use ``RequestContext.completeWith`` for completion which takes an ``Future<RouteResult>`` as an argument.
This is demonstrated in the following example. Consider a asynchronous service defined like this
(making use of Java 8 lambdas):
.. includecode:: /../../akka-http-tests-java8/src/test/java/docs/http/javadsl/server/HandlerExampleDocTest.java
:include: async-service-definition
Here the calculator runs the actual calculation in the background and only eventually returns the result. The HTTP
service should provide a front-end to that service without having to block while waiting for the results. As explained
above this can be done in two ways.
First, you can use ``handleWithAsyncN`` to be able to return a ``Future<RouteResult>``:
.. includecode:: /../../akka-http-tests-java8/src/test/java/docs/http/javadsl/server/HandlerExampleDocTest.java
:include: async-handler-1
The handler invokes the service and then maps the calculation result to a ``RouteResult`` using ``Future.map`` and
returns the resulting ``Future<RouteResult>``.
Otherwise, you can also still use ``handleWithN`` and use ``RequestContext.completeWith`` to "convert" a
``Future<RouteResult>`` into a ``RouteResult`` as shown here:
.. includecode:: /../../akka-http-tests-java8/src/test/java/docs/http/javadsl/server/HandlerExampleDocTest.java
:include: async-handler-2
Using this style, you can decide in your handler if you want to return a direct synchronous result or if you need
to defer completion.
Both alternatives will not block and show the same runtime behavior.
Here's the complete example:
.. includecode:: /../../akka-http-tests-java8/src/test/java/docs/http/javadsl/server/HandlerExampleDocTest.java
:include: async-example-full

View file

@ -14,4 +14,5 @@ To use the high-level API you need to add a dependency to the ``akka-http-experi
request-vals/index
handlers
marshalling
testkit
json-jackson-support

View file

@ -7,7 +7,7 @@ The Akka HTTP :ref:`http-low-level-server-side-api-java` provides a ``Flow``- or
an application to respond to incoming HTTP requests by simply mapping requests to responses
(excerpt from :ref:`Low-level server side example <http-low-level-server-side-example-java>`):
.. includecode:: ../../code/docs/http/javadsl/HttpServerExampleDocTest.java
.. includecode:: ../../code/docs/http/javadsl/server/HttpServerExampleDocTest.java
:include: request-handler
While it'd be perfectly possible to define a complete REST API service purely by inspecting the incoming
@ -21,7 +21,7 @@ async handler function) that can be directly supplied to a ``bind`` call.
Here's the complete example rewritten using the composable high-level API:
.. includecode:: ../../code/docs/http/javadsl/HighLevelServerExample.java
.. includecode:: ../../code/docs/http/javadsl/server/HighLevelServerExample.java
:include: high-level-server-example
Heart of the high-level architecture is the route tree. It is a big expression of type ``Route``
@ -68,4 +68,9 @@ to HTTP entities.
Read more about :ref:`marshalling-java`.
akka-http contains a testkit that simplifies testing routes. It allows to run test-requests against (sub-)routes
quickly without running them over the network and helps with writing assertions on HTTP response properties.
Read more about :ref:`http-testkit-java`.
.. _DRY: http://en.wikipedia.org/wiki/Don%27t_repeat_yourself

View file

@ -0,0 +1,80 @@
.. _http-testkit-java:
Route Testkit
=============
akka-http has a testkit that provides a convenient way of testing your routes with JUnit. It allows
running requests against a route (without hitting the network) and provides means to assert against
response properties in a compact way.
To use the testkit you need to take these steps:
* add a dependency to the ``akka-http-testkit-experimental`` module
* derive the test class from ``JUnitRouteTest``
* wrap the route under test with ``RouteTest.testRoute`` to create a ``TestRoute``
* run requests against the route using ``TestRoute.run(request)`` which will return
a ``TestResponse``
* use the methods of ``TestResponse`` to assert on properties of the response
Example
-------
To see the testkit in action consider the following simple calculator app service:
.. includecode:: ../../code/docs/http/javadsl/server/testkit/MyAppService.java
:include: simple-app
The app extends from ``HttpApp`` which brings all of the directives into scope. Method ``createRoute``
needs to be implemented to return the complete route of the app.
Here's how you would test that service:
.. includecode:: ../../code/docs/http/javadsl/server/testkit/TestkitExampleTest.java
:include: simple-app-testing
Writing Asserting against the HttpResponse
------------------------------------------
The testkit supports a fluent DSL to write compact assertions on the response by chaining assertions
using "dot-syntax". To simplify working with streamed responses the entity of the response is first "strictified", i.e.
entity data is collected into a single ``ByteString`` and provided the entity is supplied as an ``HttpEntityStrict``. This
allows to write several assertions against the same entity data which wouldn't (necessarily) be possible for the
streamed version.
All of the defined assertions provide HTTP specific error messages aiding in diagnosing problems.
Currently, these methods are defined on ``TestResponse`` to assert on the response:
=================================================================== =======================================================================
Assertion Description
=================================================================== =======================================================================
``assertStatusCode(int expectedCode)`` Asserts that the numeric response status code equals the expected one
``assertStatusCode(StatusCode expectedCode)`` Asserts that the response ``StatusCode`` equals the expected one
``assertMediaType(String expectedType)`` Asserts that the media type part of the response's content type matches
the given String
``assertMediaType(MediaType expectedType)`` Asserts that the media type part of the response's content type matches
the given ``MediaType``
``assertEntity(String expectedStringContent)`` Asserts that the entity data interpreted as UTF8 equals the expected
String
``assertEntityBytes(ByteString expectedBytes)`` Asserts that the entity data bytes equal the expected ones
``assertEntityAs(Unmarshaller<T> unmarshaller, expectedValue: T)`` Asserts that the entity data if unmarshalled with the given marshaller
equals the given value
``assertHeaderExists(HttpHeader expectedHeader)`` Asserts that the response contains an HttpHeader instance equal to the
expected one
``assertHeaderKindExists(String expectedHeaderName)`` Asserts that the response contains a header with the expected name
``assertHeader(String name, String expectedValue)`` Asserts that the response contains a header with the given name and
value.
=================================================================== =======================================================================
It's, of course, possible to use any other means of writing assertions by inspecting the properties the response
manually. As written above, ``TestResponse.entity`` and ``TestResponse.response`` return strict versions of the
entity data.
Supporting Custom Test Frameworks
---------------------------------
Adding support for a custom test framework is achieved by creating new superclass analogous to
``JUnitRouteTest`` for writing tests with the custom test framwork deriving from ``akka.http.javadsl.testkit.RouteTest``
and implementing its abstract methods. This will allow users of the test framework to use ``testRoute`` and
to write assertions using the assertion methods defined on ``TestResponse``.

View file

@ -62,7 +62,7 @@ Starting and Stopping
On the most basic level an Akka HTTP server is bound by invoking the ``bind`` method of the `akka.http.javadsl.Http`_
extension:
.. includecode:: ../../code/docs/http/javadsl/HttpServerExampleDocTest.java
.. includecode:: ../../code/docs/http/javadsl/server/HttpServerExampleDocTest.java
:include: binding-example
Arguments to the ``Http().bind`` method specify the interface and port to bind to and register interest in handling
@ -99,7 +99,7 @@ Requests are handled by calling one of the ``handleWithXXX`` methods with a hand
Here is a complete example:
.. includecode:: ../../code/docs/http/javadsl/HttpServerExampleDocTest.java
.. includecode:: ../../code/docs/http/javadsl/server/HttpServerExampleDocTest.java
:include: full-server-example
In this example, a request is handled by transforming the request stream with a function ``Function<HttpRequest, HttpResponse>``
@ -149,8 +149,12 @@ optional ``httpsContext`` parameter, which can receive the HTTPS configuration i
instance.
If defined encryption is enabled on all accepted connections. Otherwise it is disabled (which is the default).
.. _http-server-layer-java:
Stand-Alone HTTP Layer Usage
----------------------------
// TODO
It is currently only possible to use the HTTP server layer with Scala in a stand-alone fashion.
See :ref:`http-server-layer-scala` and `#18027`_ for the plan to add Java support.
.. _`#18027`: https://github.com/akka/akka/issues/18027

View file

@ -150,6 +150,7 @@ optional ``httpsContext`` parameter, which can receive the HTTPS configuration i
instance.
If defined encryption is enabled on all accepted connections. Otherwise it is disabled (which is the default).
.. _http-server-layer-scala:
Stand-Alone HTTP Layer Usage
----------------------------

View file

@ -10,7 +10,7 @@ import scala.concurrent.duration._
import akka.stream.ActorMaterializer
import akka.http.scaladsl.server
import akka.http.javadsl.model.HttpRequest
import akka.http.javadsl.server.{ AllDirectives, Route, Directives }
import akka.http.javadsl.server.{ HttpApp, AllDirectives, Route, Directives }
import akka.http.impl.util.JavaMapping.Implicits._
import akka.http.impl.server.RouteImplementation
import akka.http.scaladsl.model.HttpResponse
@ -39,6 +39,9 @@ abstract class RouteTest extends AllDirectives {
}
}
/**
* Wraps a list of route alternatives with testing support.
*/
@varargs
def testRoute(first: Route, others: Route*): TestRoute =
new TestRoute {
@ -47,5 +50,10 @@ abstract class RouteTest extends AllDirectives {
def run(request: HttpRequest): TestResponse = runRoute(underlying, request)
}
/**
* Creates a [[TestRoute]] for the main route of an [[HttpApp]].
*/
def testAppRoute(app: HttpApp): TestRoute = testRoute(app.createRoute)
protected def createTestResponse(response: HttpResponse): TestResponse
}

View file

@ -21,49 +21,124 @@ import akka.http.javadsl.model._
* A wrapper for responses
*/
abstract class TestResponse(_response: HttpResponse, awaitAtMost: FiniteDuration)(implicit ec: ExecutionContext, materializer: ActorMaterializer) {
lazy val entity: HttpEntityStrict =
_response.entity.toStrict(awaitAtMost).awaitResult(awaitAtMost)
/**
* Returns the strictified entity of the response. It will be strictified on first access.
*/
lazy val entity: HttpEntityStrict = _response.entity.toStrict(awaitAtMost).awaitResult(awaitAtMost)
/**
* Returns a copy of the underlying response with the strictified entity.
*/
lazy val response: HttpResponse = _response.withEntity(entity)
// FIXME: add header getters / assertions
/**
* Returns the media-type of the the response's content-type
*/
def mediaType: MediaType = extractFromResponse(_.entity.contentType.mediaType)
/**
* Returns a string representation of the media-type of the the response's content-type
*/
def mediaTypeString: String = mediaType.toString
/**
* Returns the bytes of the response entity
*/
def entityBytes: ByteString = entity.data()
/**
* Returns the entity of the response unmarshalled with the given ``Unmarshaller``.
*/
def entityAs[T](unmarshaller: Unmarshaller[T]): T =
Unmarshal(response)
.to(unmarshaller.asInstanceOf[UnmarshallerImpl[T]].scalaUnmarshaller(ec, materializer), ec)
.awaitResult(awaitAtMost)
/**
* Returns the entity of the response interpreted as an UTF-8 encoded string.
*/
def entityAsString: String = entity.data().utf8String
/**
* Returns the [[StatusCode]] of the response.
*/
def status: StatusCode = response.status.asJava
/**
* Returns the numeric status code of the response.
* @return
*/
def statusCode: Int = response.status.intValue
/**
* Returns the first header of the response which is of the given class.
*/
def header[T <: HttpHeader](clazz: Class[T]): T =
response.header(ClassTag(clazz))
.getOrElse(fail(s"Expected header of type ${clazz.getSimpleName} but wasn't found."))
/**
* Assert on the numeric status code.
*/
def assertStatusCode(expected: Int): TestResponse =
assertStatusCode(StatusCodes.get(expected))
/**
* Assert on the status code.
*/
def assertStatusCode(expected: StatusCode): TestResponse =
assertEqualsKind(expected, status, "status code")
/**
* Assert on the media type of the response.
*/
def assertMediaType(expected: String): TestResponse =
assertEqualsKind(expected, mediaTypeString, "media type")
/**
* Assert on the media type of the response.
*/
def assertMediaType(expected: MediaType): TestResponse =
assertEqualsKind(expected, mediaType, "media type")
/**
* Assert on the response entity to be a UTF8 representation of the given string.
*/
def assertEntity(expected: String): TestResponse =
assertEqualsKind(expected, entityAsString, "entity")
/**
* Assert on the response entity to equal the given bytes.
*/
def assertEntityBytes(expected: ByteString): TestResponse =
assertEqualsKind(expected, entityBytes, "entity")
/**
* Assert on the response entity to equal the given object after applying an [[Unmarshaller]].
*/
def assertEntityAs[T <: AnyRef](unmarshaller: Unmarshaller[T], expected: T): TestResponse =
assertEqualsKind(expected, entityAs(unmarshaller), "entity")
/**
* Assert that a given header instance exists in the response.
*/
def assertHeaderExists(expected: HttpHeader): TestResponse = {
assertTrue(response.headers.exists(_ == expected), s"Header $expected was missing.")
this
}
/**
* Assert that a header of the given type exists.
*/
def assertHeaderKindExists(name: String): TestResponse = {
val lowercased = name.toRootLowerCase
assertTrue(response.headers.exists(_.is(lowercased)), s"Expected `$name` header was missing.")
this
}
/**
* Assert that a header of the given name and value exists.
*/
def assertHeaderExists(name: String, value: String): TestResponse = {
val lowercased = name.toRootLowerCase
val headers = response.headers.filter(_.is(lowercased))

View file

@ -4,14 +4,19 @@
package docs.http.javadsl.server;
import akka.dispatch.Futures;
import akka.dispatch.Mapper;
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.server.*;
import akka.http.javadsl.server.values.Parameters;
import akka.http.javadsl.server.values.PathMatchers;
import akka.http.javadsl.testkit.JUnitRouteTest;
import akka.http.javadsl.testkit.TestRoute;
import akka.japi.function.Function;
import org.junit.Test;
import scala.concurrent.ExecutionContext;
import scala.concurrent.Future;
import java.util.concurrent.Callable;
public class HandlerExampleDocTest extends JUnitRouteTest {
@Test
@ -212,4 +217,83 @@ public class HandlerExampleDocTest extends JUnitRouteTest {
.assertEntity("x * y = 115");
//#reflective-example-full
}
@Test
public void testDeferredResultAsyncHandler() {
//#async-example-full
//#async-service-definition
class CalculatorService {
public Future<Integer> multiply(final int x, final int y, ExecutionContext ec) {
return akka.dispatch.Futures.future(() -> x * y, ec);
}
public Future<Integer> add(final int x, final int y, ExecutionContext ec) {
return akka.dispatch.Futures.future(() -> x + y, ec);
}
}
//#async-service-definition
class TestHandler extends akka.http.javadsl.server.AllDirectives {
RequestVal<Integer> xParam = Parameters.intValue("x");
RequestVal<Integer> yParam = Parameters.intValue("y");
//#async-handler-1
// would probably be injected or passed at construction time in real code
CalculatorService calculatorService = new CalculatorService();
public Future<RouteResult> multiplyAsync(final RequestContext ctx, int x, int y) {
Future<Integer> result = calculatorService.multiply(x, y, ctx.executionContext());
Mapper<Integer, RouteResult> func = new Mapper<Integer, RouteResult>() {
@Override
public RouteResult apply(Integer product) {
return ctx.complete("x * y = " + product);
}
}; // cannot be written as lambda, unfortunately
return result.map(func, ctx.executionContext());
}
Route multiplyAsyncRoute =
path("multiply").route(
handleWithAsync2(xParam, yParam, this::multiplyAsync)
);
//#async-handler-1
//#async-handler-2
public RouteResult addAsync(final RequestContext ctx, int x, int y) {
Future<Integer> result = calculatorService.add(x, y, ctx.executionContext());
Mapper<Integer, RouteResult> func = new Mapper<Integer, RouteResult>() {
@Override
public RouteResult apply(Integer sum) {
return ctx.complete("x + y = " + sum);
}
}; // cannot be written as lambda, unfortunately
return ctx.completeWith(result.map(func, ctx.executionContext()));
}
Route addAsyncRoute =
path("add").route(
handleWith2(xParam, yParam, this::addAsync)
);
//#async-handler-2
Route createRoute() {
return route(
get(
pathPrefix("calculator").route(
multiplyAsyncRoute,
addAsyncRoute
)
)
);
}
}
// testing code
TestRoute r = testRoute(new TestHandler().createRoute());
r.run(HttpRequest.GET("/calculator/multiply?x=12&y=42"))
.assertStatusCode(200)
.assertEntity("x * y = 504");
r.run(HttpRequest.GET("/calculator/add?x=23&y=5"))
.assertStatusCode(200)
.assertEntity("x + y = 28");
//#async-example-full
}
}

View file

@ -16,7 +16,7 @@ import akka.http.scaladsl.Http.ServerBinding
abstract class HttpApp
extends AllDirectives
with HttpServiceBase {
protected def createRoute(): Route
def createRoute(): Route
/**
* Starts an HTTP server on the given interface and port. Creates the route by calling the