Skip to content

Commit

Permalink
Merge pull request #70 from bennor/generic-interfaces
Browse files Browse the repository at this point in the history
Generic interface support
  • Loading branch information
anaisbetts committed Nov 3, 2014
2 parents fe93623 + 9c25bdf commit 54a12ee
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ namespace {{Namespace}}
using RefitInternalGenerated;
[Preserve]
public partial class AutoGenerated{{InterfaceName}} : {{InterfaceName}}
public partial class AutoGenerated{{InterfaceName}}{{#TypeParameters}}<{{.}}>{{/TypeParameters}} : {{InterfaceName}}{{#TypeParameters}}<{{.}}>{{/TypeParameters}}
{{#ConstraintClauses}}
{{.}}
{{/ConstraintClauses}}
{
public HttpClient Client { get; protected set; }
readonly Dictionary<string, Func<HttpClient, object[], object>> methodImpls;
Expand Down
10 changes: 9 additions & 1 deletion InterfaceStubGenerator/InterfaceStubGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ namespace Refit.Generator
// guess the class name based on our template
//
// What if the Interface is in another module? (since we copy usings, should be fine)
// What if the Interface itself is Generic? (fuck 'em)
public class InterfaceStubGenerator
{
public string GenerateInterfaceStubs(string[] paths)
Expand Down Expand Up @@ -104,6 +103,13 @@ public ClassTemplateInfo GenerateClassInfoForInterface(InterfaceDeclarationSynta
ret.Namespace = ns.Name.ToString();
ret.InterfaceName = interfaceTree.Identifier.ValueText;

if (interfaceTree.TypeParameterList != null) {
var typeParameters = interfaceTree.TypeParameterList.Parameters;
if (typeParameters.Any()) {
ret.TypeParameters = string.Join(", ", typeParameters.Select(p => p.Identifier.ValueText));
}
ret.ConstraintClauses = interfaceTree.ConstraintClauses.ToFullString().Trim();
}
ret.MethodList = interfaceTree.Members
.OfType<MethodDeclarationSyntax>()
.Select(x => new MethodTemplateInfo() {
Expand Down Expand Up @@ -147,6 +153,8 @@ public class ClassTemplateInfo
{
public string Namespace { get; set; }
public string InterfaceName { get; set; }
public string TypeParameters { get; set; }
public string ConstraintClauses { get; set; }
public List<MethodTemplateInfo> MethodList { get; set; }
}

Expand Down
2 changes: 1 addition & 1 deletion InterfaceStubGenerator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ static void Main(string[] args)

var template = generator.GenerateInterfaceStubs(files.Select(x => x.FullName).ToArray());

int retryCount = 3;
retry:
int retryCount = 3;
var file = default(FileStream);

// NB: Parallel build weirdness means that we might get >1 person
Expand Down
68 changes: 50 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Refit is a library heavily inspired by Square's
[Retrofit](http://square.github.io/retrofit) library, and it turns your REST
API into a live interface:

```cs
```csharp
public interface IGitHubApi
{
[Get("/users/{user}")]
Expand All @@ -15,7 +15,7 @@ public interface IGitHubApi
The `RestService` class generates an implementation of `IGitHubApi` that uses
`HttpClient` to make its calls:

```cs
```csharp
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com");

var octocat = await gitHubApi.GetUser("octocat");
Expand Down Expand Up @@ -43,13 +43,13 @@ Every method must have an HTTP attribute that provides the request method and
relative URL. There are five built-in annotations: Get, Post, Put, Delete, and
Head. The relative URL of the resource is specified in the annotation.

```cs
```csharp
[Get("/users/list")]
```

You can also specify query parameters in the URL:

```cs
```csharp
[Get("/users/list?sort=desc")]
```

Expand All @@ -60,7 +60,7 @@ surrounded by { and }.
If the name of your parameter doesn't match the name in the URL path, use the
`AliasAs` attribute.

```cs
```csharp
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId);
```
Expand All @@ -73,7 +73,7 @@ The comparison between parameter name and URL parameter is *not*
case-sensitive, so it will work correctly if you name your parameter `groupId`
in the path `/group/{groupid}/show` for example.

```cs
```csharp
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId, [AliasAs("sort")] string sortOrder);

Expand All @@ -86,7 +86,7 @@ GroupList(4, "desc");
One of the parameters in your method can be used as the body, by using the
Body attribute:

```cs
```csharp
[Post("/users/new")]
Task CreateUser([Body] User user);
```
Expand All @@ -106,7 +106,7 @@ JSON requests and responses are serialized/deserialized using Json.NET.
Default settings for the API can be configured by setting
_Newtonsoft.Json.JsonConvert.DefaultSettings_:

```cs
```csharp
JsonConvert.DefaultSettings =
() => new JsonSerializerSettings() {
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Expand All @@ -120,7 +120,7 @@ await PostSomeStuff(new { Day = DayOfWeek.Saturday });
Property serialization/deserialization can be customised using Json.NET's
JsonProperty attribute:

```cs
```csharp
public class Foo
{
// Works like [AliasAs("b")] would in form posts (see below)
Expand All @@ -136,7 +136,7 @@ initialize the Body attribute with `BodySerializationMethod.UrlEncoded`.

The parameter can be an `IDictionary`:

```cs
```csharp
public interface IMeasurementProtocolApi
{
[Post("/collect")]
Expand All @@ -159,7 +159,7 @@ be serialized as form fields in the request. This approach allows you to alias
property names using `[AliasAs("whatever")]` which can help if the API has
cryptic field names:

```cs
```csharp
public interface IMeasurementProtocolApi
{
[Post("/collect")]
Expand Down Expand Up @@ -200,7 +200,7 @@ await api.Collect(measurement);
You can set one or more static request headers for a request applying a `Headers`
attribute to the method:

```cs
```csharp
[Headers("User-Agent: Awesome Octocat App")]
[Get("/users/{user}")]
Task<User> GetUser(string user);
Expand All @@ -209,7 +209,7 @@ Task<User> GetUser(string user);
Static headers can also be added to _every request in the API_ by applying the
`Headers` attribute to the interface:

```cs
```csharp
[Headers("User-Agent: Awesome Octocat App")]
public interface IGitHubApi
{
Expand All @@ -226,7 +226,7 @@ public interface IGitHubApi
If the content of the header needs to be set at runtime, you can add a header
with a dynamic value to a request by applying a `Header` attribute to a parameter:

```cs
```csharp
[Get("/users/{user}")]
Task<User> GetUser(string user, [Header("Authorization")] string authorization);

Expand All @@ -245,7 +245,7 @@ a similar approach to the approach ASP.NET MVC takes with action filters &mdash;
* `Headers` attribute on the method
* `Header` attribute on a method parameter _(highest priority)_

```cs
```csharp
[Headers("X-Emoji: :rocket:")]
public interface IGitHubApi
{
Expand Down Expand Up @@ -277,7 +277,7 @@ Headers defined on an interface or method can be removed by redefining
a static header without a value (i.e. without `: <value>`) or passing `null` for
a dynamic header. _Empty strings will be included as empty headers._

```cs
```csharp
[Headers("X-Emoji: :rocket:")]
public interface IGitHubApi
{
Expand Down Expand Up @@ -319,7 +319,7 @@ will determine the content returned.
Returning Task without a type parameter will discard the content and solely
tell you whether or not the call succeeded:

```cs
```csharp
[Post("/users/new")]
Task CreateUser([Body] User user);

Expand All @@ -330,7 +330,7 @@ await CreateUser(someUser);
If the type parameter is 'HttpResponseMessage' or 'string', the raw response
message or the content as a string will be returned respectively.

```cs
```csharp
// Returns the content as a string (i.e. the JSON data)
[Get("/users/{user}")]
Task<string> GetUser(string user);
Expand All @@ -341,6 +341,38 @@ Task<string> GetUser(string user);
IObservable<HttpResponseMessage> GetUser(string user);
```

### Using generic interfaces

When using something like ASP.NET Web API, it's a fairly common pattern to have a whole stack of CRUD REST services. Refit now supports these, allowing you to define a single API interface with a generic type:

```csharp
public interface IReallyExcitingCrudApi<T, in TKey> where T : class
{
[Post("")]
Task<T> Create([Body] T paylod);

[Get("")]
Task<List<T>> ReadAll();

[Get("/{key}")]
Task<T> ReadOne(TKey key);

[Put("/{key}")]
Task Update(TKey key, [Body]T payload);

[Delete("/{key}")]
Task Delete(TKey key);
}
```

Which can be used like this:

```csharp
// The "/users" part here is kind of important if you want it to work for more
// than one type (unless you have a different domain for each type)
var api = RestService.For<IReallyExcitingCrudApi<User, string>>("http://api.example.com/users");
```

### What's missing / planned?

Currently Refit is missing the following features from Retrofit that are
Expand Down
24 changes: 22 additions & 2 deletions Refit-Tests/InterfaceStubGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ public void FindInterfacesSmokeTest()
input = IntegrationTestHelper.GetPath("InterfaceStubGenerator.cs");

result = fixture.FindInterfacesToGenerate(CSharpSyntaxTree.ParseFile(input));
Assert.AreEqual(1, result.Count);
Assert.AreEqual(2, result.Count);
Assert.True(result.Any(x => x.Identifier.ValueText == "IAmARefitInterfaceButNobodyUsesMe"));
Assert.True(result.Any(x => x.Identifier.ValueText == "IBoringCrudApi"));
Assert.True(result.All(x => x.Identifier.ValueText != "IAmNotARefitInterface"));
}

Expand All @@ -67,6 +68,7 @@ public void HasRefitHttpMethodAttributeSmokeTest()
Assert.IsTrue(result["AnotherRefitMethod"]);
Assert.IsFalse(result["NoConstantsAllowed"]);
Assert.IsFalse(result["NotARefitMethod"]);
Assert.IsTrue(result["ReadOne"]);
}

[Test]
Expand Down Expand Up @@ -97,7 +99,7 @@ public void GenerateTemplateInfoForInterfaceListSmokeTest()
.ToList();

var result = fixture.GenerateTemplateInfoForInterfaceList(input);
Assert.AreEqual(4, result.ClassList.Count);
Assert.AreEqual(5, result.ClassList.Count);
}

[Test]
Expand Down Expand Up @@ -136,4 +138,22 @@ public interface IAmNotARefitInterface
{
Task NotARefitMethod();
}

public interface IBoringCrudApi<T, in TKey> where T : class
{
[Post("")]
Task<T> Create([Body] T paylod);

[Get("")]
Task<List<T>> ReadAll();

[Get("/{key}")]
Task<T> ReadOne(TKey key);

[Put("/{key}")]
Task Update(TKey key, [Body]T payload);

[Delete("/{key}")]
Task Delete(TKey key);
}
}
77 changes: 77 additions & 0 deletions Refit-Tests/RefitStubs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,56 @@ public virtual Task NoConstantsAllowed()
}
}

namespace Refit.Tests
{
using RefitInternalGenerated;

[Preserve]
public partial class AutoGeneratedIBoringCrudApi<T, TKey> : IBoringCrudApi<T, TKey>
where T : class
{
public HttpClient Client { get; protected set; }
readonly Dictionary<string, Func<HttpClient, object[], object>> methodImpls;

public AutoGeneratedIBoringCrudApi(HttpClient client, IRequestBuilder requestBuilder)
{
methodImpls = requestBuilder.InterfaceHttpMethods.ToDictionary(k => k, v => requestBuilder.BuildRestResultFuncForMethod(v));
Client = client;
}

public virtual Task<T> Create(T paylod)
{
var arguments = new object[] { paylod };
return (Task<T>) methodImpls["Create"](Client, arguments);
}

public virtual Task<List<T>> ReadAll()
{
var arguments = new object[] { };
return (Task<List<T>>) methodImpls["ReadAll"](Client, arguments);
}

public virtual Task<T> ReadOne(TKey key)
{
var arguments = new object[] { key };
return (Task<T>) methodImpls["ReadOne"](Client, arguments);
}

public virtual Task Update(TKey key,T payload)
{
var arguments = new object[] { key,payload };
return (Task) methodImpls["Update"](Client, arguments);
}

public virtual Task Delete(TKey key)
{
var arguments = new object[] { key };
return (Task) methodImpls["Delete"](Client, arguments);
}

}
}

namespace Refit.Tests
{
using RefitInternalGenerated;
Expand Down Expand Up @@ -246,4 +296,31 @@ public virtual Task Get()
}
}

namespace Refit.Tests
{
using RefitInternalGenerated;

[Preserve]
public partial class AutoGeneratedIHttpBinApi<TResponse, TParam, THeader> : IHttpBinApi<TResponse, TParam, THeader>
where TResponse : class
where THeader : struct
{
public HttpClient Client { get; protected set; }
readonly Dictionary<string, Func<HttpClient, object[], object>> methodImpls;

public AutoGeneratedIHttpBinApi(HttpClient client, IRequestBuilder requestBuilder)
{
methodImpls = requestBuilder.InterfaceHttpMethods.ToDictionary(k => k, v => requestBuilder.BuildRestResultFuncForMethod(v));
Client = client;
}

public virtual Task<TResponse> Get(TParam param,THeader header)
{
var arguments = new object[] { param,header };
return (Task<TResponse>) methodImpls["Get"](Client, arguments);
}

}
}


0 comments on commit 54a12ee

Please sign in to comment.