提交 130bcc7c 编写于 作者: J Jake Wharton 提交者: Jesse Wilson

Add Scala Future adapter. (#2521)

上级 aa1e87c9
......@@ -56,6 +56,7 @@
<rxjava.version>1.3.0</rxjava.version>
<rxjava2.version>2.0.0</rxjava2.version>
<guava.version>19.0</guava.version>
<scala.version>2.12.3</scala.version>
<!-- Converter Dependencies -->
<gson.version>2.8.2</gson.version>
......@@ -156,6 +157,11 @@
<artifactId>moshi</artifactId>
<version>${moshi.version}</version>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
......
......@@ -19,5 +19,6 @@
<module>java8</module>
<module>rxjava</module>
<module>rxjava2</module>
<module>scala</module>
</modules>
</project>
Scala Adapter
=============
An `Adapter` for adapting Scala `Future`.
Usage
-----
Add `ScalaCallAdapterFactory` as a `Call` adapter when building your `Retrofit` instance:
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://example.com/")
.addCallAdapterFactory(ScalaCallAdapterFactory.create())
.build();
```
Your service methods can now use `Future` as their return type.
```java
interface MyService {
@GET("/user")
Future<User> getUser();
}
```
Download
--------
Download [the latest JAR][2] or grab via [Maven][3]:
```xml
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>adapter-scala</artifactId>
<version>latest.version</version>
</dependency>
```
or [Gradle][3]:
```groovy
compile 'com.squareup.retrofit2:adapter-scala:latest.version'
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
[2]: https://search.maven.org/remote_content?g=com.squareup.retrofit2&a=adapter-scala&v=LATEST
[3]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.squareup.retrofit2%22%20a%3A%22adapter-scala%22
[snap]: https://oss.sonatype.org/content/repositories/snapshots/
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit-adapters</artifactId>
<version>2.4.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>adapter-scala</artifactId>
<name>Adapter: Scala</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>retrofit</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
<version>${animal.sniffer.version}</version>
<configuration>
<signature>
<groupId>org.kaazing.mojo.signature</groupId>
<artifactId>java18</artifactId>
<version>1.0</version>
</signature>
</configuration>
</plugin>
</plugins>
</build>
</project>
/*
* Copyright (C) 2017 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2.converter.scala;
import java.lang.reflect.Type;
import javax.annotation.Nonnull;
import retrofit2.Call;
import retrofit2.CallAdapter;
import retrofit2.Callback;
import retrofit2.HttpException;
import retrofit2.Response;
import scala.concurrent.Future;
import scala.concurrent.Promise;
final class BodyCallAdapter<T> implements CallAdapter<T, Future<T>> {
private final Type responseType;
BodyCallAdapter(Type responseType) {
this.responseType = responseType;
}
@Override public Type responseType() {
return responseType;
}
@Override public Future<T> adapt(@Nonnull Call<T> call) {
Promise<T> promise = Promise.apply();
call.enqueue(new Callback<T>() {
@Override public void onResponse(@Nonnull Call<T> call, @Nonnull Response<T> response) {
if (response.isSuccessful()) {
promise.success(response.body());
} else {
promise.failure(new HttpException(response));
}
}
@Override public void onFailure(@Nonnull Call<T> call, @Nonnull Throwable t) {
promise.failure(t);
}
});
return promise.future();
}
}
/*
* Copyright (C) 2017 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2.converter.scala;
import java.lang.reflect.Type;
import javax.annotation.Nonnull;
import retrofit2.Call;
import retrofit2.CallAdapter;
import retrofit2.Callback;
import retrofit2.Response;
import scala.concurrent.Future;
import scala.concurrent.Promise;
final class ResponseCallAdapter<T> implements CallAdapter<T, Future<Response<T>>> {
private final Type responseType;
ResponseCallAdapter(Type responseType) {
this.responseType = responseType;
}
@Override public Type responseType() {
return responseType;
}
@Override public Future<Response<T>> adapt(@Nonnull Call<T> call) {
Promise<Response<T>> promise = Promise.apply();
call.enqueue(new Callback<T>() {
@Override public void onResponse(@Nonnull Call<T> call, @Nonnull Response<T> response) {
promise.success(response);
}
@Override public void onFailure(@Nonnull Call<T> call, @Nonnull Throwable t) {
promise.failure(t);
}
});
return promise.future();
}
}
/*
* Copyright (C) 2017 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2.converter.scala;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import javax.annotation.Nonnull;
import retrofit2.CallAdapter;
import retrofit2.Response;
import retrofit2.Retrofit;
import scala.concurrent.Future;
/**
* A {@linkplain CallAdapter.Factory call adapter} which creates Scala futures.
* <p>
* Adding this class to {@link Retrofit} allows you to return {@link Future} from
* service methods.
* <pre><code>
* interface MyService {
* &#64;GET("user/me")
* Future&lt;User&gt; getUser()
* }
* </code></pre>
* There are two configurations supported for the {@code Future} type parameter:
* <ul>
* <li>Direct body (e.g., {@code Future<User>}) returns the deserialized body for 2XX
* responses, sets {@link retrofit2.HttpException HttpException} errors for non-2XX responses, and
* sets {@link IOException} for network errors.</li>
* <li>Response wrapped body (e.g., {@code Future<Response<User>>}) returns a
* {@link Response} object for all HTTP responses and sets {@link IOException} for network
* errors</li>
* </ul>
*/
public final class ScalaCallAdapterFactory extends CallAdapter.Factory {
public static ScalaCallAdapterFactory create() {
return new ScalaCallAdapterFactory();
}
private ScalaCallAdapterFactory() {
}
@Override
public CallAdapter<?, ?> get(@Nonnull Type returnType, @Nonnull Annotation[] annotations,
@Nonnull Retrofit retrofit) {
if (getRawType(returnType) != Future.class) {
return null;
}
if (!(returnType instanceof ParameterizedType)) {
throw new IllegalStateException(
"Future return type must be parameterized as Future<Foo> or Future<? extends Foo>");
}
Type innerType = getParameterUpperBound(0, (ParameterizedType) returnType);
if (getRawType(innerType) != Response.class) {
// Generic type is not Response<T>. Use it for body-only adapter.
return new BodyCallAdapter<>(innerType);
}
if (!(innerType instanceof ParameterizedType)) {
throw new IllegalStateException(
"Response must be parameterized as Response<Foo> or Response<? extends Foo>");
}
Type responseType = getParameterUpperBound(0, (ParameterizedType) innerType);
return new ResponseCallAdapter<>(responseType);
}
}
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2.converter.scala;
import java.io.IOException;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import retrofit2.HttpException;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.http.GET;
import scala.concurrent.Await;
import scala.concurrent.Future;
import scala.concurrent.duration.Duration;
import static java.util.concurrent.TimeUnit.SECONDS;
import static okhttp3.mockwebserver.SocketPolicy.DISCONNECT_AFTER_REQUEST;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
public final class FutureTest {
@Rule public final MockWebServer server = new MockWebServer();
interface Service {
@GET("/") Future<String> body();
@GET("/") Future<Response<String>> response();
}
private Service service;
@Before public void setUp() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(server.url("/"))
.addConverterFactory(new StringConverterFactory())
.addCallAdapterFactory(ScalaCallAdapterFactory.create())
.build();
service = retrofit.create(Service.class);
}
@Test public void bodySuccess200() throws Exception {
server.enqueue(new MockResponse().setBody("Hi"));
Future<String> future = service.body();
String result = Await.result(future, Duration.create(5, SECONDS));
assertThat(result).isEqualTo("Hi");
}
@Test public void bodySuccess404() {
server.enqueue(new MockResponse().setResponseCode(404));
Future<String> future = service.body();
try {
Await.result(future, Duration.create(5, SECONDS));
fail();
} catch (Exception e) {
assertThat(e)
.isInstanceOf(HttpException.class) // Required for backwards compatibility.
.isInstanceOf(retrofit2.HttpException.class)
.hasMessage("HTTP 404 Client Error");
}
}
@Test public void bodyFailure() {
server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AFTER_REQUEST));
Future<String> future = service.body();
try {
Await.result(future, Duration.create(5, SECONDS));
fail();
} catch (Exception e) {
assertThat(e).isInstanceOf(IOException.class);
}
}
@Test public void responseSuccess200() throws Exception {
server.enqueue(new MockResponse().setBody("Hi"));
Future<Response<String>> future = service.response();
Response<String> response = Await.result(future, Duration.create(5, SECONDS));
assertThat(response.isSuccessful()).isTrue();
assertThat(response.body()).isEqualTo("Hi");
}
@Test public void responseSuccess404() throws Exception {
server.enqueue(new MockResponse().setResponseCode(404).setBody("Hi"));
Future<Response<String>> future = service.response();
Response<String> response = Await.result(future, Duration.create(5, SECONDS));
assertThat(response.isSuccessful()).isFalse();
assertThat(response.errorBody().string()).isEqualTo("Hi");
}
@Test public void responseFailure() {
server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AFTER_REQUEST));
Future<Response<String>> future = service.response();
try {
Await.result(future, Duration.create(5, SECONDS));
fail();
} catch (Exception e) {
assertThat(e).isInstanceOf(IOException.class);
}
}
}
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2.converter.scala;
import com.google.common.reflect.TypeToken;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.List;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import retrofit2.CallAdapter;
import retrofit2.Response;
import retrofit2.Retrofit;
import scala.concurrent.Future;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
public final class ScalaCallAdapterFactoryTest {
private static final Annotation[] NO_ANNOTATIONS = new Annotation[0];
@Rule public final MockWebServer server = new MockWebServer();
private final CallAdapter.Factory factory = ScalaCallAdapterFactory.create();
private Retrofit retrofit;
@Before public void setUp() {
retrofit = new Retrofit.Builder()
.baseUrl(server.url("/"))
.addConverterFactory(new StringConverterFactory())
.addCallAdapterFactory(factory)
.build();
}
@Test public void responseType() {
Type bodyClass = new TypeToken<Future<String>>() {}.getType();
assertThat(factory.get(bodyClass, NO_ANNOTATIONS, retrofit).responseType())
.isEqualTo(String.class);
Type bodyWildcard = new TypeToken<Future<? extends String>>() {}.getType();
assertThat(factory.get(bodyWildcard, NO_ANNOTATIONS, retrofit).responseType())
.isEqualTo(String.class);
Type bodyGeneric = new TypeToken<Future<List<String>>>() {}.getType();
assertThat(factory.get(bodyGeneric, NO_ANNOTATIONS, retrofit).responseType())
.isEqualTo(new TypeToken<List<String>>() {}.getType());
Type responseClass = new TypeToken<Future<Response<String>>>() {}.getType();
assertThat(factory.get(responseClass, NO_ANNOTATIONS, retrofit).responseType())
.isEqualTo(String.class);
Type responseWildcard = new TypeToken<Future<Response<? extends String>>>() {}.getType();
assertThat(factory.get(responseWildcard, NO_ANNOTATIONS, retrofit).responseType())
.isEqualTo(String.class);
Type resultClass = new TypeToken<Future<Response<String>>>() {}.getType();
assertThat(factory.get(resultClass, NO_ANNOTATIONS, retrofit).responseType())
.isEqualTo(String.class);
Type resultWildcard = new TypeToken<Future<Response<? extends String>>>() {}.getType();
assertThat(factory.get(resultWildcard, NO_ANNOTATIONS, retrofit).responseType())
.isEqualTo(String.class);
}
@Test public void nonListenableFutureReturnsNull() {
CallAdapter<?, ?> adapter = factory.get(String.class, NO_ANNOTATIONS, retrofit);
assertThat(adapter).isNull();
}
@Test public void rawTypeThrows() {
Type observableType = new TypeToken<Future>() {}.getType();
try {
factory.get(observableType, NO_ANNOTATIONS, retrofit);
fail();
} catch (IllegalStateException e) {
assertThat(e).hasMessage(
"Future return type must be parameterized as Future<Foo> or Future<? extends Foo>");
}
}
@Test public void rawResponseTypeThrows() {
Type observableType = new TypeToken<Future<Response>>() {}.getType();
try {
factory.get(observableType, NO_ANNOTATIONS, retrofit);
fail();
} catch (IllegalStateException e) {
assertThat(e).hasMessage(
"Response must be parameterized as Response<Foo> or Response<? extends Foo>");
}
}
}
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2.converter.scala;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Converter;
import retrofit2.Retrofit;
final class StringConverterFactory extends Converter.Factory {
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
Retrofit retrofit) {
return new Converter<ResponseBody, String>() {
@Override public String convert(ResponseBody value) throws IOException {
return value.string();
}
};
}
@Override public Converter<?, RequestBody> requestBodyConverter(Type type,
Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
return new Converter<String, RequestBody>() {
@Override public RequestBody convert(String value) throws IOException {
return RequestBody.create(MediaType.parse("text/plain"), value);
}
};
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册