Browse Source

Added feign-gson codec, used via new GsonModule()

pull/22/head
adriancole 11 years ago
parent
commit
9fb1c0719d
  1. 1
      CHANGES.md
  2. 125
      README.md
  3. 16
      build.gradle
  4. 10
      feign-gson/README.md
  5. 127
      feign-gson/src/main/java/feign/gson/GsonModule.java
  6. 182
      feign-gson/src/test/java/feign/gson/GsonModuleTest.java
  7. 2
      settings.gradle

1
CHANGES.md

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
### Version 3.0
* Added support for asynchronous callbacks via `IncrementalCallback<T>` and `IncrementalDecoder.TextStream<T>`.
* Wire is now Logger, with configurable Logger.Level.
* Added `feign-gson` codec, used via `new GsonModule()`
* changed codec to be similar to [WebSocket JSR 356](http://docs.oracle.com/javaee/7/api/javax/websocket/package-summary.html)
* Decoder is now `Decoder.TextStream<T>`
* BodyEncoder is now `Encoder.Text<T>`

125
README.md

@ -34,40 +34,9 @@ public static void main(String... args) { @@ -34,40 +34,9 @@ public static void main(String... args) {
}
}
```
### Decoders
The last argument to `Feign.create` specifies how to decode the responses, modeled in Dagger. Here's how it looks to wire in a default gson decoder:
```java
@Module(overrides = true, library = true)
static class GsonModule {
@Provides(type = SET) Decoder decoder() {
return new Decoder.TextStream<Object>() {
Gson gson = new Gson();
@Override public Object decode(Reader reader, Type type) throws IOException {
try {
return gson.fromJson(reader, type);
} catch (JsonIOException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw IOException.class.cast(e.getCause());
}
throw e;
}
}
};
}
}
```
Feign doesn't offer a built-in json decoder as you can see above it is very few lines of code to wire yours in. If you are a jackson user, you'd probably thank us for not dragging in a dependency you don't use.
Feign includes a fully functional json codec in the `feign-gson` extension. See the `Decoder` section for how to write your own.
#### Type-specific Decoders
The generic parameter of `Decoder.TextStream<T>` designates which The type parameter is either a concrete type, or `Object`, if your decoder can handle multiple types. To add a type-specific decoder, ensure your type parameter is correct. Here's an example of an xml decoder that will only apply to methods that return `ZoneList`.
```
@Provides(type = SET) Decoder zoneListDecoder(Provider<ListHostedZonesResponseHandler> handlers) {
return new SAXDecoder<ZoneList>(handlers){};
}
```
### Asynchronous Incremental Callbacks
If specified as the last argument of a method `IncrementalCallback<T>` fires a background task to add new elements to the callback as they are decoded. Think of `IncrementalCallback<T>` as an asynchronous equivalent to a lazy sequence.
@ -91,36 +60,6 @@ IncrementalCallback<Contributor> printlnObserver = new IncrementalCallback<Contr @@ -91,36 +60,6 @@ IncrementalCallback<Contributor> printlnObserver = new IncrementalCallback<Contr
};
github.contributors("netflix", "feign", printlnObserver);
```
#### Incremental Decoding
When using an `IncrementalCallback<T>`, you'll need to configure an `IncrementalDecoderi.TextStream<T>` or a general one for all types (`IncrementalDecoder.TextStream<Object>`).
Here's how to wire in a reflective incremental json decoder:
```java
@Provides(type = SET) IncrementalDecoder incrementalDecoder(final Gson gson) {
return new IncrementalDecoder.TextStream<Object>() {
@Override
public void decode(Reader reader, Type type, IncrementalCallback<? super Object> incrementalCallback) throws IOException {
JsonReader jsonReader = new JsonReader(reader);
jsonReader.beginArray();
while (jsonReader.hasNext()) {
try {
incrementalCallback.onNext(gson.fromJson(jsonReader, type));
} catch (JsonIOException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw IOException.class.cast(e.getCause());
}
throw e;
}
}
jsonReader.endArray();
}
};
}
```
### Multiple Interfaces
Feign can produce multiple api interfaces. These are defined as `Target<T>` (default `HardCodedTarget<T>`), which allow for dynamic discovery and decoration of requests prior to execution.
@ -134,6 +73,14 @@ You can find [several examples](https://github.com/Netflix/feign/tree/master/fei @@ -134,6 +73,14 @@ You can find [several examples](https://github.com/Netflix/feign/tree/master/fei
### Integrations
Feign intends to work well within Netflix and other Open Source communities. Modules are welcome to integrate with your favorite projects!
### Gson
[GsonModule](https://github.com/Netflix/feign/tree/master/feign-gson) adds default encoders and decoders so you get get started with a json api.
Integration requires you pass `new GsonModule()` to `Feign.create()`, or add it to your graph with Dagger:
```java
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
```
### JAX-RS
[JAXRSModule](https://github.com/Netflix/feign/tree/master/feign-jaxrs) overrides annotation processing to instead use standard ones supplied by the JAX-RS specification. This is currently targeted at the 1.1 spec.
@ -151,6 +98,60 @@ Integration requires you to pass your ribbon client name as the host part of the @@ -151,6 +98,60 @@ Integration requires you to pass your ribbon client name as the host part of the
```java
MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule());
```
### Decoders
The last argument to `Feign.create` allows you to specify additional configuration such as how to decode a responses, modeled in Dagger.
If any methods in your interface return types besides `void` or `String`, you'll need to configure a `Decoder.TextStream<T>` or a general one for all types (`Decoder.TextStream<Object>`).
The `GsonModule` in the `feign-gson` extension configures a (`Decoder.TextStream<Object>`) which parses objects from json using reflection.
Here's how you could write this yourself, using whatever library you prefer:
```java
@Module(overrides = true, library = true)
static class JsonModule {
@Provides(type = SET) Decoder decoder(final JsonParser parser) {
return new Decoder.TextStream<Object>() {
@Override public Object decode(Reader reader, Type type) throws IOException {
return parser.readJson(reader, type);
}
};
}
}
```
#### Type-specific Decoders
The generic parameter of `Decoder.TextStream<T>` designates which The type parameter is either a concrete type, or `Object`, if your decoder can handle multiple types. To add a type-specific decoder, ensure your type parameter is correct. Here's an example of an xml decoder that will only apply to methods that return `ZoneList`.
```
@Provides(type = SET) Decoder zoneListDecoder(Provider<ListHostedZonesResponseHandler> handlers) {
return new SAXDecoder<ZoneList>(handlers){};
}
```
#### Incremental Decoding
The last argument to `Feign.create` allows you to specify additional configuration such as how to decode a responses, modeled in Dagger.
When using an `IncrementalCallback<T>`, if `T` is not `Void` or `String`, you'll need to configure an `IncrementalDecoder.TextStream<T>` or a general one for all types (`IncrementalDecoder.TextStream<Object>`).
The `GsonModule` in the `feign-gson` extension configures a (`IncrementalDecoder.TextStream<Object>`) which parses objects from json using reflection.
Here's how you could write this yourself, using whatever library you prefer:
```java
@Provides(type = SET) IncrementalDecoder incrementalDecoder(final JsonParser parser) {
return new IncrementalDecoder.TextStream<Object>() {
@Override
public void decode(Reader reader, Type type, IncrementalCallback<? super Object> incrementalCallback) throws IOException {
jsonReader.beginArray();
while (jsonReader.hasNext()) {
incrementalCallback.onNext(parser.readJson(reader, type));
}
jsonReader.endArray();
}
};
}
```
### Advanced usage and Dagger
#### Dagger
Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger.

16
build.gradle

@ -61,7 +61,21 @@ project(':feign-jaxrs') { @@ -61,7 +61,21 @@ project(':feign-jaxrs') {
testCompile 'com.google.guava:guava:14.0.1'
testCompile 'com.google.code.gson:gson:2.2.4'
testCompile 'org.testng:testng:6.8.1'
testCompile 'com.google.mockwebserver:mockwebserver:20130505'
}
}
project(':feign-gson') {
apply plugin: 'java'
test {
useTestNG()
}
dependencies {
compile project(':feign-core')
compile 'com.google.code.gson:gson:2.2.4'
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
testCompile 'org.testng:testng:6.8.1'
}
}

10
feign-gson/README.md

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
Gson Codec
===================
This module adds support for encoding and decoding json via the Gson library.
Add this to your object graph like so:
```java
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
```

127
feign-gson/src/main/java/feign/gson/GsonModule.java

@ -0,0 +1,127 @@ @@ -0,0 +1,127 @@
/*
* Copyright 2013 Netflix, 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 feign.gson;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import com.google.gson.JsonIOException;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.internal.bind.MapTypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import dagger.Provides;
import feign.IncrementalCallback;
import feign.codec.Decoder;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.codec.IncrementalDecoder;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Map;
import static dagger.Provides.Type.SET;
@dagger.Module(library = true, overrides = true)
public final class GsonModule {
@Provides(type = SET) Encoder encoder(GsonCodec codec) {
return codec;
}
@Provides(type = SET) Decoder decoder(GsonCodec codec) {
return codec;
}
@Provides(type = SET) IncrementalDecoder incrementalDecoder(GsonCodec codec) {
return codec;
}
static class GsonCodec implements Encoder.Text<Object>, Decoder.TextStream<Object>, IncrementalDecoder.TextStream<Object> {
private final Gson gson;
@Inject GsonCodec(Gson gson) {
this.gson = gson;
}
@Override public String encode(Object object) throws EncodeException {
return gson.toJson(object);
}
@Override public Object decode(Reader reader, Type type) throws IOException {
return fromJson(new JsonReader(reader), type);
}
@Override
public void decode(Reader reader, Type type, IncrementalCallback<? super Object> incrementalCallback) throws IOException {
JsonReader jsonReader = new JsonReader(reader);
jsonReader.beginArray();
while (jsonReader.hasNext()) {
incrementalCallback.onNext(fromJson(jsonReader, type));
}
jsonReader.endArray();
}
private Object fromJson(JsonReader jsonReader, Type type) throws IOException {
try {
return gson.fromJson(jsonReader, type);
} catch (JsonIOException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw IOException.class.cast(e.getCause());
}
throw e;
}
}
}
// deals with scenario where gson Object type treats all numbers as doubles.
@Provides TypeAdapter<Map<String, Object>> doubleToInt() {
return new TypeAdapter<Map<String, Object>>() {
TypeAdapter<Map<String, Object>> delegate = new MapTypeAdapterFactory(new ConstructorConstructor(
Collections.<Type, InstanceCreator<?>>emptyMap()), false).create(new Gson(), token);
@Override
public void write(JsonWriter out, Map<String, Object> value) throws IOException {
delegate.write(out, value);
}
@Override
public Map<String, Object> read(JsonReader in) throws IOException {
Map<String, Object> map = delegate.read(in);
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof Double) {
entry.setValue(Double.class.cast(entry.getValue()).intValue());
}
}
return map;
}
}.nullSafe();
}
@Provides @Singleton Gson gson(TypeAdapter<Map<String, Object>> doubleToInt) {
return new GsonBuilder().registerTypeAdapter(token.getType(), doubleToInt).setPrettyPrinting().create();
}
protected final static TypeToken<Map<String, Object>> token = new TypeToken<Map<String, Object>>() {
};
}

182
feign-gson/src/test/java/feign/gson/GsonModuleTest.java

@ -0,0 +1,182 @@ @@ -0,0 +1,182 @@
/*
* Copyright 2013 Netflix, 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 feign.gson;
import com.google.gson.reflect.TypeToken;
import dagger.Module;
import dagger.ObjectGraph;
import feign.IncrementalCallback;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.IncrementalDecoder;
import org.testng.annotations.Test;
import javax.inject.Inject;
import java.io.StringReader;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.fail;
@Test
public class GsonModuleTest {
@Test public void providesEncoderDecoderAndIncrementalDecoder() throws Exception {
@Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
@Inject Set<Encoder> encoders;
@Inject Set<Decoder> decoders;
@Inject Set<IncrementalDecoder> incrementalDecoders;
}
SetBindings bindings = new SetBindings();
ObjectGraph.create(bindings).inject(bindings);
assertEquals(bindings.encoders.size(), 1);
assertEquals(bindings.encoders.iterator().next().getClass(), GsonModule.GsonCodec.class);
assertEquals(bindings.decoders.size(), 1);
assertEquals(bindings.decoders.iterator().next().getClass(), GsonModule.GsonCodec.class);
assertEquals(bindings.incrementalDecoders.size(), 1);
assertEquals(bindings.incrementalDecoders.iterator().next().getClass(), GsonModule.GsonCodec.class);
}
@Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
@Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
@Inject Set<Encoder> encoders;
}
SetBindings bindings = new SetBindings();
ObjectGraph.create(bindings).inject(bindings);
Map<String, Object> map = new LinkedHashMap<String, Object>();
map.put("foo", 1);
assertEquals(Encoder.Text.class.cast(bindings.encoders.iterator().next()).encode(map), ""//
+ "{\n" //
+ " \"foo\": 1\n" //
+ "}");
}
@Test public void encodesFormParams() throws Exception {
@Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
@Inject Set<Encoder> encoders;
}
SetBindings bindings = new SetBindings();
ObjectGraph.create(bindings).inject(bindings);
Map<String, Object> form = new LinkedHashMap<String, Object>();
form.put("foo", 1);
form.put("bar", Arrays.asList(2, 3));
assertEquals(Encoder.Text.class.cast(bindings.encoders.iterator().next()).encode(form), ""//
+ "{\n" //
+ " \"foo\": 1,\n" //
+ " \"bar\": [\n" //
+ " 2,\n" //
+ " 3\n" //
+ " ]\n" //
+ "}");
}
static class Zone extends LinkedHashMap<String, Object> {
Zone() {
// for reflective instantiation.
}
Zone(String name) {
this(name, null);
}
Zone(String name, String id) {
put("name", name);
if (id != null)
put("id", id);
}
private static final long serialVersionUID = 1L;
}
@Test public void decodes() throws Exception {
@Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
@Inject Set<Decoder> decoders;
}
SetBindings bindings = new SetBindings();
ObjectGraph.create(bindings).inject(bindings);
List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("denominator.io."));
zones.add(new Zone("denominator.io.", "ABCD"));
assertEquals(Decoder.TextStream.class.cast(bindings.decoders.iterator().next())
.decode(new StringReader(zonesJson), new TypeToken<List<Zone>>() {
}.getType()), zones);
}
@Test public void decodesIncrementally() throws Exception {
@Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
@Inject Set<IncrementalDecoder> decoders;
}
SetBindings bindings = new SetBindings();
ObjectGraph.create(bindings).inject(bindings);
final List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("denominator.io."));
zones.add(new Zone("denominator.io.", "ABCD"));
final AtomicInteger index = new AtomicInteger(0);
IncrementalCallback<Zone> zoneCallback = new IncrementalCallback<Zone>() {
@Override public void onNext(Zone element) {
assertEquals(element, zones.get(index.getAndIncrement()));
}
@Override public void onSuccess() {
// decoder shouldn't call onSuccess
fail();
}
@Override public void onFailure(Throwable cause) {
// decoder shouldn't call onFailure
fail();
}
};
IncrementalDecoder.TextStream.class.cast(bindings.decoders.iterator().next())
.decode(new StringReader(zonesJson), Zone.class, zoneCallback);
assertEquals(index.get(), 2);
}
private String zonesJson = ""//
+ "[\n"//
+ " {\n"//
+ " \"name\": \"denominator.io.\"\n"//
+ " },\n"//
+ " {\n"//
+ " \"name\": \"denominator.io.\",\n"//
+ " \"id\": \"ABCD\"\n"//
+ " }\n"//
+ "]\n";
}

2
settings.gradle

@ -1,2 +1,2 @@ @@ -1,2 +1,2 @@
rootProject.name='feign'
include 'feign-core', 'feign-jaxrs', 'feign-ribbon', 'examples:feign-example-cli'
include 'feign-core', 'feign-gson', 'feign-jaxrs', 'feign-ribbon', 'examples:feign-example-cli'

Loading…
Cancel
Save