Skip to content
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
5 changes: 4 additions & 1 deletion src/Refitter.Core/IdentifierUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ public static string Sanitize(this string value)
{
const char dash = '-';

// @ can be used and still make valid methode names. but this should make most use cases safe
if (string.IsNullOrEmpty(value))
return value;

// @ can be used and still make valid method names. but this should make most use cases safe
if (
(value.First() < 'A' || value.First() > 'Z') &&
(value.First() < 'a' || value.First() > 'z') &&
Expand Down
2 changes: 1 addition & 1 deletion src/Refitter.Core/OperationNameGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public OperationNameGenerator(OpenApiDocument document, RefitGeneratorSettings s
}

[ExcludeFromCodeCoverage]
public bool SupportsMultipleClients => throw new System.NotImplementedException();
public bool SupportsMultipleClients => defaultGenerator.SupportsMultipleClients;

[ExcludeFromCodeCoverage]
public string GetClientName(OpenApiDocument document, string path, string httpMethod, OpenApiOperation operation)
Expand Down
30 changes: 21 additions & 9 deletions src/Refitter.Core/RefitGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,19 @@
var factory = new CSharpClientGeneratorFactory(settings, document);
var generator = factory.Create();
var docGenerator = new XmlDocumentationGenerator(settings);

// Create the interface generator before calling GenerateFile() so that
// OperationNameGenerator.CheckForDuplicateOperationIds() sees the original
// (pre-generation) operation IDs. GenerateFile() auto-populates operation IDs
// with globally unique names which would prevent the switch to the path segments
// generator, causing unnecessary numeric suffixes in ByTag mode.
IRefitInterfaceGenerator interfaceGenerator = settings.MultipleInterfaces switch
{
MultipleInterfaces.ByEndpoint => new RefitMultipleInterfaceGenerator(settings, document, generator, docGenerator),
MultipleInterfaces.ByTag => new RefitMultipleInterfaceByTagGenerator(settings, document, generator, docGenerator),
_ => new RefitInterfaceGenerator(settings, document, generator, docGenerator),
};

var contracts = generator.GenerateFile();
contracts = SanitizeGeneratedContracts(contracts);

Expand All @@ -126,13 +139,6 @@
(current, import) => current.Replace($"{import}.", string.Empty));
}

IRefitInterfaceGenerator interfaceGenerator = settings.MultipleInterfaces switch
{
MultipleInterfaces.ByEndpoint => new RefitMultipleInterfaceGenerator(settings, document, generator, docGenerator),
MultipleInterfaces.ByTag => new RefitMultipleInterfaceByTagGenerator(settings, document, generator, docGenerator),
_ => new RefitInterfaceGenerator(settings, document, generator, docGenerator),
};

var refitInterfaces = GenerateClient(interfaceGenerator);
var interfaceNames = refitInterfaces.Select(c => c.TypeName).ToArray();
var refitInterfacesCode = string.Join("", refitInterfaces.Select(c => c.Content));
Expand All @@ -152,7 +158,7 @@
}

/// <summary>
/// Generates multiple files containing Refit interfaces and contracts.

Check warning on line 161 in src/Refitter.Core/RefitGenerator.cs

View workflow job for this annotation

GitHub Actions / 👌 Verify build

Possible null reference argument for parameter 'suffix' in 'string ContractTypeSuffixApplier.ApplySuffix(string generatedCode, string suffix)'.

Check warning on line 161 in src/Refitter.Core/RefitGenerator.cs

View workflow job for this annotation

GitHub Actions / 👌 Verify build

Possible null reference argument for parameter 'suffix' in 'string ContractTypeSuffixApplier.ApplySuffix(string generatedCode, string suffix)'.

Check warning on line 161 in src/Refitter.Core/RefitGenerator.cs

View workflow job for this annotation

GitHub Actions / 👌 Verify build

Possible null reference argument for parameter 'suffix' in 'string ContractTypeSuffixApplier.ApplySuffix(string generatedCode, string suffix)'.

Check warning on line 161 in src/Refitter.Core/RefitGenerator.cs

View workflow job for this annotation

GitHub Actions / 👌 Verify build

Possible null reference argument for parameter 'suffix' in 'string ContractTypeSuffixApplier.ApplySuffix(string generatedCode, string suffix)'.

Check warning on line 161 in src/Refitter.Core/RefitGenerator.cs

View workflow job for this annotation

GitHub Actions / script

Possible null reference argument for parameter 'suffix' in 'string ContractTypeSuffixApplier.ApplySuffix(string generatedCode, string suffix)'.
/// </summary>
/// <returns>A GeneratorOutput containing all generated code files.</returns>
public GeneratorOutput GenerateMultipleFiles()
Expand All @@ -160,9 +166,12 @@
var factory = new CSharpClientGeneratorFactory(settings, document);
var generator = factory.Create();
var docGenerator = new XmlDocumentationGenerator(settings);
var contracts = generator.GenerateFile();
contracts = SanitizeGeneratedContracts(contracts);

// Create the interface generator before calling GenerateFile() so that
// OperationNameGenerator.CheckForDuplicateOperationIds() sees the original
// (pre-generation) operation IDs. GenerateFile() auto-populates operation IDs
// with globally unique names which would prevent the switch to the path segments
// generator, causing unnecessary numeric suffixes in ByTag mode.
IRefitInterfaceGenerator interfaceGenerator = settings.MultipleInterfaces switch
{
MultipleInterfaces.ByEndpoint
Expand All @@ -172,6 +181,9 @@
_ => new RefitInterfaceGenerator(settings, document, generator, docGenerator),
};

var contracts = generator.GenerateFile();
contracts = SanitizeGeneratedContracts(contracts);

var generatedFiles = new List<GeneratedCode>();

var refitInterfaces = GenerateClient(interfaceGenerator);
Expand Down Expand Up @@ -217,7 +229,7 @@

const string pattern = @"^\s*\[(System\.Text\.Json\.Serialization\.)?JsonConverter\(typeof\((System\.Text\.Json\.Serialization\.)?JsonStringEnumConverter\)\)\]\s*$";
var lines = contracts.Split(["\r\n", "\r", "\n"], StringSplitOptions.None);
var filteredLines = lines

Check warning on line 232 in src/Refitter.Core/RefitGenerator.cs

View workflow job for this annotation

GitHub Actions / 👌 Verify build

Possible null reference argument for parameter 'suffix' in 'string ContractTypeSuffixApplier.ApplySuffix(string generatedCode, string suffix)'.

Check warning on line 232 in src/Refitter.Core/RefitGenerator.cs

View workflow job for this annotation

GitHub Actions / 👌 Verify build

Possible null reference argument for parameter 'suffix' in 'string ContractTypeSuffixApplier.ApplySuffix(string generatedCode, string suffix)'.

Check warning on line 232 in src/Refitter.Core/RefitGenerator.cs

View workflow job for this annotation

GitHub Actions / 👌 Verify build

Possible null reference argument for parameter 'suffix' in 'string ContractTypeSuffixApplier.ApplySuffix(string generatedCode, string suffix)'.

Check warning on line 232 in src/Refitter.Core/RefitGenerator.cs

View workflow job for this annotation

GitHub Actions / 👌 Verify build

Possible null reference argument for parameter 'suffix' in 'string ContractTypeSuffixApplier.ApplySuffix(string generatedCode, string suffix)'.

Check warning on line 232 in src/Refitter.Core/RefitGenerator.cs

View workflow job for this annotation

GitHub Actions / script

Possible null reference argument for parameter 'suffix' in 'string ContractTypeSuffixApplier.ApplySuffix(string generatedCode, string suffix)'.
.Where(
line => !Regex.IsMatch(
line,
Expand Down
4 changes: 3 additions & 1 deletion src/Refitter.Core/RefitInterfaceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ private string GenerateInterfaceBody(out string? dynamicQuerystringParameters)
{
var code = new StringBuilder();
var dynamicQuerystringParametersCodeBuilder = new StringBuilder();
var knownIdentifiers = new HashSet<string>();
foreach (var kv in document.Paths)
{
foreach (var operations in kv.Value)
Expand All @@ -58,7 +59,8 @@ private string GenerateInterfaceBody(out string? dynamicQuerystringParameters)

var returnType = GetTypeName(operation);
var verb = operations.Key.CapitalizeFirstCharacter();
var operationName = GenerateOperationName(kv.Key, verb, operation);
var operationName = IdentifierUtils.Counted(knownIdentifiers, GenerateOperationName(kv.Key, verb, operation));
knownIdentifiers.Add(operationName);
var dynamicQuerystringParameterType = operationName + "QueryParams";

var operationModel = generator.CreateOperationModel(operation);
Expand Down
6 changes: 5 additions & 1 deletion src/Refitter.Core/RefitMultipleInterfaceByTagGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public override IEnumerable<GeneratedCode> GenerateCode()
var returnType = GetTypeName(operation);
var verb = operations.Key.CapitalizeFirstCharacter();

string interfaceName = null!;
string interfaceName;
if (!interfacesByGroup.TryGetValue(kv.Key, out var sb))
{
interfacesByGroup[kv.Key] = sb = new StringBuilder();
Expand All @@ -59,6 +59,10 @@ public override IEnumerable<GeneratedCode> GenerateCode()

interfacesNamesByGroup[kv.Key] = interfaceName;
}
else
{
interfaceName = interfacesNamesByGroup[kv.Key];
}

var operationName = GetOperationName(interfaceName, op.PathItem.Key, operations.Key, operation);
var dynamicQuerystringParameterType = operationName + "QueryParams";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using FluentAssertions;
using Refitter.Core;
using Refitter.Tests.Build;
using Refitter.Tests.TestUtilities;
using TUnit.Core;

namespace Refitter.Tests.Examples;

/// <summary>
/// Regression tests for https://github.com/christianhelle/refitter/issues/361
/// When using ByTag, method names should not have numeric suffixes added for
/// cross-interface uniqueness - deduplication should be per-interface.
/// </summary>
public class MultipleInterfacesByTagsWithSamePathSegmentTests
{
private const string OpenApiSpec = @"
openapi: '3.0.0'
info:
title: 'Test API'
version: '1.0'
paths:
/api/manage/2fa:
post:
tags:
- 'Manage'
responses:
'200':
description: 'ok'
/api/manage/info:
get:
tags:
- 'Manage'
responses:
'200':
description: 'ok'
post:
tags:
- 'Manage'
responses:
'200':
description: 'ok'
/api/account/info:
get:
tags:
- 'Account'
responses:
'200':
description: 'ok'
/api/user/info:
get:
tags:
- 'User'
responses:
'200':
description: 'ok'
";

[Test]
public async Task Can_Generate_Code()
{
string generatedCode = await GenerateCode();
generatedCode.Should().NotBeNullOrWhiteSpace();
}

[Test]
public async Task Generates_Manage_Interface()
{
string generatedCode = await GenerateCode();
generatedCode.Should().Contain("partial interface IManageApi");
}

[Test]
public async Task Generates_Account_Interface()
{
string generatedCode = await GenerateCode();
generatedCode.Should().Contain("partial interface IAccountApi");
}

[Test]
public async Task Generates_User_Interface()
{
string generatedCode = await GenerateCode();
generatedCode.Should().Contain("partial interface IUserApi");
}

[Test]
public async Task Account_Interface_Does_Not_Have_Numeric_Suffix()
{
string generatedCode = await GenerateCode();
// IAccountApi's GET /api/account/info should not get a numeric suffix
// even though IManageApi also has a GET /api/manage/info operation
generatedCode.Should().NotContainAny("Info2(", "Info3(", "InfoGet2(", "InfoGet3(", "InfoGET2(", "InfoGET3(");
}

[Test]
public async Task User_Interface_Does_Not_Have_Numeric_Suffix()
{
string generatedCode = await GenerateCode();
// IUserApi's GET /api/user/info should not get a numeric suffix
generatedCode.Should().NotContainAny("Info2(", "Info3(", "InfoGet2(", "InfoGet3(", "InfoGET2(", "InfoGET3(");
}

[Test]
public async Task Can_Build_Generated_Code()
{
string generatedCode = await GenerateCode();
BuildHelper
.BuildCSharp(generatedCode)
.Should()
.BeTrue();
}

private static async Task<string> GenerateCode()
{
var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(OpenApiSpec);
var settings = new RefitGeneratorSettings
{
OpenApiPath = swaggerFile,
MultipleInterfaces = MultipleInterfaces.ByTag,
};

var sut = await RefitGenerator.CreateAsync(settings);
return sut.Generate();
}
}
Loading