From 09bd47b8a1d9c0800fd73cfe090411a2a3cac30c Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Mon, 22 Jul 2013 19:16:22 -0400 Subject: [PATCH 1/6] Write a test to verify valid return types --- RequestBuilder.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/RequestBuilder.cs b/RequestBuilder.cs index 56226fd72..008ab0e4b 100644 --- a/RequestBuilder.cs +++ b/RequestBuilder.cs @@ -223,7 +223,10 @@ public interface IRestMethodInfoTests Task FetchSomeStuffWithAlias([AliasAs("id")] int anId); [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithBody([AliasAs("id")] int anId, [Body] Dictionary theData); + IObservable FetchSomeStuffWithBody([AliasAs("id")] int anId, [Body] Dictionary theData); + + [Post("/foo/{id}")] + string AsyncOnlyBuddy(int id); } [TestFixture] @@ -310,6 +313,21 @@ public void FindTheBodyParameter() Assert.AreEqual(0, fixture.QueryParameterMap.Count); Assert.AreEqual(1, fixture.BodyParameterInfo.Item2); } + + [Test] + public void SyncMethodsShouldThrow() + { + bool shouldDie = true; + + try { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "AsyncOnlyBuddy")); + } catch (ArgumentException) { + shouldDie = false; + } + + Assert.IsFalse(shouldDie); + } } public interface IDummyHttpApi From 074d986906664aa3b307c6ac710b767e81c765e2 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Mon, 22 Jul 2013 19:16:45 -0400 Subject: [PATCH 2/6] Add method return type validation in RestMethodInfo --- RequestBuilder.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/RequestBuilder.cs b/RequestBuilder.cs index 008ab0e4b..124e769ca 100644 --- a/RequestBuilder.cs +++ b/RequestBuilder.cs @@ -96,6 +96,8 @@ class RestMethodInfo public Dictionary ParameterMap { get; set; } public Tuple BodyParameterInfo { get; set; } public Dictionary QueryParameterMap { get; set; } + public Type ReturnType { get; set; } + public Type SerializedReturnType { get; set; } static readonly Regex parameterRegex = new Regex(@"^{(.*)}$"); @@ -113,6 +115,7 @@ public RestMethodInfo(Type targetInterface, MethodInfo methodInfo) RelativePath = hma.Path; verifyUrlPathIsSane(RelativePath); + determineReturnTypeInfo(methodInfo); var parameterList = methodInfo.GetParameters().ToList(); @@ -196,6 +199,26 @@ Tuple findBodyParameter(List parame return Tuple.Create(ret.BodyAttribute.SerializationMethod, parameterList.IndexOf(ret.Parameter)); } + + void determineReturnTypeInfo(MethodInfo methodInfo) + { + if (methodInfo.ReturnType.IsGenericType == false && methodInfo.ReturnType != typeof(Task)) { + goto bogusMethod; + } + + var genericType = methodInfo.ReturnType.GetGenericTypeDefinition(); + if (genericType != typeof(Task<>) && genericType != typeof(IObservable<>)) { + goto bogusMethod; + } + + ReturnType = methodInfo.ReturnType; + SerializedReturnType = methodInfo.ReturnType.GetGenericArguments()[0]; + if (SerializedReturnType == typeof(HttpResponseMessage)) SerializedReturnType = null; + return; + + bogusMethod: + throw new ArgumentException("All REST Methods must return either Task or IObservable"); + } } /* From 7221f1d5737a8be97d7b91c0cc48559a2158f6e1 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Mon, 22 Jul 2013 19:24:36 -0400 Subject: [PATCH 3/6] Implement BuildRestResultFuncForMethod for Task --- RequestBuilder.cs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/RequestBuilder.cs b/RequestBuilder.cs index 124e769ca..cdc757ba4 100644 --- a/RequestBuilder.cs +++ b/RequestBuilder.cs @@ -84,6 +84,40 @@ public Func BuildRequestFactoryForMethod(string me return ret; }; } + + public Func BuildRestResultFuncForMethod(string methodName) + { + if (!interfaceHttpMethods.ContainsKey(methodName)) { + throw new ArgumentException("Method must be defined and have an HTTP Method attribute"); + } + + var restMethod = interfaceHttpMethods[methodName]; + if (restMethod.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) { + return buildTaskFuncForMethod(restMethod); + } else { + return buildRxFuncForMethod(restMethod); + } + } + + Func> buildTaskFuncForMethod(RestMethodInfo restMethod) + { + var factory = BuildRequestFactoryForMethod(restMethod.Name); + + return async (client, paramList) => { + var rq = factory(paramList); + var resp = await client.SendAsync(rq); + if (restMethod.SerializedReturnType == null) { + return resp; + } + + var content = await resp.Content.ReadAsStringAsync(); + if (restMethod.SerializedReturnType == typeof(string)) { + return content; + } + + return JsonConvert.DeserializeObject(content, restMethod.SerializedReturnType); + }; + } } class RestMethodInfo From 1a66f3a73a590b6388145356f44fd6bf5ec22601 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Mon, 22 Jul 2013 19:50:37 -0400 Subject: [PATCH 4/6] Handle Rx --- RequestBuilder.cs | 109 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/RequestBuilder.cs b/RequestBuilder.cs index cdc757ba4..af45b0247 100644 --- a/RequestBuilder.cs +++ b/RequestBuilder.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json; using System.IO; using System.Web; +using System.Threading; namespace Refit { @@ -118,6 +119,114 @@ Func> buildTaskFuncForMethod(RestMethodInfo r return JsonConvert.DeserializeObject(content, restMethod.SerializedReturnType); }; } + + Func> buildRxFuncForMethod(RestMethodInfo restMethod) + { + var taskFunc = buildTaskFuncForMethod(restMethod); + + return (client, paramList) => { + var ret = new FakeAsyncSubject(); + + taskFunc(client, paramList).ContinueWith(t => { + if (t.Exception != null) { + ret.OnError(t.Exception); + } else { + ret.OnNext(t.Result); + ret.OnCompleted(); + } + }); + + return ret; + }; + } + + class CompletionResult + { + public bool IsCompleted { get; set; } + public Exception Error { get; set; } + } + + class FakeAsyncSubject : IObservable, IObserver + { + bool resultSet; + T result; + CompletionResult completion; + List> subscriberList = new List>(); + + public void OnNext(T value) + { + if (completion == null) return; + + result = value; + resultSet = true; + + var currentList = default(IObserver[]); + lock (subscriberList) { currentList = subscriberList.ToArray(); } + foreach (var v in currentList) v.OnNext(value); + } + + public void OnError(Exception error) + { + var final = Interlocked.CompareExchange(ref completion, new CompletionResult() { IsCompleted = false, Error = error }, null); + if (final.IsCompleted) return; + + var currentList = default(IObserver[]); + lock (subscriberList) { currentList = subscriberList.ToArray(); } + foreach (var v in currentList) v.OnError(error); + + final.IsCompleted = true; + } + + public void OnCompleted() + { + var final = Interlocked.CompareExchange(ref completion, new CompletionResult() { IsCompleted = false, Error = null }, null); + if (final.IsCompleted) return; + + var currentList = default(IObserver[]); + lock (subscriberList) { currentList = subscriberList.ToArray(); } + foreach (var v in currentList) v.OnCompleted(); + + final.IsCompleted = true; + } + + public IDisposable Subscribe(IObserver observer) + { + if (completion != null) { + if (completion.Error != null) { + observer.OnError(completion.Error); + return new AnonymousDisposable(() => {}); + } + + if (resultSet) observer.OnNext(result); + observer.OnCompleted(); + + return new AnonymousDisposable(() => {}); + } + + lock (subscriberList) { + subscriberList.Add(observer); + } + + return new AnonymousDisposable(() => { + lock (subscriberList) { subscriberList.Remove(observer); } + }); + } + } + } + + sealed class AnonymousDisposable : IDisposable + { + readonly Action block; + + public AnonymousDisposable(Action block) + { + this.block = block; + } + + public void Dispose() + { + block(); + } } class RestMethodInfo From 6b9e551e6f07203818cb64600fbb4d3ade70a34b Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Mon, 22 Jul 2013 19:52:51 -0400 Subject: [PATCH 5/6] Throw on failed HTTP requests --- RequestBuilder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RequestBuilder.cs b/RequestBuilder.cs index af45b0247..f3de1cfad 100644 --- a/RequestBuilder.cs +++ b/RequestBuilder.cs @@ -111,6 +111,8 @@ Func> buildTaskFuncForMethod(RestMethodInfo r return resp; } + resp.EnsureSuccessStatusCode(); + var content = await resp.Content.ReadAsStringAsync(); if (restMethod.SerializedReturnType == typeof(string)) { return content; From b913d01f55c55c1b8c2d9a3837b034e135b8f995 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Mon, 22 Jul 2013 19:53:03 -0400 Subject: [PATCH 6/6] Handle requests that only care about completion --- RequestBuilder.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/RequestBuilder.cs b/RequestBuilder.cs index f3de1cfad..253493383 100644 --- a/RequestBuilder.cs +++ b/RequestBuilder.cs @@ -93,13 +93,28 @@ public Func BuildRestResultFuncForMethod(string me } var restMethod = interfaceHttpMethods[methodName]; - if (restMethod.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) { + + if (restMethod.ReturnType == typeof(Task)) { + return buildVoidTaskFuncForMethod(restMethod); + } else if (restMethod.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) { return buildTaskFuncForMethod(restMethod); } else { return buildRxFuncForMethod(restMethod); } } + Func buildVoidTaskFuncForMethod(RestMethodInfo restMethod) + { + var factory = BuildRequestFactoryForMethod(restMethod.Name); + + return async (client, paramList) => { + var rq = factory(paramList); + var resp = await client.SendAsync(rq); + + resp.EnsureSuccessStatusCode(); + }; + } + Func> buildTaskFuncForMethod(RestMethodInfo restMethod) { var factory = BuildRequestFactoryForMethod(restMethod.Name);