Browse Source

Merge pull request #65 from davidmc24/builder

Add Feign.Builder (#34)
pull/66/head
Adrian Cole 12 years ago
parent
commit
b607fe373b
  1. 1
      CHANGES.md
  2. 24
      README.md
  3. 169
      core/src/main/java/feign/Feign.java
  4. 126
      core/src/test/java/feign/FeignBuilderTest.java

1
CHANGES.md

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
* Remove pattern decoders in favor of SaxDecoder.
* Use single non-generic Decoder/Encoder instead of sets of type-specific Decoders/Encoders.
* Decoders/Encoders are now more flexible, having access to the Response/RequestTemplate respectively.
* Added Feign.Builder to simplify client customizations without using Dagger.
### Version 4.4.1
* Fix NullPointerException on calling equals and hashCode.

24
README.md

@ -40,23 +40,33 @@ public static void main(String... args) { @@ -40,23 +40,33 @@ public static void main(String... args) {
Feign includes a fully functional json codec in the `feign-gson` extension. See the `Decoder` section for how to write your own.
### Customization
Feign has several aspects that can be customized. For simple cases, you can use `Feign.builder()` to construct an API interface with your custom components. For example:
```java
interface Bank {
@RequestLine("POST /account/{id}")
Account getAccountInfo(@Named("id") String id);
}
...
Bank bank = Feign.builder().decoder(new AccountDecoder()).target(Bank.class, "https://api.examplebank.com");
```
For further flexibility, you can use Dagger modules directly. See the `Dagger` section for more details.
### Request Interceptors
When you need to change all requests, regardless of their target, you'll want to configure a `RequestInterceptor`.
For example, if you are acting as an intermediary, you might want to propagate the `X-Forwarded-For` header.
```
@Module(library = true)
static class ForwardedForInterceptor implements RequestInterceptor {
@Provides(type = SET) RequestInterceptor provideThis() {
return this;
}
@Override public void apply(RequestTemplate template) {
template.header("X-Forwarded-For", "origin.host.com");
}
}
...
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule(), new ForwardedForInterceptor());
Bank bank = Feign.builder().decoder(accountDecoder).requestInterceptor(new ForwardedForInterceptor()).target(Bank.class, "https://api.examplebank.com");
```
### Multiple Interfaces
@ -65,7 +75,7 @@ Feign can produce multiple api interfaces. These are defined as `Target<T>` (de @@ -65,7 +75,7 @@ Feign can produce multiple api interfaces. These are defined as `Target<T>` (de
For example, the following pattern might decorate each request with the current url and auth token from the identity service.
```java
CloudDNS cloudDNS = Feign.create().newInstance(new CloudIdentityTarget<CloudDNS>(user, apiKey));
CloudDNS cloudDNS = Feign.builder().target(new CloudIdentityTarget<CloudDNS>(user, apiKey));
```
You can find [several examples](https://github.com/Netflix/feign/tree/master/feign-core/src/test/java/feign/examples) in the test tree. Do take time to look at them, as seeing is believing!

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

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package feign;
import dagger.Module;
import dagger.ObjectGraph;
import dagger.Provides;
import feign.Logger.NoOpLogger;
@ -25,12 +26,15 @@ import feign.codec.Decoder; @@ -25,12 +26,15 @@ import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import javax.inject.Inject;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Feign's purpose is to ease development against http apis that feign
@ -48,6 +52,10 @@ public abstract class Feign { @@ -48,6 +52,10 @@ public abstract class Feign {
*/
public abstract <T> T newInstance(Target<T> target);
public static Builder builder() {
return new Builder();
}
public static <T> T create(Class<T> apiType, String url, Object... modules) {
return create(new HardCodedTarget<T>(apiType, url), modules);
}
@ -78,7 +86,7 @@ public abstract class Feign { @@ -78,7 +86,7 @@ public abstract class Feign {
}
@SuppressWarnings("rawtypes")
@dagger.Module(complete = false, injects = Feign.class, library = true)
@dagger.Module(complete = false, injects = {Feign.class, Builder.class}, library = true)
public static class Defaults {
@Provides Logger.Level logLevel() {
@ -168,4 +176,163 @@ public abstract class Feign { @@ -168,4 +176,163 @@ public abstract class Feign {
modulesForGraph.add(module);
return modulesForGraph;
}
public static class Builder {
private final Set<RequestInterceptor> requestInterceptors = new LinkedHashSet<RequestInterceptor>();
@Inject Logger.Level logLevel;
@Inject Contract contract;
@Inject Client client;
@Inject Retryer retryer;
@Inject Logger logger;
@Inject Encoder encoder;
@Inject Decoder decoder;
@Inject ErrorDecoder errorDecoder;
@Inject Options options;
Builder() {
ObjectGraph.create(new Defaults()).inject(this);
}
public Builder logLevel(Logger.Level logLevel) {
this.logLevel = logLevel;
return this;
}
public Builder contract(Contract contract) {
this.contract = contract;
return this;
}
public Builder client(Client client) {
this.client = client;
return this;
}
public Builder retryer(Retryer retryer) {
this.retryer = retryer;
return this;
}
public Builder logger(Logger logger) {
this.logger = logger;
return this;
}
public Builder encoder(Encoder encoder) {
this.encoder = encoder;
return this;
}
public Builder decoder(Decoder decoder) {
this.decoder = decoder;
return this;
}
public Builder errorDecoder(ErrorDecoder errorDecoder) {
this.errorDecoder = errorDecoder;
return this;
}
public Builder options(Options options) {
this.options = options;
return this;
}
/**
* Adds a single request interceptor to the builder.
*/
public Builder requestInterceptor(RequestInterceptor requestInterceptor) {
this.requestInterceptors.add(requestInterceptor);
return this;
}
/**
* Sets the full set of request interceptors for the builder, overwriting any previous interceptors.
*/
public Builder requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {
this.requestInterceptors.clear();
for (RequestInterceptor requestInterceptor : requestInterceptors) {
this.requestInterceptors.add(requestInterceptor);
}
return this;
}
public <T> T target(Class<T> apiType, String url) {
return target(new HardCodedTarget<T>(apiType, url));
}
public <T> T target(Target<T> target) {
BuilderModule module = new BuilderModule(this);
return create(module).newInstance(target);
}
}
@Module(library = true, overrides = true, addsTo = Defaults.class)
static class BuilderModule {
private final Logger.Level logLevel;
private final Contract contract;
private final Client client;
private final Retryer retryer;
private final Logger logger;
private final Encoder encoder;
private final Decoder decoder;
private final ErrorDecoder errorDecoder;
private final Options options;
private final Set<RequestInterceptor> requestInterceptors;
BuilderModule(Builder builder) {
this.logLevel = builder.logLevel;
this.contract = builder.contract;
this.client = builder.client;
this.retryer = builder.retryer;
this.logger = builder.logger;
this.encoder = builder.encoder;
this.decoder = builder.decoder;
this.errorDecoder = builder.errorDecoder;
this.options = builder.options;
this.requestInterceptors = builder.requestInterceptors;
}
@Provides Logger.Level logLevel() {
return logLevel;
}
@Provides Contract contract() {
return contract;
}
@Provides Client client() {
return client;
}
@Provides Retryer retryer() {
return retryer;
}
@Provides Logger logger() {
return logger;
}
@Provides
Encoder encoder() {
return encoder;
}
@Provides
Decoder decoder() {
return decoder;
}
@Provides ErrorDecoder errorDecoder() {
return errorDecoder;
}
@Provides Options options() {
return options;
}
@Provides(type = Provides.Type.SET_VALUES) Set<RequestInterceptor> requestInterceptors() {
return requestInterceptors;
}
}
}

126
core/src/test/java/feign/FeignBuilderTest.java

@ -0,0 +1,126 @@ @@ -0,0 +1,126 @@
/*
* 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;
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.MockWebServer;
import com.google.mockwebserver.RecordedRequest;
import feign.codec.Decoder;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import org.testng.annotations.Test;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class FeignBuilderTest {
interface TestInterface {
@RequestLine("POST /") Response codecPost(String data);
@RequestLine("POST /") void encodedPost(List<String> data);
@RequestLine("POST /") String decodedPost();
}
@Test public void testDefaults() throws Exception {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("response data"));
server.play();
String url = "http://localhost:" + server.getPort();
try {
TestInterface api = Feign.builder().target(TestInterface.class, url);
Response response = api.codecPost("request data");
assertEquals(Util.toString(response.body().asReader()), "response data");
} finally {
server.shutdown();
assertEquals(server.getRequestCount(), 1);
assertEquals(server.takeRequest().getUtf8Body(), "request data");
}
}
@Test public void testOverrideEncoder() throws Exception {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("response data"));
server.play();
String url = "http://localhost:" + server.getPort();
Encoder encoder = new Encoder() {
@Override
public void encode(Object object, RequestTemplate template) throws EncodeException {
template.body(object.toString());
}
};
try {
TestInterface api = Feign.builder().encoder(encoder).target(TestInterface.class, url);
api.encodedPost(Arrays.asList("This", "is", "my", "request"));
} finally {
server.shutdown();
assertEquals(server.getRequestCount(), 1);
assertEquals(server.takeRequest().getUtf8Body(), "[This, is, my, request]");
}
}
@Test public void testOverrideDecoder() throws Exception {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("success!"));
server.play();
String url = "http://localhost:" + server.getPort();
Decoder decoder = new Decoder() {
@Override
public Object decode(Response response, Type type) {
return "fail";
}
};
try {
TestInterface api = Feign.builder().decoder(decoder).target(TestInterface.class, url);
assertEquals(api.decodedPost(), "fail");
} finally {
server.shutdown();
assertEquals(server.getRequestCount(), 1);
}
}
@Test public void testProvideRequestInterceptors() throws Exception {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("response data"));
server.play();
String url = "http://localhost:" + server.getPort();
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
template.header("Content-Type", "text/plain");
}
};
try {
TestInterface api = Feign.builder().requestInterceptor(requestInterceptor).target(TestInterface.class, url);
Response response = api.codecPost("request data");
assertEquals(Util.toString(response.body().asReader()), "response data");
} finally {
server.shutdown();
assertEquals(server.getRequestCount(), 1);
RecordedRequest request = server.takeRequest();
assertEquals(request.getUtf8Body(), "request data");
assertEquals(request.getHeader("Content-Type"), "text/plain");
}
}
}
Loading…
Cancel
Save