Skip to content
Closed
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
41 changes: 41 additions & 0 deletions src/Refitter.Core/EnumStringConverterInjector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Text.RegularExpressions;
using NSwag;

namespace Refitter.Core;

internal sealed class EnumStringConverterInjector : IContractsPostProcessor
{
private static readonly Regex JsonStringEnumConverterAttributeRegex = new(
@"^\s*\[(System\.Text\.Json\.Serialization\.)?JsonConverter\(typeof\((System\.Text\.Json\.Serialization\.)?JsonStringEnumConverter(?:<[\w.]+>)?\)\)\]\s*\r?\n?",
RegexOptions.Compiled | RegexOptions.Multiline,
TimeSpan.FromSeconds(1));

private static readonly Regex EnumDeclarationRegex = new(
@"^(\s*)((?:public|internal)\s+(?:partial\s+)?enum\s+\w+\b)",
RegexOptions.Compiled | RegexOptions.Multiline,
TimeSpan.FromSeconds(1));

public string Process(OpenApiDocument document, RefitGeneratorSettings settings, string contracts)
{
if (settings.CodeGeneratorSettings is not { InlineJsonConverters: false })
{
contracts = JsonStringEnumConverterAttributeRegex.Replace(contracts, string.Empty);
var newLine = GetPreferredNewLine(contracts);
return EnumDeclarationRegex
.Replace(
contracts,
match =>
$"{match.Groups[1].Value}[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))]{newLine}{match.Groups[1].Value}{match.Groups[2].Value}")
.TrimEnd();
}

return JsonStringEnumConverterAttributeRegex
.Replace(contracts, string.Empty)
.TrimEnd();
}

private static string GetPreferredNewLine(string content) =>
content.Contains("\r\n", StringComparison.Ordinal)
? "\r\n"
: "\n";
}
107 changes: 14 additions & 93 deletions src/Refitter.Core/GeneratorPipeline.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,31 @@
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NSwag;

namespace Refitter.Core;

internal sealed class GeneratorPipeline
{
private static readonly Regex JsonStringEnumConverterAttributeRegex = new(
@"^\s*\[(System\.Text\.Json\.Serialization\.)?JsonConverter\(typeof\((System\.Text\.Json\.Serialization\.)?JsonStringEnumConverter(?:<[\w.]+>)?\)\)\]\s*\r?\n?",
RegexOptions.Compiled | RegexOptions.Multiline,
TimeSpan.FromSeconds(1));
private readonly InterfaceGenerator interfaceGenerator;
private readonly IReadOnlyList<IContractsPostProcessor> contractsPostProcessors;

private static readonly Regex EnumDeclarationRegex = new(
@"^(\s*)((?:public|internal)\s+(?:partial\s+)?enum\s+\w+\b)",
RegexOptions.Compiled | RegexOptions.Multiline,
TimeSpan.FromSeconds(1));
internal GeneratorPipeline(
XmlDocumentationGenerator docGenerator,
InterfaceGenerator interfaceGenerator,
IEnumerable<IContractsPostProcessor> contractsPostProcessors)
{
this.interfaceGenerator = interfaceGenerator;
this.contractsPostProcessors = contractsPostProcessors.ToArray();
}

public GenerationResult Run(
OpenApiDocument document,
RefitGeneratorSettings settings,
CustomCSharpClientGenerator generator)
{
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.
var interfaceGenerator = new InterfaceGenerator(settings, document, generator, docGenerator);

var contracts = generator.GenerateFile();
contracts = SanitizeGeneratedContracts(document, settings, contracts);
foreach (var postProcessor in contractsPostProcessors)
contracts = postProcessor.Process(document, settings, contracts);

var serializerContext = GenerateJsonSerializerContext(document, settings, contracts);
var interfaces = GenerateClient(document, settings, interfaceGenerator);
var interfaceNames = interfaces.Select(c => c.TypeName).ToArray();
Expand All @@ -58,51 +49,7 @@ private static IInterfacePartitioning GetInterfacePartitioning(OpenApiDocument d
};
}

internal static string SanitizeGeneratedContracts(OpenApiDocument document, RefitGeneratorSettings settings, string contracts)
{
contracts = NormalizeSwagger2OptionalReferencePropertyNullability(document, settings, contracts);

if (settings.CodeGeneratorSettings is not { InlineJsonConverters: false })
{
contracts = JsonStringEnumConverterAttributeRegex.Replace(contracts, string.Empty);
var newLine = GetPreferredNewLine(contracts);
return EnumDeclarationRegex
.Replace(
contracts,
match =>
$"{match.Groups[1].Value}[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))]{newLine}{match.Groups[1].Value}{match.Groups[2].Value}")
.TrimEnd();
}

return JsonStringEnumConverterAttributeRegex
.Replace(contracts, string.Empty)
.TrimEnd();
}

private static string GetPreferredNewLine(string content) =>
content.Contains("\r\n", StringComparison.Ordinal)
? "\r\n"
: "\n";

internal static string NormalizeSwagger2OptionalReferencePropertyNullability(
OpenApiDocument document,
RefitGeneratorSettings settings,
string contracts)
{
if (document.SchemaType != NJsonSchema.SchemaType.Swagger2 ||
settings.CodeGeneratorSettings?.GenerateNullableReferenceTypes != true ||
settings.CodeGeneratorSettings.GenerateOptionalPropertiesAsNullable)
{
return contracts;
}

var tree = CSharpSyntaxTree.ParseText(contracts);
var root = tree.GetCompilationUnitRoot();
var rewrittenRoot = new Swagger2OptionalReferencePropertyNullabilityRewriter().Visit(root);
return rewrittenRoot!.ToFullString();
}

internal static string GenerateJsonSerializerContext(
private static string GenerateJsonSerializerContext(
OpenApiDocument document,
RefitGeneratorSettings settings,
string contracts) =>
Expand Down Expand Up @@ -188,32 +135,6 @@ private static void GenerateAutoGeneratedHeader(RefitGeneratorSettings settings,

""");
}

private sealed class Swagger2OptionalReferencePropertyNullabilityRewriter : CSharpSyntaxRewriter
{
public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node)
{
if (node.Type is NullableTypeSyntax nullableType &&
IsReferenceType(nullableType.ElementType))
{
node = node.WithType(nullableType.ElementType.WithTriviaFrom(node.Type));
}

return base.VisitPropertyDeclaration(node);
}

private static bool IsReferenceType(TypeSyntax typeSyntax) =>
typeSyntax switch
{
PredefinedTypeSyntax predefinedType => predefinedType.Keyword.Kind() is SyntaxKind.ObjectKeyword or SyntaxKind.StringKeyword,
ArrayTypeSyntax => true,
IdentifierNameSyntax => true,
GenericNameSyntax => true,
QualifiedNameSyntax => true,
AliasQualifiedNameSyntax => true,
_ => false,
};
}
}

internal record GenerationResult(
Expand Down
9 changes: 9 additions & 0 deletions src/Refitter.Core/IContractsPostProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using NSwag;
using Refitter.Core;

namespace Refitter.Core;

internal interface IContractsPostProcessor
{
string Process(OpenApiDocument document, RefitGeneratorSettings settings, string contracts);
}
28 changes: 18 additions & 10 deletions src/Refitter.Core/RefitGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace Refitter.Core;
/// </summary>
public class RefitGenerator(RefitGeneratorSettings settings, OpenApiDocument document)
{
private readonly GeneratorPipeline pipeline = new();

/// <summary>
/// OpenAPI specifications used to generate Refit clients and interfaces.
Expand Down Expand Up @@ -157,18 +156,27 @@ private GenerationResult RunPipeline()
{
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.
var interfaceGenerator = new InterfaceGenerator(settings, document, generator, docGenerator);

var pipeline = new GeneratorPipeline(
docGenerator,
interfaceGenerator,
new IContractsPostProcessor[]
{
new Swagger2OptionalReferenceNullabilityNormalizer(),
new EnumStringConverterInjector(),
});

return pipeline.Run(document, settings, generator);
}

private string SanitizeGeneratedContracts(string contracts) =>
GeneratorPipeline.SanitizeGeneratedContracts(document, settings, contracts);

private string NormalizeSwagger2OptionalReferencePropertyNullability(string contracts) =>
GeneratorPipeline.NormalizeSwagger2OptionalReferencePropertyNullability(document, settings, contracts);

private string GenerateJsonSerializerContext(string contracts) =>
GeneratorPipeline.GenerateJsonSerializerContext(document, settings, contracts);

private string FormatSingleFile(GenerationResult result)
{
var contracts = settings.GenerateClients
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NJsonSchema;
using NSwag;

namespace Refitter.Core;

internal sealed class Swagger2OptionalReferenceNullabilityNormalizer : IContractsPostProcessor
{
public string Process(OpenApiDocument document, RefitGeneratorSettings settings, string contracts)
{
if (document.SchemaType != SchemaType.Swagger2 ||
settings.CodeGeneratorSettings?.GenerateNullableReferenceTypes != true ||
settings.CodeGeneratorSettings.GenerateOptionalPropertiesAsNullable)
{
return contracts;
}

var tree = CSharpSyntaxTree.ParseText(contracts);
var root = tree.GetCompilationUnitRoot();
var rewrittenRoot = new Swagger2OptionalReferencePropertyNullabilityRewriter().Visit(root);
return rewrittenRoot!.ToFullString();
}

private sealed class Swagger2OptionalReferencePropertyNullabilityRewriter : CSharpSyntaxRewriter
{
public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node)
{
if (node.Type is NullableTypeSyntax nullableType &&
IsReferenceType(nullableType.ElementType))
{
node = node.WithType(nullableType.ElementType.WithTriviaFrom(node.Type));
}

return base.VisitPropertyDeclaration(node);
}

private static bool IsReferenceType(TypeSyntax typeSyntax) =>
typeSyntax switch
{
PredefinedTypeSyntax predefinedType => predefinedType.Keyword.Kind() is SyntaxKind.ObjectKeyword or SyntaxKind.StringKeyword,
ArrayTypeSyntax => true,
IdentifierNameSyntax => true,
GenericNameSyntax => true,
QualifiedNameSyntax => true,
AliasQualifiedNameSyntax => true,
_ => false,
};
}
}
8 changes: 7 additions & 1 deletion src/Refitter.Tests/GeneratorPipelineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ public async Task Run_ByTag_Preserves_PreGeneration_OperationIds_For_Interface_N
private static GenerationResult RunPipeline(OpenApiDocument document, RefitGeneratorSettings settings)
{
var generator = new CSharpClientGeneratorFactory(settings, document).Create();
return new GeneratorPipeline().Run(document, settings, generator);
var docGenerator = new XmlDocumentationGenerator(settings);
var interfaceGenerator = new InterfaceGenerator(settings, document, generator, docGenerator);
var pipeline = new GeneratorPipeline(
docGenerator,
interfaceGenerator,
Array.Empty<IContractsPostProcessor>());
return pipeline.Run(document, settings, generator);
}
}
Loading
Loading