diff --git a/InterfaceStubGenerator/GeneratedInterfaceStubTemplate.mustache b/InterfaceStubGenerator/GeneratedInterfaceStubTemplate.mustache index 463b77d97..8f837e646 100644 --- a/InterfaceStubGenerator/GeneratedInterfaceStubTemplate.mustache +++ b/InterfaceStubGenerator/GeneratedInterfaceStubTemplate.mustache @@ -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> methodImpls; diff --git a/InterfaceStubGenerator/InterfaceStubGenerator.cs b/InterfaceStubGenerator/InterfaceStubGenerator.cs index f47c463f6..51fb0a32f 100644 --- a/InterfaceStubGenerator/InterfaceStubGenerator.cs +++ b/InterfaceStubGenerator/InterfaceStubGenerator.cs @@ -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) @@ -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() .Select(x => new MethodTemplateInfo() { @@ -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 MethodList { get; set; } } diff --git a/InterfaceStubGenerator/Program.cs b/InterfaceStubGenerator/Program.cs index 63d6f9f6d..77663e6ce 100644 --- a/InterfaceStubGenerator/Program.cs +++ b/InterfaceStubGenerator/Program.cs @@ -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 diff --git a/README.md b/README.md index 8dd00c5e5..1eed95862 100644 --- a/README.md +++ b/README.md @@ -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}")] @@ -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("https://api.github.com"); var octocat = await gitHubApi.GetUser("octocat"); @@ -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")] ``` @@ -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> GroupList([AliasAs("id")] int groupId); ``` @@ -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> GroupList([AliasAs("id")] int groupId, [AliasAs("sort")] string sortOrder); @@ -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); ``` @@ -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(), @@ -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) @@ -136,7 +136,7 @@ initialize the Body attribute with `BodySerializationMethod.UrlEncoded`. The parameter can be an `IDictionary`: -```cs +```csharp public interface IMeasurementProtocolApi { [Post("/collect")] @@ -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")] @@ -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 GetUser(string user); @@ -209,7 +209,7 @@ Task 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 { @@ -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 GetUser(string user, [Header("Authorization")] string authorization); @@ -245,7 +245,7 @@ a similar approach to the approach ASP.NET MVC takes with action filters — * `Headers` attribute on the method * `Header` attribute on a method parameter _(highest priority)_ -```cs +```csharp [Headers("X-Emoji: :rocket:")] public interface IGitHubApi { @@ -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 `: `) or passing `null` for a dynamic header. _Empty strings will be included as empty headers._ -```cs +```csharp [Headers("X-Emoji: :rocket:")] public interface IGitHubApi { @@ -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); @@ -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 GetUser(string user); @@ -341,6 +341,38 @@ Task GetUser(string user); IObservable 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 where T : class +{ + [Post("")] + Task Create([Body] T paylod); + + [Get("")] + Task> ReadAll(); + + [Get("/{key}")] + Task 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>("http://api.example.com/users"); +``` + ### What's missing / planned? Currently Refit is missing the following features from Retrofit that are diff --git a/Refit-Tests/InterfaceStubGenerator.cs b/Refit-Tests/InterfaceStubGenerator.cs index bb3d38013..8f8612b84 100644 --- a/Refit-Tests/InterfaceStubGenerator.cs +++ b/Refit-Tests/InterfaceStubGenerator.cs @@ -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")); } @@ -67,6 +68,7 @@ public void HasRefitHttpMethodAttributeSmokeTest() Assert.IsTrue(result["AnotherRefitMethod"]); Assert.IsFalse(result["NoConstantsAllowed"]); Assert.IsFalse(result["NotARefitMethod"]); + Assert.IsTrue(result["ReadOne"]); } [Test] @@ -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] @@ -136,4 +138,22 @@ public interface IAmNotARefitInterface { Task NotARefitMethod(); } + + public interface IBoringCrudApi where T : class + { + [Post("")] + Task Create([Body] T paylod); + + [Get("")] + Task> ReadAll(); + + [Get("/{key}")] + Task ReadOne(TKey key); + + [Put("/{key}")] + Task Update(TKey key, [Body]T payload); + + [Delete("/{key}")] + Task Delete(TKey key); + } } \ No newline at end of file diff --git a/Refit-Tests/RefitStubs.cs b/Refit-Tests/RefitStubs.cs index 7f8d7da1f..27409b792 100644 --- a/Refit-Tests/RefitStubs.cs +++ b/Refit-Tests/RefitStubs.cs @@ -141,6 +141,56 @@ public virtual Task NoConstantsAllowed() } } +namespace Refit.Tests +{ + using RefitInternalGenerated; + + [Preserve] + public partial class AutoGeneratedIBoringCrudApi : IBoringCrudApi + where T : class + { + public HttpClient Client { get; protected set; } + readonly Dictionary> methodImpls; + + public AutoGeneratedIBoringCrudApi(HttpClient client, IRequestBuilder requestBuilder) + { + methodImpls = requestBuilder.InterfaceHttpMethods.ToDictionary(k => k, v => requestBuilder.BuildRestResultFuncForMethod(v)); + Client = client; + } + + public virtual Task Create(T paylod) + { + var arguments = new object[] { paylod }; + return (Task) methodImpls["Create"](Client, arguments); + } + + public virtual Task> ReadAll() + { + var arguments = new object[] { }; + return (Task>) methodImpls["ReadAll"](Client, arguments); + } + + public virtual Task ReadOne(TKey key) + { + var arguments = new object[] { key }; + return (Task) 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; @@ -246,4 +296,31 @@ public virtual Task Get() } } +namespace Refit.Tests +{ + using RefitInternalGenerated; + + [Preserve] + public partial class AutoGeneratedIHttpBinApi : IHttpBinApi + where TResponse : class + where THeader : struct + { + public HttpClient Client { get; protected set; } + readonly Dictionary> methodImpls; + + public AutoGeneratedIHttpBinApi(HttpClient client, IRequestBuilder requestBuilder) + { + methodImpls = requestBuilder.InterfaceHttpMethods.ToDictionary(k => k, v => requestBuilder.BuildRestResultFuncForMethod(v)); + Client = client; + } + + public virtual Task Get(TParam param,THeader header) + { + var arguments = new object[] { param,header }; + return (Task) methodImpls["Get"](Client, arguments); + } + + } +} + diff --git a/Refit-Tests/RestService.cs b/Refit-Tests/RestService.cs index 7c6e1e009..229c98d00 100644 --- a/Refit-Tests/RestService.cs +++ b/Refit-Tests/RestService.cs @@ -45,6 +45,22 @@ public interface IAmHalfRefit Task Get(); } + public interface IHttpBinApi + where TResponse : class + where THeader : struct + { + [Get("")] + Task Get(TParam param, [Header("X-Refit")] THeader header); + } + + public class HttpBinGet + { + public Dictionary Args { get; set; } + public Dictionary Headers { get; set; } + public string Origin { get; set; } + public string Url { get; set; } + } + [TestFixture] public class RestServiceIntegrationTests { @@ -221,5 +237,17 @@ public async Task NonRefitMethodsThrowMeaningfulExceptions() StringAssert.Contains("no Refit HTTP method attribute", exception.Message); } } + + [Test] + public async Task GenericsWork() + { + var fixture = RestService.For>("http://httpbin.org/get"); + + var result = await fixture.Get("foo", 99); + + Assert.AreEqual("http://httpbin.org/get?param=foo", result.Url); + Assert.AreEqual("foo", result.Args["param"]); + Assert.AreEqual("99", result.Headers["X-Refit"]); + } } } diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index a4699abb8..a0e7f39e2 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -352,8 +352,11 @@ public RestMethodInfo(Type targetInterface, MethodInfo methodInfo) } } - void verifyUrlPathIsSane(string relativePath) + void verifyUrlPathIsSane(string relativePath) { + if (relativePath == "") + return; + if (!relativePath.StartsWith("/")) { goto bogusPath; }