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

Conversation

wsargent
Copy link
Member

@wsargent wsargent commented May 28, 2017

Add typed body to Play WS. After this, Play-WS should not have any dependencies at all on the body -- the response has a body of String / ByteString / Source[ByteString]. If you want to get something, you need to add a BodyReadable or BodyWritable function to transform it.

So for Scala, you use a context bound:

implicit val jsonWritable: BodyWritable[JsValue] = new JsonWritable(objectMapper)
val json: JsValue = ...
request.post(json);

For Java you do:

interface DefaultBodyWritables {
   default BodyWritable<JsonNode> body(JsonNode node) { ... }
}

And then you can use overloading so it's not quite so ugly:

class MyClass extends DefaultBodyWritables {
    request.post(body(json));
}

Although this only works so far, and falls over completely when you use generics, i.e. it can't tell a Source from a Source<Multipart.Part<Source>>, so you have to do things like

request.post(multipartBody(parts));

For readables, it looks the same, only you have to specify the type. So for Scala:

implicit val stringReadable: BodyReadable[String] = ...
val string = response.body[String]

And for Java, it looks like this:

class MyClass extends DefaultBodyReadables {
   JsonNode node = response.body(json());
}

@wsargent wsargent requested a review from schmitch May 28, 2017 22:38
build.sbt Outdated
// JSON Readables and Writables
//---------------------------------------------------------------

lazy val `play-ahc-ws-standalone-json` = project
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we just have play-ws-standalone-json and not have a dependency on ahc? I suppose it doesn't matter much since AHC is the only real implementation we have, but just wondering.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well the problem here is probably that we need the underlying response/request type, which is a ahc class.

this.contentType = contentType;
}

class InMemoryBody extends AbstractWSBody<ByteString> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this just a special case of the streamed body?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well I think for performance reasons it's good to have both.

*
* @param <A> the type of body.
*/
public interface BodyWritable<A> extends Supplier<WSBody<A>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we could use a common interface for both the Writeable interface used for Play bodies and this one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not saying that needs to be implemented now but would be interested in getting your thoughts on if that'd be eventually possible to write a single implementation that works for Play action and WS bodies.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tricky as we really need something that does => Source[ByteString] as the base type, and Writable does ByteString.

README.md Outdated
}
```

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if I try to do this on a non-streamed body?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source.single(response.bodyAsBytes) :-)

Copy link
Contributor

@schmitch schmitch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well small changes needed, but besides them it looks really really good.
Hm. The only thing which gmethvin already mentioned is that the play-json/xml readable/writeables need to have a direct dependency on ahc. But I think there is not a lot to go around that.

*/
public interface DefaultBodyWritables {

DefaultBodyWritables instance = new DefaultBodyWritables() {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for what is this instance? Especially since it's package-private?

}

// Always replace the content type header to make sure exactly one exists
List<String> contentTypeList = new ArrayList<>();
contentTypeList.add(contentType);
List<String> contentTypeList = Collections.singletonList(contentType);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might just inline it:
possiblyModifiedHeaders.set(HttpHeaders.Names.CONTENT_TYPE, Collections.singletonList(contentType)).
I mean the possibility is high that the java compiler will do that, but I think there is no reason to keep a variable around just for passing it a line below

README.md Outdated
To use the default type mappings in Java, you should use the following:

```java
import static play.libs.ws.ahc.DefaultBodyReadables.instance.*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is incorrect.
Basically you can't statically import default methods.
Well In the Java API I would prefer to have both ways either to have:

import play.libs.ws.ahc.DefaultBodyReadables;
import play.libs.ws.ahc.DefaultBodyWritables;

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())
       );
     }
 }

and a second version which does not have a implements:

import static play.libs.ws.ahc.DefaultBodyReadables.Static.*;
import static play.libs.ws.ahc.DefaultBodyWritables.Static.*;

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())
       );
     }
 }

Example:

public interface DefaultBodyWritables {

    DefaultBodyWritables instance = new DefaultBodyWritables() {};

    /**
     * Creates a {@link InMemoryBodyWritable} from a String, setting the content-type to "text/plain".
     *
     * @param s the string to pass in.
     * @return a {@link InMemoryBodyWritable} instance.
     */
    default InMemoryBodyWritable body(String s) {
        return new InMemoryBodyWritable(ByteString.fromString(s), "text/plain");
    }

public final class Static {

static InMemoryBodyWritable body(String s) {
        return instance.body(s);
    }

}

  // ...

I think that's the only way to mimic the Scala object DefaultBodyWritables. Another way would be to declare all methods as static then nobody would need to implement the interfaces, which is probably more java-ish but also harder for backwards compatibility.

build.sbt Outdated
// JSON Readables and Writables
//---------------------------------------------------------------

lazy val `play-ahc-ws-standalone-json` = project
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well the problem here is probably that we need the underlying response/request type, which is a ahc class.

this.contentType = contentType;
}

class InMemoryBody extends AbstractWSBody<ByteString> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well I think for performance reasons it's good to have both.

import java.util.List;
import java.util.Map;
import java.util.Optional;

public interface WSResponseHeaders {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be removed completly since it's no longer in use (or deprecated)

}

@Override
public String getContentType() {
final List<String> values = headers.get(HttpHeaders.Names.CONTENT_BASE);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note CONTENT_BASE is the wrong field, so we fix this here

WSRequestExecutor executor = foldRight(r -> {
StandaloneAhcWSRequest ahcWsRequest = (StandaloneAhcWSRequest) r;
Request ahcRequest = ahcWsRequest.buildRequest();
return client.executeStream(buildRequest(), materializer.executionContext());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enable request filters for requests that go through stream rather than execute()

stringListMap.forEach((key, values) -> values.forEach(value -> builder.addFormParam(key, value)));
possiblyModifiedHeaders.set(CONTENT_TYPE, singletonList(contentType));

if (bodyWritable instanceof InMemoryBodyWritable) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only in-memory or source

@schmitch schmitch dismissed their stale review June 6, 2017 12:30

PR applied changes and now differs from my initial review

@wsargent wsargent force-pushed the refactor-for-types branch 5 times, most recently from e7ddab0 to 0647195 Compare June 10, 2017 04:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants