-
-
Notifications
You must be signed in to change notification settings - Fork 64
RefitGenerator into separate modules (document filter, schema cleaner, code generator, pipeline) #1146
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
Merged
Merged
RefitGenerator into separate modules (document filter, schema cleaner, code generator, pipeline) #1146
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
ddc8522
feat: create RefitDocumentFilter with FilterByTags and FilterByPath
christianhelle 541d8cc
feat: create IRefitSchemaCleaner, RefitSchemaCleaner, IRefitCodeGener…
christianhelle 9cec463
refactor: create RefitPipeline orchestrator and refactor RefitGenerat…
christianhelle e56b6a0
docs: add XML doc comments to all new public types
christianhelle ccb4df0
test: add unit tests for RefitDocumentFilter, RefitSchemaCleaner, Ref…
christianhelle 0b8dae1
Add missing System.Text.RegularExpressions using directive to RefitDo…
Copilot b41e0ad
fix: apply CodeRabbit auto-fixes
coderabbitai[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
|
||
| { | ||
| if (document == null) throw new ArgumentNullException(nameof(document)); | ||
| if (includeTags == null) throw new ArgumentNullException(nameof(includeTags)); | ||
|
|
||
| if (includeTags.Length == 0) | ||
| return document; | ||
|
|
||
| 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(); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.