Adrian Cole
11 years ago
7 changed files with 399 additions and 64 deletions
@ -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()); |
||||
``` |
@ -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>>() { |
||||
}; |
||||
} |
@ -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"; |
||||
} |
Loading…
Reference in new issue