Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add typed body on request and response #124

Merged
merged 2 commits into from Jun 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
197 changes: 162 additions & 35 deletions README.md
Expand Up @@ -16,6 +16,13 @@ libraryDependencies += "com.typesafe.play" %% "play-ahc-ws-standalone" % "1.0.0-

This adds the standalone version of Play WS, backed by [AsyncHttpClient](https://github.com/AsyncHttpClient/async-http-client). This library contains both the Scala and Java APIs, under `play.api.libs.ws` and `play.libs.ws`.

To add XML and JSON support using Play-JSON or Scala XML, add the following:

```scala
libraryDependencies += "com.typesafe.play" %% "play-ws-standalone-xml" % playWsStandaloneVersion
libraryDependencies += "com.typesafe.play" %% "play-ws-standalone-json" % playWsStandaloneVersion
```

## Shading

Play WS uses shaded versions of AsyncHttpClient and OAuth Signpost, repackaged under the `play.shaded.ahc` and `play.shaded.oauth` package names, respectively. Shading AsyncHttpClient means that the version of Netty used behind AsyncHttpClient is completely independent of the application and Play as a whole.
Expand All @@ -32,6 +39,157 @@ Because Play WS shades AsyncHttpClient, the default settings are also shaded and
play.shaded.ahc.org.asynchttpclient.usePooledMemory=true
```

### Typed Bodies

The type system in Play-WS has changed so that the request body and the response body can use richer types.

You can define your own BodyWritable or BodyReadable, but if you want to use the default out of the box settings, you can import the type mappings with the DefaultBodyReadables / DefaultBodyWritables.

#### Scala

```scala
import play.api.libs.ws.DefaultBodyReadables._
import play.api.libs.ws.DefaultBodyWritables._
```

More likely you will want the XML and JSON support:

```scala
import play.api.libs.ws.XMLBodyReadables._
import play.api.libs.ws.XMLBodyWritables._
```

or

```scala
import play.api.libs.ws.JsonBodyReadables._
import play.api.libs.ws.JsonBodyWritables._
```

To use a BodyReadable in a response, you must type the response explicitly:

```scala
val responseBody: Future[scala.xml.Elem] = ws.url(...).get().map { response =>
response.body[scala.xml.Elem]
}
```

or using Play-JSON:

```scala
val jsonBody: Future[JsValue] = ws.url(...).get().map { response =>
response.body[JsValue]
}
```

Note that there is a special case: when you are streaming the response, then you should get the body as a Source:

```scala
ws.url(...).stream().map { response =>
val source: Source[ByteString, NotUsed] = response.bodyAsSource
}
```

To POST, you should pass in a type which has an implicit class mapping of BodyWritable:

```scala
val stringData = "Hello world"
ws.url(...).post(stringData).map { response => ... }
```

You can also define your own custom BodyReadable:

```scala
case class Foo(body: String)

implicit val fooBodyReadable = BodyReadable[Foo] { response =>
import play.shaded.ahc.org.asynchttpclient.{ Response => AHCResponse }
val ahcResponse = response.asInstanceOf[StandaloneAhcWSResponse].underlying[AHCResponse]
Foo(ahcResponse.getResponseBody)
}
```

or custom BodyWritable:

```scala
implicit val writeableOf_Foo: BodyWritable[Foo] = {
// https://tools.ietf.org/html/rfc6838#section-3.2
BodyWritable(foo => InMemoryBody(ByteString.fromString(foo.serialize)), application/vnd.company.category+foo)
}
```

#### Java

To use the default type mappings in Java, you should use the following:

```java
import play.libs.ws.DefaultBodyReadables;
import play.libs.ws.DefaultBodyWritables;
```

followed by:

```java
public class MyClient implements DefaultBodyWritables, DefaultBodyReadables {
public CompletionStage<String> doStuff() {
return client.url("http://example.com").post(body("hello world")).thenApply(response ->
response.body(string())
);
}
}
```

Note that there is a special case: when you are using a stream, then you should get the body as a Source:

```java

class MyClass {
public CompletionStage<Source<ByteString, NotUsed>> readResponseAsStream() {
return ws.url(url).stream().thenApply(response ->
response.bodyAsSource()
);
}
}
```

You can also post a Source:

```java
class MyClass {
public CompletionStage<String> doStuff() {
Source<ByteString, NotUsed> source = fromSource();
return ws.url(url).post(source).thenApply(response ->
response.body()
);
}
}
```

You can define a custom `BodyReadable`:

```java
import play.libs.ws.ahc.*;
import play.shaded.ahc.org.asynchttpclient.Response;

class FooReadable implements BodyReadable<StandaloneWSResponse, Foo> {
public Foo apply(StandaloneWSResponse response) {
Response ahcResponse = (Response) response.getUnderlying();
return Foo.serialize(ahcResponse.getResponseBody(StandardCharsets.UTF_8));
}
}
```

You can also define your own custom `BodyWritable`:

```java
public class MyClient {
private BodyWritable<String> someOtherMethod(String string) {
akka.util.ByteString byteString = akka.util.ByteString.fromString(string);
return new DefaultBodyWritables.InMemoryBodyWritable(byteString, "text/plain");
}
}
```

## Instantiating a standalone client

The standalone client needs [Akka](http://akka.io/) to handle streaming data internally:
Expand All @@ -51,6 +209,7 @@ import play.api.libs.ws.ahc._
import scala.concurrent.Future

object ScalaClient {
import DefaultBodyReadables._
import scala.concurrent.ExecutionContext.Implicits._

def main(args: Array[String]): Unit = {
Expand All @@ -74,6 +233,7 @@ object ScalaClient {
def call(wsClient: StandaloneWSClient): Future[Unit] = {
wsClient.url("http://www.google.com").get().map { response ⇒
val statusText: String = response.statusText
val body = response.body[String]
println(s"Got a response $statusText")
}
}
Expand All @@ -97,7 +257,7 @@ import play.libs.ws.ahc.*;

import java.util.concurrent.CompletionStage;

public class JavaClient {
public class JavaClient implements DefaultBodyReadables {

public static void main(String[] args) {
// Set up Akka materializer to handle streaming
Expand All @@ -116,6 +276,7 @@ public class JavaClient {
client.url("http://www.google.com").get()
.whenComplete((response, throwable) -> {
String statusText = response.getStatusText();
String body = response.body(string());
System.out.println("Got a response " + statusText);
})
.thenRun(() -> {
Expand Down Expand Up @@ -159,40 +320,6 @@ There are a number of guides that help with putting together Cache-Control heade
* [HTTP Caching](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching)
* [REST Easy: HTTP Cache](http://odino.org/rest-better-http-cache/)

## API changes

The standalone client API has changed from the Play-WS 2.5.x version in somme ways,

### Request Body changes

Setting a request body was not well defined, and had repeated type information. Setting a body on a request (usually for POST, PUT and PATCH) now uses the following:

#### Scala

The body methods now take an implicit `BodyWritable` which returns a `WSBody`. The `DefaultBodyWritables` trait contains the expected body types.

```scala
val file = new File("foo") // A BodyWritable exists for File so the type class will be used.
request.setBody(file)
```

#### Java

There is a type `WSBody<T>` which contains the body information. You set the request by calling out to a concrete provider of WSBody:

```java
StandaloneAhcWSClient client = ...
WSBody<String> body = client.body("Hello world");
request.setBody(body);
```

or by using `AhcWSBody` directly, which is useful for unit testing when you have a mock client:

```java
WSBody<String> body = AhcWSBody.string("Hello world");
request.setBody(body);
```

## Releasing

This project uses `sbt-release` to push to Sonatype and Maven. You will need Lightbend Sonatype credentials and a GPG key that is available on one of the public keyservers to release this project.
Expand Down
64 changes: 55 additions & 9 deletions build.sbt
Expand Up @@ -136,8 +136,8 @@ val ahcMerge: MergeStrategy = new MergeStrategy {
override val name: String = "ahcMerge"
}

import xml.{NodeSeq, Node => XNode, Elem}
import xml.transform.{RuleTransformer, RewriteRule}
import scala.xml.transform.{RewriteRule, RuleTransformer}
import scala.xml.{Elem, NodeSeq, Node => XNode}

def dependenciesFilter(n: XNode) = new RuleTransformer(new RewriteRule {
override def transform(n: XNode): NodeSeq = n match {
Expand Down Expand Up @@ -245,7 +245,6 @@ lazy val shaded = Project(id = "shaded", base = file("shaded") )
lazy val `play-ws-standalone` = project
.in(file("play-ws-standalone"))
.settings(commonSettings)
.settings(libraryDependencies ++= (specsBuild ++ junitInterface).map(_ % Test))
.settings(libraryDependencies ++= standaloneApiWSDependencies)
.disablePlugins(sbtassembly.AssemblyPlugin)

Expand Down Expand Up @@ -275,8 +274,7 @@ lazy val `play-ahc-ws-standalone` = project
.settings(SbtScalariform.scalariformSettings)
.settings(
fork in Test := true,
testOptions in Test := Seq(Tests.Argument(TestFrameworks.JUnit, "-a", "-v")),
libraryDependencies ++= (slf4jtest ++ specsBuild ++ junitInterface).map(_ % Test)
testOptions in Test := Seq(Tests.Argument(TestFrameworks.JUnit, "-a", "-v"))
)
.settings(
// The scaladoc generation
Expand Down Expand Up @@ -305,8 +303,53 @@ lazy val `play-ahc-ws-standalone` = project
`play-ws-standalone`
).aggregate(
`shaded`
).disablePlugins(sbtassembly.AssemblyPlugin)

//---------------------------------------------------------------
// JSON Readables and Writables
//---------------------------------------------------------------

lazy val `play-ws-standalone-json` = project
.in(file("play-ws-standalone-json"))
.settings(commonSettings)
.settings(formattingSettings)
.settings(SbtScalariform.scalariformSettings)
.settings(
fork in Test := true,
testOptions in Test := Seq(Tests.Argument(TestFrameworks.JUnit, "-a", "-v"))
)
.disablePlugins(sbtassembly.AssemblyPlugin)
.settings(
// The scaladoc generation
)
.settings(libraryDependencies ++= standaloneAhcWSJsonDependencies)
.dependsOn(
`play-ws-standalone`
).disablePlugins(sbtassembly.AssemblyPlugin)

//---------------------------------------------------------------
// XML Readables and Writables
//---------------------------------------------------------------

lazy val `play-ws-standalone-xml` = project
.in(file("play-ws-standalone-xml"))
.settings(commonSettings)
.settings(formattingSettings)
.settings(SbtScalariform.scalariformSettings)
.settings(
fork in Test := true,
testOptions in Test := Seq(Tests.Argument(TestFrameworks.JUnit, "-a", "-v"))
)
.settings(
// The scaladoc generation
)
.settings(libraryDependencies ++= standaloneAhcWSXMLDependencies)
.dependsOn(
`play-ws-standalone`
).disablePlugins(sbtassembly.AssemblyPlugin)

//---------------------------------------------------------------
// Integration Tests
//---------------------------------------------------------------

lazy val `integration-tests` = project.in(file("integration-tests"))
.settings(commonSettings)
Expand All @@ -318,13 +361,14 @@ lazy val `integration-tests` = project.in(file("integration-tests"))
fork in Test := true,
concurrentRestrictions += Tags.limitAll(1), // only one integration test at a time
testOptions in Test := Seq(Tests.Argument(TestFrameworks.JUnit, "-a", "-v")),
libraryDependencies ++= (specsBuild ++ akkaHttp).map(_ % Test)
libraryDependencies ++= akkaHttp.map(_ % Test) ++ testDependencies
)
.settings(libraryDependencies ++= standaloneAhcWSDependencies)
.settings(shadedAhcSettings)
.settings(shadedOAuthSettings)
.dependsOn(
`play-ahc-ws-standalone`
`play-ahc-ws-standalone`,
`play-ws-standalone-json`,
`play-ws-standalone-xml`
)
.disablePlugins(sbtassembly.AssemblyPlugin)

Expand All @@ -342,6 +386,8 @@ lazy val root = project
.aggregate(
`shaded`,
`play-ws-standalone`,
`play-ws-standalone-json`,
`play-ws-standalone-xml`,
`play-ahc-ws-standalone`,
`integration-tests`
)
Expand Down