Skip to content

Commit 369c022

Browse files
author
adriancole
committed
Replaced IncrementalCallback with full RxJava-style Observer support
1 parent a590c2d commit 369c022

File tree

21 files changed

+605
-383
lines changed

21 files changed

+605
-383
lines changed

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
### Version 4.0
2+
* Support RxJava-style Observers.
3+
* Return type can be `Observable<T>` for an async equiv of `Iterable<T>`.
4+
* `Observer<T>` replaces `IncrementalCallback<T>` and is passed to `Observable.subscribe()`.
5+
* On `Subscription.unsubscribe()`, `Observer.onNext()` will stop being called.
6+
17
### Version 3.0
28
* Added support for asynchronous callbacks via `IncrementalCallback<T>` and `IncrementalDecoder.TextStream<T>`.
39
* Wire is now Logger, with configurable Logger.Level.

README.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Feign makes writing java http clients easier
2-
Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSockets](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems).
2+
Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [RxJava](https://github.com/Netflix/RxJava), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSocket](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems).
33

44
### Why Feign and not X?
55

@@ -37,12 +37,20 @@ public static void main(String... args) {
3737

3838
Feign includes a fully functional json codec in the `feign-gson` extension. See the `Decoder` section for how to write your own.
3939

40-
### Asynchronous Incremental Callbacks
41-
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.
40+
### Observable Methods
41+
If specified as the last return type of a method `Observable<T>` will invoke a new http request for each call to `subscribe()`. This is the async equivalent to an `Iterable`.
42+
Here's how one looks:
43+
```java
44+
Observable<Contributor> observable = github.contributorsObservable("netflix", "feign");
45+
subscription = observable.subscribe(newObserver());
46+
subscription = observable.subscribe(newObserver());
47+
```
48+
49+
`Observer<T>` is fired as a background which adds new elements as they are decoded, or until `subscription.unsubscribe()` is called. Think of `Observer<T>` as an asynchronous equivalent to a lazy sequence.
4250

4351
Here's how one looks:
4452
```java
45-
IncrementalCallback<Contributor> printlnObserver = new IncrementalCallback<Contributor>() {
53+
Observer<Contributor> printlnObserver = new Observer<Contributor>() {
4654

4755
public int count;
4856

@@ -58,8 +66,10 @@ IncrementalCallback<Contributor> printlnObserver = new IncrementalCallback<Contr
5866
cause.printStackTrace();
5967
}
6068
};
61-
github.contributors("netflix", "feign", printlnObserver);
6269
```
70+
71+
For more robust integration with `Observable` check out [RxJava](https://github.com/Netflix/RxJava).
72+
6373
### Multiple Interfaces
6474
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.
6575

@@ -142,10 +152,10 @@ Here's how you could write this yourself, using whatever library you prefer:
142152
return new IncrementalDecoder.TextStream<Object>() {
143153

144154
@Override
145-
public void decode(Reader reader, Type type, IncrementalCallback<? super Object> incrementalCallback) throws IOException {
155+
public void decode(Reader reader, Type type, IncrementalCallback<? super Object> observer) throws IOException {
146156
jsonReader.beginArray();
147157
while (jsonReader.hasNext()) {
148-
incrementalCallback.onNext(parser.readJson(reader, type));
158+
observer.onNext(parser.readJson(reader, type));
149159
}
150160
jsonReader.endArray();
151161
}

build.gradle

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ project(':feign-core') {
4545
}
4646
}
4747

48-
project(':feign-jaxrs') {
48+
project(':feign-gson') {
4949
apply plugin: 'java'
5050

5151
test {
@@ -54,17 +54,13 @@ project(':feign-jaxrs') {
5454

5555
dependencies {
5656
compile project(':feign-core')
57-
compile 'javax.ws.rs:jsr311-api:1.1.1'
57+
compile 'com.google.code.gson:gson:2.2.4'
5858
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
59-
// for example classes
60-
testCompile project(':feign-core').sourceSets.test.output
61-
testCompile 'com.google.guava:guava:14.0.1'
62-
testCompile 'com.google.code.gson:gson:2.2.4'
6359
testCompile 'org.testng:testng:6.8.1'
6460
}
6561
}
6662

67-
project(':feign-gson') {
63+
project(':feign-jaxrs') {
6864
apply plugin: 'java'
6965

7066
test {
@@ -73,8 +69,12 @@ project(':feign-gson') {
7369

7470
dependencies {
7571
compile project(':feign-core')
76-
compile 'com.google.code.gson:gson:2.2.4'
72+
compile 'javax.ws.rs:jsr311-api:1.1.1'
7773
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
74+
// for example classes
75+
testCompile project(':feign-core').sourceSets.test.output
76+
testCompile project(':feign-gson')
77+
testCompile 'com.google.guava:guava:14.0.1'
7878
testCompile 'org.testng:testng:6.8.1'
7979
}
8080
}

feign-core/src/main/java/feign/Contract.java

Lines changed: 81 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -31,99 +31,105 @@
3131
/**
3232
* Defines what annotations and values are valid on interfaces.
3333
*/
34-
public abstract class Contract {
34+
public interface Contract {
3535

3636
/**
3737
* Called to parse the methods in the class that are linked to HTTP requests.
3838
*/
39-
public List<MethodMetadata> parseAndValidatateMetadata(Class<?> declaring) {
40-
List<MethodMetadata> metadata = new ArrayList<MethodMetadata>();
41-
for (Method method : declaring.getDeclaredMethods()) {
42-
if (method.getDeclaringClass() == Object.class)
43-
continue;
44-
metadata.add(parseAndValidatateMetadata(method));
45-
}
46-
return metadata;
47-
}
39+
List<MethodMetadata> parseAndValidatateMetadata(Class<?> declaring);
4840

49-
/**
50-
* Called indirectly by {@link #parseAndValidatateMetadata(Class)}.
51-
*/
52-
public MethodMetadata parseAndValidatateMetadata(Method method) {
53-
MethodMetadata data = new MethodMetadata();
54-
data.decodeInto(method.getGenericReturnType());
55-
data.configKey(Feign.configKey(method));
41+
public static abstract class BaseContract implements Contract {
5642

57-
for (Annotation methodAnnotation : method.getAnnotations()) {
58-
processAnnotationOnMethod(data, methodAnnotation, method);
43+
@Override public List<MethodMetadata> parseAndValidatateMetadata(Class<?> declaring) {
44+
List<MethodMetadata> metadata = new ArrayList<MethodMetadata>();
45+
for (Method method : declaring.getDeclaredMethods()) {
46+
if (method.getDeclaringClass() == Object.class)
47+
continue;
48+
metadata.add(parseAndValidatateMetadata(method));
49+
}
50+
return metadata;
5951
}
60-
checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)",
61-
method.getName());
62-
Class<?>[] parameterTypes = method.getParameterTypes();
6352

64-
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
65-
int count = parameterAnnotations.length;
66-
for (int i = 0; i < count; i++) {
67-
boolean isHttpAnnotation = false;
68-
if (parameterAnnotations[i] != null) {
69-
isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
53+
/**
54+
* Called indirectly by {@link #parseAndValidatateMetadata(Class)}.
55+
*/
56+
public MethodMetadata parseAndValidatateMetadata(Method method) {
57+
MethodMetadata data = new MethodMetadata();
58+
data.returnType(method.getGenericReturnType());
59+
data.configKey(Feign.configKey(method));
60+
61+
if (Observable.class.isAssignableFrom(method.getReturnType())) {
62+
Type context = method.getGenericReturnType();
63+
Type observableType = resolveLastTypeParameter(method.getGenericReturnType(), Observable.class);
64+
checkState(observableType != null, "Expected param %s to be Observable<X> or Observable<? super X> or a subtype",
65+
context, observableType);
66+
data.incrementalType(observableType);
67+
}
68+
69+
for (Annotation methodAnnotation : method.getAnnotations()) {
70+
processAnnotationOnMethod(data, methodAnnotation, method);
7071
}
71-
if (parameterTypes[i] == URI.class) {
72-
data.urlIndex(i);
73-
} else if (IncrementalCallback.class.isAssignableFrom(parameterTypes[i])) {
74-
checkState(method.getReturnType() == void.class, "IncrementalCallback methods must return void: %s", method);
75-
checkState(i == count - 1, "IncrementalCallback must be the last parameter: %s", method);
76-
Type context = method.getGenericParameterTypes()[i];
77-
Type incrementalCallbackType = resolveLastTypeParameter(context, IncrementalCallback.class);
78-
data.decodeInto(incrementalCallbackType);
79-
data.incrementalCallbackIndex(i);
80-
checkState(incrementalCallbackType != null, "Expected param %s to be IncrementalCallback<X> or IncrementalCallback<? super X> or a subtype",
81-
context, incrementalCallbackType);
82-
} else if (!isHttpAnnotation) {
83-
checkState(data.formParams().isEmpty(), "Body parameters cannot be used with form parameters.");
84-
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
85-
data.bodyIndex(i);
86-
data.bodyType(method.getGenericParameterTypes()[i]);
72+
checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)",
73+
method.getName());
74+
Class<?>[] parameterTypes = method.getParameterTypes();
75+
76+
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
77+
int count = parameterAnnotations.length;
78+
for (int i = 0; i < count; i++) {
79+
boolean isHttpAnnotation = false;
80+
if (parameterAnnotations[i] != null) {
81+
isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
82+
}
83+
if (parameterTypes[i] == URI.class) {
84+
data.urlIndex(i);
85+
} else if (!isHttpAnnotation) {
86+
checkState(!Observer.class.isAssignableFrom(parameterTypes[i]),
87+
"Please return Observer as opposed to passing an Observable arg: %s", method);
88+
checkState(data.formParams().isEmpty(), "Body parameters cannot be used with form parameters.");
89+
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
90+
data.bodyIndex(i);
91+
data.bodyType(method.getGenericParameterTypes()[i]);
92+
}
8793
}
94+
return data;
8895
}
89-
return data;
90-
}
9196

92-
/**
93-
* @param data metadata collected so far relating to the current java method.
94-
* @param annotation annotations present on the current method annotation.
95-
* @param method method currently being processed.
96-
*/
97-
protected abstract void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method);
97+
/**
98+
* @param data metadata collected so far relating to the current java method.
99+
* @param annotation annotations present on the current method annotation.
100+
* @param method method currently being processed.
101+
*/
102+
protected abstract void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method);
98103

99-
/**
100-
* @param data metadata collected so far relating to the current java method.
101-
* @param annotations annotations present on the current parameter annotation.
102-
* @param paramIndex if you find a name in {@code annotations}, call {@link #nameParam(MethodMetadata, String,
103-
* int)} with this as the last parameter.
104-
* @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an http-relevant
105-
* annotation.
106-
*/
107-
protected abstract boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex);
104+
/**
105+
* @param data metadata collected so far relating to the current java method.
106+
* @param annotations annotations present on the current parameter annotation.
107+
* @param paramIndex if you find a name in {@code annotations}, call {@link #nameParam(MethodMetadata, String,
108+
* int)} with this as the last parameter.
109+
* @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an http-relevant
110+
* annotation.
111+
*/
112+
protected abstract boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex);
108113

109114

110-
protected Collection<String> addTemplatedParam(Collection<String> possiblyNull, String name) {
111-
if (possiblyNull == null)
112-
possiblyNull = new ArrayList<String>();
113-
possiblyNull.add(String.format("{%s}", name));
114-
return possiblyNull;
115-
}
115+
protected Collection<String> addTemplatedParam(Collection<String> possiblyNull, String name) {
116+
if (possiblyNull == null)
117+
possiblyNull = new ArrayList<String>();
118+
possiblyNull.add(String.format("{%s}", name));
119+
return possiblyNull;
120+
}
116121

117-
/**
118-
* links a parameter name to its index in the method signature.
119-
*/
120-
protected void nameParam(MethodMetadata data, String name, int i) {
121-
Collection<String> names = data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList<String>();
122-
names.add(name);
123-
data.indexToName().put(i, names);
122+
/**
123+
* links a parameter name to its index in the method signature.
124+
*/
125+
protected void nameParam(MethodMetadata data, String name, int i) {
126+
Collection<String> names = data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList<String>();
127+
names.add(name);
128+
data.indexToName().put(i, names);
129+
}
124130
}
125131

126-
static class Default extends Contract {
132+
static class Default extends BaseContract {
127133

128134
@Override
129135
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {

feign-core/src/main/java/feign/Feign.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public static class Defaults {
137137
}
138138

139139
/**
140-
* Used for both http invocation and decoding when incrementalCallbacks are used.
140+
* Used for both http invocation and decoding when observers are used.
141141
*/
142142
@Provides @Singleton @Named("http") Executor httpExecutor() {
143143
return Executors.newCachedThreadPool(new ThreadFactory() {

0 commit comments

Comments
 (0)