Skip to content
26 changes: 26 additions & 0 deletions src/Refitter.Core/IRefitCodeGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using NSwag;

namespace Refitter.Core;

/// <summary>
/// Generates Refit client code from a cleaned OpenAPI document.
/// Produces single-file or multi-file output.
/// </summary>
public interface IRefitCodeGenerator
{
/// <summary>
/// Generates all Refit code as a single string.
/// </summary>
/// <param name="document">The OpenAPI document to generate code from.</param>
/// <param name="settings">The generator settings.</param>
/// <returns>The generated C# code.</returns>
string Generate(OpenApiDocument document, RefitGeneratorSettings settings);

/// <summary>
/// Generates Refit code as multiple files (interfaces, contracts, DI, serializer context).
/// </summary>
/// <param name="document">The OpenAPI document to generate code from.</param>
/// <param name="settings">The generator settings.</param>
/// <returns>A collection of generated code files.</returns>
GeneratorOutput GenerateMultipleFiles(OpenApiDocument document, RefitGeneratorSettings settings);
}
24 changes: 24 additions & 0 deletions src/Refitter.Core/IRefitSchemaCleaner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using NSwag;

namespace Refitter.Core;

/// <summary>
/// Cleans an OpenAPI document by removing unreferenced schemas.
/// Returns a new document without mutating the input.
/// </summary>
public interface IRefitSchemaCleaner
{
/// <summary>
/// Removes unreferenced schemas from the document when <paramref name="removeUnusedSchema"/> is true.
/// </summary>
/// <param name="document">The OpenAPI document to clean.</param>
/// <param name="removeUnusedSchema">Whether to remove unused schemas.</param>
/// <param name="keepSchemaPatterns">Regex patterns for schemas to always keep.</param>
/// <param name="includeInheritanceHierarchy">Whether to keep inheritance hierarchy types.</param>
/// <returns>The cleaned OpenAPI document.</returns>
OpenApiDocument Clean(
OpenApiDocument document,
bool removeUnusedSchema,
string[] keepSchemaPatterns,
bool includeInheritanceHierarchy);
}
127 changes: 127 additions & 0 deletions src/Refitter.Core/RefitCodeGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Text;
using NSwag;

namespace Refitter.Core;

/// <summary>
/// Generates Refit client and interface code from an OpenAPI document.
/// Handles both single-file and multi-file output modes.
/// </summary>
public sealed class RefitCodeGenerator : IRefitCodeGenerator
{
/// <summary>
/// Generates all Refit code as a single string.
/// </summary>
public string Generate(OpenApiDocument document, RefitGeneratorSettings settings)
{
var result = RunPipeline(document, settings);
return FormatSingleFile(result, settings);
}

/// <summary>
/// Generates Refit code as multiple files (interfaces, contracts, DI, serializer context).
/// </summary>
public GeneratorOutput GenerateMultipleFiles(OpenApiDocument document, RefitGeneratorSettings settings)
{
var result = RunPipeline(document, settings);
return new GeneratorOutput(FormatMultipleFiles(result, settings, document));
}

private static GenerationResult RunPipeline(
OpenApiDocument document,
RefitGeneratorSettings settings)
{
var factory = new CSharpClientGeneratorFactory(settings, document);
var generator = factory.Create();
var docGenerator = new XmlDocumentationGenerator(settings);

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 static string FormatSingleFile(GenerationResult result, RefitGeneratorSettings settings)
{
var contracts = settings.GenerateClients
? RefitInterfaceImports
.GetImportedNamespaces(settings)
.Aggregate(
result.Contracts,
(current, import) => current.Replace($"{import}.", string.Empty))
: result.Contracts;

var output = new StringBuilder()
.AppendLine(settings.GenerateClients ? string.Join("", result.Interfaces.Select(c => c.Content)) : string.Empty)
.AppendLine()
.AppendLine(settings.GenerateContracts ? contracts : string.Empty)
.AppendLine(result.SerializerContext)
.AppendLine(result.DependencyInjectionCode)
.ToString()
.TrimEnd();

return ApplyContractTypeSuffix(output, settings);
}

private static IReadOnlyList<GeneratedCode> FormatMultipleFiles(
GenerationResult result,
RefitGeneratorSettings settings,
OpenApiDocument document)
{
var generatedFiles = new List<GeneratedCode>(result.Interfaces);

if (settings.GenerateContracts)
{
generatedFiles.Add(new GeneratedCode(TypenameConstants.Contracts, result.Contracts));
}

if (!string.IsNullOrWhiteSpace(result.SerializerContext))
{
generatedFiles.Add(
new GeneratedCode(
JsonSerializerContextGenerator.GetContextTypeName(settings, document.Info?.Title),
result.SerializerContext));
}

if (!string.IsNullOrWhiteSpace(result.DependencyInjectionCode))
{
generatedFiles.Add(
new GeneratedCode(
TypenameConstants.DependencyInjection,
result.DependencyInjectionCode));
}

return ApplyContractTypeSuffix(generatedFiles, settings);
}

private static string ApplyContractTypeSuffix(string content, RefitGeneratorSettings settings)
{
var contractTypeSuffix = settings.ContractTypeSuffix;
return contractTypeSuffix is not null && !string.IsNullOrWhiteSpace(contractTypeSuffix)
? ContractTypeSuffixApplier.ApplySuffix(content, contractTypeSuffix)
: content;
}

private static IReadOnlyList<GeneratedCode> ApplyContractTypeSuffix(
IReadOnlyList<GeneratedCode> generatedFiles,
RefitGeneratorSettings settings)
{
var contractTypeSuffix = settings.ContractTypeSuffix;
return contractTypeSuffix is not null && !string.IsNullOrWhiteSpace(contractTypeSuffix)
? generatedFiles
.Select(f => f with
{
Content = ContractTypeSuffixApplier.ApplySuffix(f.Content, contractTypeSuffix)
})
.ToList()
: generatedFiles;
}
}
99 changes: 99 additions & 0 deletions src/Refitter.Core/RefitDocumentFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Text.RegularExpressions;

using NSwag;

namespace Refitter.Core;

/// <summary>
/// Filters an OpenAPI document by tags and path patterns.
/// Each filter operation returns a new document without mutating the input.
/// </summary>
public static class RefitDocumentFilter
{
/// <summary>
/// Removes operations from the document that do not match any of the specified tags.
/// Returns a new document; the original is not modified.
/// </summary>
/// <param name="document">The OpenAPI document to filter.</param>
/// <param name="includeTags">Tags to include. When empty, all operations are kept.</param>
/// <returns>A new OpenAPI document with only matching operations.</returns>
public static OpenApiDocument FilterByTags(OpenApiDocument document, string[] includeTags)

Check failure on line 20 in src/Refitter.Core/RefitDocumentFilter.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=christianhelle_refitter&issues=AZ7IfDy8Gmv_3eIbeaLA&open=AZ7IfDy8Gmv_3eIbeaLA&pullRequest=1146
{
if (document == null) throw new ArgumentNullException(nameof(document));
if (includeTags == null) throw new ArgumentNullException(nameof(includeTags));

if (includeTags.Length == 0)
return document;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

var result = CloneDocument(document);
var clonedPaths = result.Paths
.Where(pair => pair.Value != null)
.ToArray();

foreach (var path in clonedPaths)
{
if (path.Value == null)
continue;

var methods = path.Value
.Where(pair => pair.Value != null)
.ToArray();

foreach (var method in methods)
{
if (method.Value == null)
continue;

var exclude = method.Value.Tags?.Exists(includeTags.Contains) != true;
if (exclude)
path.Value.Remove(method.Key);

if (path.Value.Count == 0)
result.Paths.Remove(path.Key);
}
}

return result;
}

/// <summary>
/// Removes paths from the document that do not match any of the specified regular expressions.
/// Returns a new document; the original is not modified.
/// </summary>
/// <param name="document">The OpenAPI document to filter.</param>
/// <param name="includePathMatches">Regular expressions to match paths against. When empty, all paths are kept.</param>
/// <returns>A new OpenAPI document with only matching paths.</returns>
public static OpenApiDocument FilterByPath(OpenApiDocument document, string[] includePathMatches)
{
if (document == null) throw new ArgumentNullException(nameof(document));
if (includePathMatches == null) throw new ArgumentNullException(nameof(includePathMatches));

if (includePathMatches.Length == 0)
return document;

var result = CloneDocument(document);
var regexes = includePathMatches
.Select(x => new Regex(x, RegexOptions.Compiled, TimeSpan.FromSeconds(1)))
.ToArray();
var paths = result.Paths.Keys
.Where(pathKey =>
{
for (var i = 0; i < regexes.Length; i++)
{
if (regexes[i].IsMatch(pathKey))
return false;
}

return true;
})
.ToArray();

foreach (string pathKey in paths)
result.Paths.Remove(pathKey);

return result;
}

private static OpenApiDocument CloneDocument(OpenApiDocument document)
=> OpenApiDocument.FromJsonAsync(document.ToJson()).GetAwaiter().GetResult();
}
Loading
Loading