Skip to content

Commit

Permalink
Merge pull request #633 from Sergio0694/master
Browse files Browse the repository at this point in the history
Explicit interface inheritance
  • Loading branch information
Oren Novotny authored Mar 12, 2019
2 parents ec3d6c3 + 199dedf commit a124fb5
Show file tree
Hide file tree
Showing 11 changed files with 586 additions and 198 deletions.
8 changes: 8 additions & 0 deletions .whitesource
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
##########################################################
#### WhiteSource "Bolt for Github" configuration file ####
##########################################################

# Configuration #
#---------------#
ws.repo.scan=true
vulnerable.check.run.conclusion.level=success
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,7 @@ namespace {{Namespace}}
{{#MethodList}}

/// <inheritdoc />
public virtual {{ReturnType}} {{Name}}{{#MethodTypeParameters}}<{{.}}>
{{/MethodTypeParameters}}({{ArgumentListWithTypes}})
{{#MethodConstraintClauses}}
{{.}}
{{/MethodConstraintClauses}}
{{ReturnType}} {{InterfaceName}}{{#TypeParameters}}<{{.}}>{{/TypeParameters}}.{{Name}}{{#MethodTypeParameters}}<{{.}}>{{/MethodTypeParameters}}({{ArgumentListWithTypes}})
{
{{#IsRefitMethod}}
var arguments = new object[] { {{ArgumentList}} };
Expand Down
20 changes: 17 additions & 3 deletions InterfaceStubGenerator.Core/InterfaceStubGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -92,6 +91,7 @@ public ClassTemplateInfo GenerateClassInfoForInterface(InterfaceDeclarationSynta
ret.InterfaceName = GetInterfaceName(interfaceTree.Identifier);
ret.GeneratedClassSuffix = ret.InterfaceName.Replace(".", "");
ret.Modifiers = interfaceTree.Modifiers.Select(t => t.ValueText).FirstOrDefault(m => m == "public" || m == "internal");
ret.BaseClassNames = interfaceTree.BaseList?.Types.Select(t => t.ToString()).ToList();

var ns = parent as NamespaceDeclarationSyntax;
ret.Namespace = ns?.Name?.ToString() ?? $"AutoGenerated{ret.GeneratedClassSuffix}";
Expand All @@ -114,6 +114,8 @@ public ClassTemplateInfo GenerateClassInfoForInterface(InterfaceDeclarationSynta
var mti = new MethodTemplateInfo
{
Name = x.Identifier.Text,
InterfaceName = ret.InterfaceName,
TypeParameters = ret.TypeParameters,
ReturnType = x.ReturnType.ToString(),
ArgumentList = string.Join(",",
x.ParameterList.Parameters
Expand All @@ -134,7 +136,6 @@ public ClassTemplateInfo GenerateClassInfoForInterface(InterfaceDeclarationSynta
mti.MethodTypeParameterList = string.Join(", ", typeParameters.Select(p => $"typeof({p.Identifier.ValueText})"));
mti.MethodTypeParameterNames = $"{string.Join(", ", typeParameters.Select(p => $"{{typeof({p.Identifier.ValueText}).AssemblyQualifiedName}}"))}";
}
mti.MethodConstraintClauses = x.ConstraintClauses.ToFullString().Trim();
}
return mti;
})
Expand Down Expand Up @@ -181,9 +182,20 @@ public TemplateInformation GenerateTemplateInfoForInterfaceList(List<InterfaceDe
UsingList = usings.ToList()
};

AddInheritedMethods(ret);

return ret;
}

private static void AddInheritedMethods(TemplateInformation ret)
{
foreach (var c in ret.ClassList.Where(c => c.BaseClassNames != null && c.BaseClassNames.Any()))
{
var methodsToAdd = ret.ClassList.Where(oc => c.BaseClassNames.Contains(oc.InterfaceName)).SelectMany(oc => oc.MethodList);
c.MethodList.AddRange(methodsToAdd);
}
}

public void GenerateWarnings(List<InterfaceDeclarationSyntax> interfacesToGenerate)
{
var missingAttributeWarnings = interfacesToGenerate
Expand Down Expand Up @@ -240,6 +252,7 @@ public class ClassTemplateInfo
public string ConstraintClauses { get; set; }
public string GeneratedClassSuffix { get; set; }
public string InterfaceName { get; set; }
public List<string> BaseClassNames { get; set; }
public List<MethodTemplateInfo> MethodList { get; set; }
public string Modifiers { get; set; }
public string Namespace { get; set; }
Expand All @@ -255,9 +268,10 @@ public class MethodTemplateInfo
public string Name { get; set; }
public string ReturnType { get; set; }
public string MethodTypeParameters { get; set; }
public string MethodConstraintClauses { get; set; }
public string MethodTypeParameterList { get; set; }
public string MethodTypeParameterNames { get; set; }
public string InterfaceName { get; set; }
public string TypeParameters { get; set; }
}

public class TemplateInformation
Expand Down
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,69 @@ Which can be used like this:
// than one type (unless you have a different domain for each type)
var api = RestService.For<IReallyExcitingCrudApi<User, string>>("http://api.example.com/users");
```
### Interface inheritance

When multiple services that need to be kept separate share a number of APIs, it is possible to leverage interface inheritance to avoid having to define the same Refit methods multiple times in different services:

```csharp
public interface IBaseService
{
[Get("/resources")]
Task<Resource> GetResource(string id);
}

public interface IDerivedServiceA : IBaseService
{
[Delete("/resources")]
Task DeleteResource(string id);
}

public interface IDerivedServiceB : IBaseService
{
[Post("/resources")]
Task<string> AddResource([Body] Resource resource);
}
```

In this example, the `IDerivedServiceA` interface will expose both the `GetResource` and `DeleteResource` APIs, while `IDerivedServiceB` will expose `GetResource` and `AddResource`.

#### Headers inheritance

When using inheritance, existing header attributes will passed along as well, and the inner-most ones will have precedence:

```csharp
[Headers("User-Agent: AAA")]
public interface IAmInterfaceA
{
[Get("/get?result=Ping")]
Task<string> Ping();
}

[Headers("User-Agent: BBB")]
public interface IAmInterfaceB : IAmInterfaceA
{
[Get("/get?result=Pang")]
[Headers("User-Agent: PANG")]
Task<string> Pang();

[Get("/get?result=Foo")]
Task<string> Foo();
}
```

Here, `IAmInterfaceB.Pang()` will use `PANG` as its user agent, while `IAmInterfaceB.Foo` and `IAmInterfaceB.Ping` will use `BBB`.
Note that if `IAmInterfaceB` didn't have a header attribute, `Foo` would then use the `AAA` value inherited from `IAmInterfaceA`.
If an interface is inheriting more than one interface, the order of precedence is the same as the one in which the inherited interfaces are declared:

```csharp
public interface IAmInterfaceC : IAmInterfaceA, IAmInterfaceB
{
[Get("/get?result=Foo")]
Task<string> Foo();
}
```

Here `IAmInterfaceC.Foo` would use the header attribute inherited from `IAmInterfaceA`, if present, or the one inherited from `IAmInterfaceB`, and so on for all the declared interfaces.

### Using HttpClientFactory

Expand Down
27 changes: 27 additions & 0 deletions Refit.Tests/InheritedInterfacesApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Threading.Tasks;
using Refit; // InterfaceStubGenerator looks for this
using static System.Math; // This is here to verify https://github.com/paulcbetts/refit/issues/283

namespace Refit.Tests
{
[Headers("User-Agent: Refit Integration Tests")]
public interface IAmInterfaceA
{
[Get("/get?result=Ping")]
Task<string> Ping();
}

[Headers("User-Agent: Refit Integration Tests")]
public interface IAmInterfaceB
{
[Get("/get?result=Pong")]
Task<string> Pong();
}

[Headers("User-Agent: Refit Integration Tests")]
public interface IAmInterfaceC : IAmInterfaceB, IAmInterfaceA
{
[Get("/get?result=Pang")]
Task<string> Pang();
}
}
28 changes: 28 additions & 0 deletions Refit.Tests/InterfaceStubGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ public void GenerateInterfaceStubsSmokeTest()
var result = fixture.GenerateInterfaceStubs(new[] {
IntegrationTestHelper.GetPath("RestService.cs"),
IntegrationTestHelper.GetPath("GitHubApi.cs"),
IntegrationTestHelper.GetPath("InheritedInterfacesApi.cs"),
});

Assert.Contains("IGitHubApi", result);
Assert.Contains("IAmInterfaceC", result);
}

[Fact]
Expand Down Expand Up @@ -126,6 +128,32 @@ public void GenerateTemplateInfoForInterfaceListSmokeTest()
Assert.Equal(12, result.ClassList.Count);
}

[Fact]
public void GenerateTemplateInfoForInheritedInterfacesListSmokeTest()
{
var file = CSharpSyntaxTree.ParseText(File.ReadAllText(IntegrationTestHelper.GetPath("InheritedInterfacesApi.cs")));
var fixture = new InterfaceStubGenerator();

var input = file.GetRoot().DescendantNodes()
.OfType<InterfaceDeclarationSyntax>()
.ToList();

var result = fixture.GenerateTemplateInfoForInterfaceList(input);
Assert.Equal(3, result.ClassList.Count);

var inherited = result.ClassList.First(c => c.InterfaceName == "IAmInterfaceC");

Assert.Equal(3, inherited.MethodList.Count);
var methodNames = inherited.MethodList.Select(m => m.Name).ToList();

Assert.Contains("Ping", methodNames);
Assert.Contains("Pong", methodNames);
Assert.Contains("Pang", methodNames);

Assert.Equal("IAmInterfaceC", inherited.InterfaceName);
Assert.Equal("IAmInterfaceC", inherited.GeneratedClassSuffix);
}

[Fact]
public void RetainsAliasesInUsings()
{
Expand Down
Loading

0 comments on commit a124fb5

Please sign in to comment.