Skip to content

Commit 13e23e5

Browse files
author
Adrian Cole
committed
Merge pull request OpenFeign#22 from Netflix/gson
Added feign-gson codec, used via new GsonModule()
2 parents 31c0b2c + 9fb1c07 commit 13e23e5

File tree

7 files changed

+399
-64
lines changed

7 files changed

+399
-64
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
### Version 3.0
22
* Added support for asynchronous callbacks via `IncrementalCallback<T>` and `IncrementalDecoder.TextStream<T>`.
33
* Wire is now Logger, with configurable Logger.Level.
4+
* Added `feign-gson` codec, used via `new GsonModule()`
45
* changed codec to be similar to [WebSocket JSR 356](http://docs.oracle.com/javaee/7/api/javax/websocket/package-summary.html)
56
* Decoder is now `Decoder.TextStream<T>`
67
* BodyEncoder is now `Encoder.Text<T>`

README.md

Lines changed: 63 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -34,40 +34,9 @@ public static void main(String... args) {
3434
}
3535
}
3636
```
37-
### Decoders
38-
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:
39-
40-
```java
41-
@Module(overrides = true, library = true)
42-
static class GsonModule {
43-
@Provides(type = SET) Decoder decoder() {
44-
return new Decoder.TextStream<Object>() {
45-
Gson gson = new Gson();
4637

47-
@Override public Object decode(Reader reader, Type type) throws IOException {
48-
try {
49-
return gson.fromJson(reader, type);
50-
} catch (JsonIOException e) {
51-
if (e.getCause() != null && e.getCause() instanceof IOException) {
52-
throw IOException.class.cast(e.getCause());
53-
}
54-
throw e;
55-
}
56-
}
57-
};
58-
}
59-
}
60-
```
61-
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.
38+
Feign includes a fully functional json codec in the `feign-gson` extension. See the `Decoder` section for how to write your own.
6239

63-
#### Type-specific Decoders
64-
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`.
65-
66-
```
67-
@Provides(type = SET) Decoder zoneListDecoder(Provider<ListHostedZonesResponseHandler> handlers) {
68-
return new SAXDecoder<ZoneList>(handlers){};
69-
}
70-
```
7140
### Asynchronous Incremental Callbacks
7241
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.
7342

@@ -91,36 +60,6 @@ IncrementalCallback<Contributor> printlnObserver = new IncrementalCallback<Contr
9160
};
9261
github.contributors("netflix", "feign", printlnObserver);
9362
```
94-
#### Incremental Decoding
95-
When using an `IncrementalCallback<T>`, you'll need to configure an `IncrementalDecoderi.TextStream<T>` or a general one for all types (`IncrementalDecoder.TextStream<Object>`).
96-
97-
Here's how to wire in a reflective incremental json decoder:
98-
```java
99-
@Provides(type = SET) IncrementalDecoder incrementalDecoder(final Gson gson) {
100-
return new IncrementalDecoder.TextStream<Object>() {
101-
102-
@Override
103-
public void decode(Reader reader, Type type, IncrementalCallback<? super Object> incrementalCallback) throws IOException {
104-
JsonReader jsonReader = new JsonReader(reader);
105-
jsonReader.beginArray();
106-
while (jsonReader.hasNext()) {
107-
try {
108-
incrementalCallback.onNext(gson.fromJson(jsonReader, type));
109-
} catch (JsonIOException e) {
110-
if (e.getCause() != null && e.getCause() instanceof IOException) {
111-
throw IOException.class.cast(e.getCause());
112-
}
113-
throw e;
114-
}
115-
}
116-
jsonReader.endArray();
117-
}
118-
};
119-
}
120-
```
121-
122-
123-
12463
### Multiple Interfaces
12564
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.
12665

@@ -134,6 +73,14 @@ You can find [several examples](https://github.com/Netflix/feign/tree/master/fei
13473

13574
### Integrations
13675
Feign intends to work well within Netflix and other Open Source communities. Modules are welcome to integrate with your favorite projects!
76+
### Gson
77+
[GsonModule](https://github.com/Netflix/feign/tree/master/feign-gson) adds default encoders and decoders so you get get started with a json api.
78+
79+
Integration requires you pass `new GsonModule()` to `Feign.create()`, or add it to your graph with Dagger:
80+
```java
81+
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
82+
```
83+
13784
### JAX-RS
13885
[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.
13986

@@ -151,6 +98,60 @@ Integration requires you to pass your ribbon client name as the host part of the
15198
```java
15299
MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule());
153100
```
101+
102+
### Decoders
103+
The last argument to `Feign.create` allows you to specify additional configuration such as how to decode a responses, modeled in Dagger.
104+
105+
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>`).
106+
107+
The `GsonModule` in the `feign-gson` extension configures a (`Decoder.TextStream<Object>`) which parses objects from json using reflection.
108+
109+
Here's how you could write this yourself, using whatever library you prefer:
110+
```java
111+
@Module(overrides = true, library = true)
112+
static class JsonModule {
113+
@Provides(type = SET) Decoder decoder(final JsonParser parser) {
114+
return new Decoder.TextStream<Object>() {
115+
116+
@Override public Object decode(Reader reader, Type type) throws IOException {
117+
return parser.readJson(reader, type);
118+
}
119+
120+
};
121+
}
122+
}
123+
```
124+
#### Type-specific Decoders
125+
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`.
126+
127+
```
128+
@Provides(type = SET) Decoder zoneListDecoder(Provider<ListHostedZonesResponseHandler> handlers) {
129+
return new SAXDecoder<ZoneList>(handlers){};
130+
}
131+
```
132+
#### Incremental Decoding
133+
The last argument to `Feign.create` allows you to specify additional configuration such as how to decode a responses, modeled in Dagger.
134+
135+
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>`).
136+
137+
The `GsonModule` in the `feign-gson` extension configures a (`IncrementalDecoder.TextStream<Object>`) which parses objects from json using reflection.
138+
139+
Here's how you could write this yourself, using whatever library you prefer:
140+
```java
141+
@Provides(type = SET) IncrementalDecoder incrementalDecoder(final JsonParser parser) {
142+
return new IncrementalDecoder.TextStream<Object>() {
143+
144+
@Override
145+
public void decode(Reader reader, Type type, IncrementalCallback<? super Object> incrementalCallback) throws IOException {
146+
jsonReader.beginArray();
147+
while (jsonReader.hasNext()) {
148+
incrementalCallback.onNext(parser.readJson(reader, type));
149+
}
150+
jsonReader.endArray();
151+
}
152+
};
153+
}
154+
```
154155
### Advanced usage and Dagger
155156
#### Dagger
156157
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.

build.gradle

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,21 @@ project(':feign-jaxrs') {
6161
testCompile 'com.google.guava:guava:14.0.1'
6262
testCompile 'com.google.code.gson:gson:2.2.4'
6363
testCompile 'org.testng:testng:6.8.1'
64-
testCompile 'com.google.mockwebserver:mockwebserver:20130505'
64+
}
65+
}
66+
67+
project(':feign-gson') {
68+
apply plugin: 'java'
69+
70+
test {
71+
useTestNG()
72+
}
73+
74+
dependencies {
75+
compile project(':feign-core')
76+
compile 'com.google.code.gson:gson:2.2.4'
77+
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
78+
testCompile 'org.testng:testng:6.8.1'
6579
}
6680
}
6781

feign-gson/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Gson Codec
2+
===================
3+
4+
This module adds support for encoding and decoding json via the Gson library.
5+
6+
Add this to your object graph like so:
7+
8+
```java
9+
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
10+
```
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign.gson;
17+
18+
import com.google.gson.Gson;
19+
import com.google.gson.GsonBuilder;
20+
import com.google.gson.InstanceCreator;
21+
import com.google.gson.JsonIOException;
22+
import com.google.gson.TypeAdapter;
23+
import com.google.gson.internal.ConstructorConstructor;
24+
import com.google.gson.internal.bind.MapTypeAdapterFactory;
25+
import com.google.gson.reflect.TypeToken;
26+
import com.google.gson.stream.JsonReader;
27+
import com.google.gson.stream.JsonWriter;
28+
import dagger.Provides;
29+
import feign.IncrementalCallback;
30+
import feign.codec.Decoder;
31+
import feign.codec.EncodeException;
32+
import feign.codec.Encoder;
33+
import feign.codec.IncrementalDecoder;
34+
35+
import javax.inject.Inject;
36+
import javax.inject.Singleton;
37+
import java.io.IOException;
38+
import java.io.Reader;
39+
import java.lang.reflect.Type;
40+
import java.util.Collections;
41+
import java.util.Map;
42+
43+
import static dagger.Provides.Type.SET;
44+
45+
@dagger.Module(library = true, overrides = true)
46+
public final class GsonModule {
47+
48+
@Provides(type = SET) Encoder encoder(GsonCodec codec) {
49+
return codec;
50+
}
51+
52+
@Provides(type = SET) Decoder decoder(GsonCodec codec) {
53+
return codec;
54+
}
55+
56+
@Provides(type = SET) IncrementalDecoder incrementalDecoder(GsonCodec codec) {
57+
return codec;
58+
}
59+
60+
static class GsonCodec implements Encoder.Text<Object>, Decoder.TextStream<Object>, IncrementalDecoder.TextStream<Object> {
61+
private final Gson gson;
62+
63+
@Inject GsonCodec(Gson gson) {
64+
this.gson = gson;
65+
}
66+
67+
@Override public String encode(Object object) throws EncodeException {
68+
return gson.toJson(object);
69+
}
70+
71+
@Override public Object decode(Reader reader, Type type) throws IOException {
72+
return fromJson(new JsonReader(reader), type);
73+
}
74+
75+
@Override
76+
public void decode(Reader reader, Type type, IncrementalCallback<? super Object> incrementalCallback) throws IOException {
77+
JsonReader jsonReader = new JsonReader(reader);
78+
jsonReader.beginArray();
79+
while (jsonReader.hasNext()) {
80+
incrementalCallback.onNext(fromJson(jsonReader, type));
81+
}
82+
jsonReader.endArray();
83+
}
84+
85+
private Object fromJson(JsonReader jsonReader, Type type) throws IOException {
86+
try {
87+
return gson.fromJson(jsonReader, type);
88+
} catch (JsonIOException e) {
89+
if (e.getCause() != null && e.getCause() instanceof IOException) {
90+
throw IOException.class.cast(e.getCause());
91+
}
92+
throw e;
93+
}
94+
}
95+
}
96+
97+
// deals with scenario where gson Object type treats all numbers as doubles.
98+
@Provides TypeAdapter<Map<String, Object>> doubleToInt() {
99+
return new TypeAdapter<Map<String, Object>>() {
100+
TypeAdapter<Map<String, Object>> delegate = new MapTypeAdapterFactory(new ConstructorConstructor(
101+
Collections.<Type, InstanceCreator<?>>emptyMap()), false).create(new Gson(), token);
102+
103+
@Override
104+
public void write(JsonWriter out, Map<String, Object> value) throws IOException {
105+
delegate.write(out, value);
106+
}
107+
108+
@Override
109+
public Map<String, Object> read(JsonReader in) throws IOException {
110+
Map<String, Object> map = delegate.read(in);
111+
for (Map.Entry<String, Object> entry : map.entrySet()) {
112+
if (entry.getValue() instanceof Double) {
113+
entry.setValue(Double.class.cast(entry.getValue()).intValue());
114+
}
115+
}
116+
return map;
117+
}
118+
}.nullSafe();
119+
}
120+
121+
@Provides @Singleton Gson gson(TypeAdapter<Map<String, Object>> doubleToInt) {
122+
return new GsonBuilder().registerTypeAdapter(token.getType(), doubleToInt).setPrettyPrinting().create();
123+
}
124+
125+
protected final static TypeToken<Map<String, Object>> token = new TypeToken<Map<String, Object>>() {
126+
};
127+
}

0 commit comments

Comments
 (0)