Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stub anything that looks like a Refit interface #67

Merged
merged 5 commits into from
Nov 3, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,13 @@ namespace {{Namespace}}
{{#MethodList}}
public virtual {{ReturnType}} {{Name}}({{ArgumentListWithTypes}})
{
{{#IsRefitMethod}}
var arguments = new object[] { {{ArgumentList}} };
return ({{ReturnType}}) methodImpls["{{Name}}"](Client, arguments);
{{/IsRefitMethod}}
{{^IsRefitMethod}}
throw new NotImplementedException("Either this method has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument.");
{{/IsRefitMethod}}
}

{{/MethodList}}
Expand Down
49 changes: 32 additions & 17 deletions InterfaceStubGenerator/InterfaceStubGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

namespace Refit.Generator
{
// * Find all calls to RestService.For<T>, extract all T's
// * Search for all Interfaces, find the method definitions
// * Search for all Interfaces, find the method definitions
// and make sure there's at least one Refit attribute on one
// * Generate the data we need for the template based on interface method
// defn's
// * Get this into an EXE in tools, write a targets file to beforeBuild execute it
Expand All @@ -30,11 +30,8 @@ public class InterfaceStubGenerator
public string GenerateInterfaceStubs(string[] paths)
{
var trees = paths.Select(x => CSharpSyntaxTree.ParseFile(x)).ToList();
var interfaceNamesToFind = trees.SelectMany(FindInterfacesToGenerate).Distinct().ToList();

var interfacesToGenerate = trees.SelectMany(x => x.GetRoot().DescendantNodes().OfType<InterfaceDeclarationSyntax>())
.Where(x => interfaceNamesToFind.Contains(x.Identifier.ValueText))
.ToList();
var interfacesToGenerate = trees.SelectMany(FindInterfacesToGenerate).ToList();

var templateInfo = GenerateTemplateInfoForInterfaceList(interfacesToGenerate);

Expand All @@ -43,21 +40,37 @@ public string GenerateInterfaceStubs(string[] paths)
return text;
}

public List<string> FindInterfacesToGenerate(SyntaxTree tree)
public List<InterfaceDeclarationSyntax> FindInterfacesToGenerate(SyntaxTree tree)
{
var restServiceCalls = tree.GetRoot().DescendantNodes()
.OfType<MemberAccessExpressionSyntax>()
.Where(x => x.Expression is IdentifierNameSyntax &&
((IdentifierNameSyntax)x.Expression).Identifier.ValueText == "RestService" &&
x.Name.Identifier.ValueText == "For");

return restServiceCalls
.SelectMany(x => ((GenericNameSyntax)x.Name).TypeArgumentList.Arguments)
.Select(x => ((IdentifierNameSyntax)x).Identifier.ValueText)
.Distinct()
var nodes = tree.GetRoot().DescendantNodes().ToList();

// Make sure this file imports Refit. If not, we're not going to
// find any Refit interfaces
// NB: This falls down in the tests unless we add an explicit "using Refit;",
// but we can rely on this being there in any other file
if (nodes.OfType<UsingDirectiveSyntax>().All(u => u.Name.ToFullString() != "Refit"))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this check here instead of in HasRefitHttpMethodAttribute means that the method could return true for something that's doesn't actually have a Refit attribute (i.e. one with a different "Get" or "Post" attribute). I'm pretty sure it doesn't matter as long as we have the check here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could roll down the "using" lists to make sure they reference Refit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean? I think that's what I'm doing here - my point was just that I'm doing it here and not in the method that looks for Refit attributes.

I could take the rolled up list all usings here and pass it in as a parameter, then it's not going to be traversing the whole tree each time the method is called. I'm conscious of the fact that this runs as part of the build so we need to make it as fast as possible. Maybe it's still micro-optimising though?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm dumb, I just read the comment and not the code, disregard :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

return new List<InterfaceDeclarationSyntax>();

return nodes.OfType<InterfaceDeclarationSyntax>()
.Where(i => i.Members.OfType<MethodDeclarationSyntax>().Any(HasRefitHttpMethodAttribute))
.ToList();
}

static readonly HashSet<string> httpMethodAttributeNames = new HashSet<string>(
new[] {"Get", "Head", "Post", "Put", "Delete"}
.SelectMany(x => new[] {"{0}", "{0}Attribute"}.Select(f => string.Format(f, x))));

public bool HasRefitHttpMethodAttribute(MethodDeclarationSyntax method)
{
// We could also verify that the single argument is a string,
// but what if somebody is dumb and uses a constant?
// Could be turtles all the way down.
return method.AttributeLists.SelectMany(a => a.Attributes)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was tempted to put also check that a.ArgumentList.Arguments[0].Expression is a string literal, but that would stop someone from using a constant. I think it makes the interface less readable if the expression has a constant so maybe we just make the call that constants are dumb and don't allow them.

If we want to check for a constant, do we also need to allow for constants that reference other constants? It could get messy.

(I've referenced the wrong line here - this should be on after line 70.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore this - I have just updated it to not allow constants.

.Any(a => httpMethodAttributeNames.Contains(a.Name.ToFullString().Split('.').Last()) &&
a.ArgumentList.Arguments.Count == 1 &&
a.ArgumentList.Arguments[0].Expression.CSharpKind() == SyntaxKind.StringLiteralExpression);
}

public TemplateInformation GenerateTemplateInfoForInterfaceList(List<InterfaceDeclarationSyntax> interfaceList)
{
var usings = interfaceList
Expand Down Expand Up @@ -100,6 +113,7 @@ public ClassTemplateInfo GenerateClassInfoForInterface(InterfaceDeclarationSynta
.Select(y => y.Identifier.ValueText)),
ArgumentListWithTypes = String.Join(",", x.ParameterList.Parameters
.Select(y => String.Format("{0} {1}", y.Type.ToString(), y.Identifier.ValueText))),
IsRefitMethod = HasRefitHttpMethodAttribute(x)
})
.ToList();

Expand Down Expand Up @@ -142,6 +156,7 @@ public class MethodTemplateInfo
public string Name { get; set; }
public string ArgumentListWithTypes { get; set; }
public string ArgumentList { get; set; }
public bool IsRefitMethod { get; set; }
}

public class TemplateInformation
Expand Down
1 change: 1 addition & 0 deletions Refit-Tests/GitHubApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Refit; // InterfaceStubGenerator looks for this

namespace Refit.Tests
{
Expand Down
58 changes: 54 additions & 4 deletions Refit-Tests/InterfaceStubGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using NUnit.Framework;
using Nustache;
using Nustache.Core;
using Refit; // InterfaceStubGenerator looks for this
using Refit.Generator;

namespace Refit.Tests
Expand All @@ -33,12 +34,39 @@ public void GenerateInterfaceStubsSmokeTest()
[Test]
public void FindInterfacesSmokeTest()
{
var input = IntegrationTestHelper.GetPath("RestService.cs");
var input = IntegrationTestHelper.GetPath("GitHubApi.cs");
var fixture = new InterfaceStubGenerator();

var result = fixture.FindInterfacesToGenerate(CSharpSyntaxTree.ParseFile(input));
Assert.AreEqual(3, result.Count);
Assert.True(result.Any(x => x == "IGitHubApi"));
Assert.AreEqual(1, result.Count);
Assert.True(result.Any(x => x.Identifier.ValueText == "IGitHubApi"));

input = IntegrationTestHelper.GetPath("InterfaceStubGenerator.cs");

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

[Test]
public void HasRefitHttpMethodAttributeSmokeTest()
{
var file = CSharpSyntaxTree.ParseFile(IntegrationTestHelper.GetPath("InterfaceStubGenerator.cs"));
var fixture = new InterfaceStubGenerator();

var input = file.GetRoot().DescendantNodes()
.OfType<InterfaceDeclarationSyntax>()
.SelectMany(i => i.Members.OfType<MethodDeclarationSyntax>())
.ToList();

var result = input
.ToDictionary(m => m.Identifier.ValueText, fixture.HasRefitHttpMethodAttribute);

Assert.IsTrue(result["RefitMethod"]);
Assert.IsTrue(result["AnotherRefitMethod"]);
Assert.IsFalse(result["NoConstantsAllowed"]);
Assert.IsFalse(result["NotARefitMethod"]);
}

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

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

[Test]
Expand All @@ -86,4 +114,26 @@ public void RetainsAliasesInUsings()
CollectionAssert.Contains(usingList, "CollisionB");
}
}

public static class ThisIsDumbButMightHappen
{
public const string PeopleDoWeirdStuff = "But we don't let them";
}

public interface IAmARefitInterfaceButNobodyUsesMe
{
[Get("whatever")]
Task RefitMethod();

[Refit.GetAttribute("something-else")]
Task AnotherRefitMethod();

[Get(ThisIsDumbButMightHappen.PeopleDoWeirdStuff)]
Task NoConstantsAllowed();
}

public interface IAmNotARefitInterface
{
Task NotARefitMethod();
}
}
1 change: 1 addition & 0 deletions Refit-Tests/NamespaceCollisionApi.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using SomeType = CollisionA.SomeType;
using CollisionB;
using Refit; // InterfaceStubGenerator looks for this

namespace Refit.Tests
{
Expand Down
75 changes: 74 additions & 1 deletion Refit-Tests/RefitStubs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Refit;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NUnit.Framework;
using Nustache;
using Nustache.Core;
using Refit.Generator;
using SomeType = CollisionA.SomeType;
using CollisionB;
using System.Net;
using System.Reactive.Linq;
using NUnit.Framework;
using Newtonsoft.Json;

/* ******** Hey You! *********
Expand Down Expand Up @@ -92,6 +99,42 @@ public virtual Task NothingToSeeHere()
}
}

namespace Refit.Tests
{
using RefitInternalGenerated;

[Preserve]
public partial class AutoGeneratedIAmARefitInterfaceButNobodyUsesMe : IAmARefitInterfaceButNobodyUsesMe
{
public HttpClient Client { get; protected set; }
readonly Dictionary<string, Func<HttpClient, object[], object>> methodImpls;

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

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

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

public virtual Task NoConstantsAllowed()
{
throw new NotImplementedException("Either this method has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument.");
}

}
}

namespace Refit.Tests
{
using RefitInternalGenerated;
Expand Down Expand Up @@ -167,4 +210,34 @@ public virtual Task Post()
}
}

namespace Refit.Tests
{
using RefitInternalGenerated;

[Preserve]
public partial class AutoGeneratedIAmHalfRefit : IAmHalfRefit
{
public HttpClient Client { get; protected set; }
readonly Dictionary<string, Func<HttpClient, object[], object>> methodImpls;

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

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

public virtual Task Get()
{
throw new NotImplementedException("Either this method has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument.");
}

}
}


35 changes: 35 additions & 0 deletions Refit-Tests/RestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

using NUnit.Framework;
using Newtonsoft.Json;
using Refit; // InterfaceStubGenerator looks for this

namespace Refit.Tests
{
Expand All @@ -31,6 +32,19 @@ public interface IRequestBin
Task Post();
}

public interface INoRefitHereBuddy
{
Task Post();
}

public interface IAmHalfRefit
{
[Post("/anything")]
Task Post();

Task Get();
}

[TestFixture]
public class RestServiceIntegrationTests
{
Expand Down Expand Up @@ -173,5 +187,26 @@ public async Task CanGetDataOutOfErrorResponses()
Assert.IsNotNull(content["documentation_url"]);
}
}

[Test]
public void NonRefitInterfacesThrowMeaningfulExceptions()
{
try {
RestService.For<INoRefitHereBuddy>("http://example.com");
} catch (InvalidOperationException exception) {
StringAssert.StartsWith("INoRefitHereBuddy", exception.Message);
}
}

[Test]
public async Task NonRefitMethodsThrowMeaningfulExceptions()
{
try {
var fixture = RestService.For<IAmHalfRefit>("http://example.com");
await fixture.Get();
} catch (NotImplementedException exception) {
StringAssert.Contains("no Refit HTTP method attribute", exception.Message);
}
}
}
}
3 changes: 3 additions & 0 deletions Refit/RestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public static T For<T>(HttpClient client)
var typeName = typeof(T).AssemblyQualifiedName.Replace(typeof(T).Name, className);
var generatedType = Type.GetType(typeName);

if(generatedType == null)
throw new InvalidOperationException(typeof(T).Name + " doesn't look like a Refit interface. Make sure it has at least one method with a Refit HTTP method attribute and Refit is installed in the project.");

return (T)Activator.CreateInstance(generatedType, client, requestBuilder);
}

Expand Down