-
-
Notifications
You must be signed in to change notification settings - Fork 64
Refactor interface generator #1114
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
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
6dee666
Add AGENTS.md
christianhelle c25a8c3
feat: add OpenApiOperationInfo record for partitioning strategy
christianhelle 19c380b
feat: define IInterfacePartitioning seam for interface grouping strat…
christianhelle 4e69267
feat: add SingleInterfacePartitioning adapter for single-interface mode
christianhelle 571741f
feat: add ByEndpointInterfacePartitioning adapter for per-endpoint in…
christianhelle 35c3d21
feat: add ByTagInterfacePartitioning adapter for per-tag interfaces
christianhelle ecdc3eb
feat: add InterfaceGenerator deep module with pluggable IInterfacePar…
christianhelle a474a16
refactor: update RefitGenerator to use InterfaceGenerator and IInterf…
christianhelle 17b7995
refactor: remove old IRefitInterfaceGenerator and implementations (re…
christianhelle 1bcdce6
fix: handle empty paths and skip deprecated-only groups in InterfaceG…
christianhelle 7eb2e38
docs: add commit discipline section to AGENTS.md
christianhelle 56492f5
refactor: standardize variable naming by removing underscores from Re…
christianhelle ea9f24b
Remove bot and hubspot example specs
christianhelle 12dcb32
refactor: replace CreateInterfaceGenerator method with direct instant…
christianhelle f7421e5
refactor: simplify GroupBy usage in InterfaceGenerator
christianhelle bc2cbcc
refactor: sanitize interface name generation in partitioning classes
christianhelle 6ee74d9
Move AGENTS.md to docs
christianhelle 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,76 @@ | ||
| # AGENTS.md — Refitter | ||
|
|
||
| OpenAPI-to-Refit code generator. Multi-package solution: CLI tool, MSBuild task, C# Source Generator, and Core library. | ||
|
|
||
| ## Build & Test | ||
|
|
||
| - Solution: `src/Refitter.slnx` | ||
| - SDK: `10.0.300` with `rollForward: latestFeature` (see `global.json`) | ||
| - Build: `dotnet build -c Release src/Refitter.slnx` | ||
| - Test: `dotnet test --solution src/Refitter.slnx -c Release` | ||
|
|
||
| **Source Generator tests require a pre-build step.** Before running `dotnet test` on `Refitter.SourceGenerator.Tests`, the CI does: | ||
|
|
||
| ```bash | ||
| rm -rf src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated | ||
| dotnet restore src/Refitter.SourceGenerator.Tests/Refitter.SourceGenerator.Tests.csproj | ||
| dotnet msbuild src/Refitter.SourceGenerator.Tests/Refitter.SourceGenerator.Tests.csproj | ||
| ``` | ||
|
|
||
| Run these first if source generator tests fail with missing generated types. | ||
|
|
||
| ## Package Boundaries | ||
|
|
||
| | Project | TFM | Role | | ||
| |---|---|---| | ||
| | `Refitter.Core` | `netstandard2.0` | Core generation logic (NSwag-based) | | ||
| | `Refitter` | `net8.0;net9.0;net10.0` | CLI tool (`PackAsTool`, container support) | | ||
| | `Refitter.MSBuild` | `netstandard2.0` | MSBuild task; **requires Refitter CLI binaries** to be built first because it copies `../Refitter/bin/$(Configuration)/net{8,9,10}.0/**/*` into the package | | ||
| | `Refitter.SourceGenerator` | `netstandard2.0` | Roslyn source generator; emits code in-memory via `AddSource()` (not to disk since v2.0.0) | | ||
| | `Refitter.Tests` | `net10.0` | Unit tests | | ||
| | `Refitter.SourceGenerator.Tests` | `net8.0;net10.0` | Source generator integration tests | | ||
|
|
||
| ## Testing | ||
|
|
||
| - **Framework**: TUnit (not xUnit). Attributes: `[Test]`, `[Arguments(...)]`. | ||
| - **Runner**: `Microsoft.Testing.Platform` (configured in `global.json`). Both test projects set `OutputType=Exe` and `GenerateProgramFile=false`. | ||
| - **Unit test pattern**: Scenario tests under `Refitter.Tests.Scenarios` contain an OpenAPI spec as a `const string`, generate code via `RefitGenerator.CreateAsync(...).Generate()`, assert string patterns, and verify compilation with `BuildHelper.BuildCSharp(generatedCode)`. | ||
| - **Temp file helper**: `SwaggerFileHelper.CreateSwaggerFile(contents)` creates a temp YAML file; `CreateSwaggerJsonFile` for JSON. | ||
| - **Build helper**: `BuildHelper.BuildCSharp()` writes generated code to a temp project and invokes `dotnet build` to verify it compiles. It defaults to `net8.0` but supports `net9.0` and `net10.0`. | ||
| - **Source generator tests**: Reference the generator as an `Analyzer` with `ReferenceOutputAssembly=false`. They verify generated types exist at runtime and have Refit HTTP attributes. | ||
|
|
||
| ## CI / Smoke Tests | ||
|
|
||
| - Build workflow: `.github/workflows/build.yml` | ||
| - Smoke tests: `.github/workflows/smoke-tests.yml` runs `test/smoke-tests.ps1` | ||
| - The smoke test script publishes the CLI from source, generates clients against dozens of OpenAPI specs (v2.0, v3.0, v3.1, v3.4), and compiles them against console apps in `test/ConsoleApp/`. | ||
|
|
||
| ## Code Style | ||
|
|
||
| - `.editorconfig` is authoritative. | ||
| - 4-space indentation; `crlf` line endings for C# files per `.editorconfig`. | ||
| - `var` is discouraged (`csharp_style_var_* = false:silent`). | ||
| - `TreatWarningsAsErrors` is enabled on most production projects. | ||
|
|
||
| ## Key Conventions | ||
|
|
||
| - **New features** must have unit tests in the `Refitter.Tests.Scenarios` namespace following the pattern: spec string → generate → assert → build. | ||
| - **New CLI arguments** must be documented in `README.md` and in the `.refitter` file format docs. | ||
| - **`.refitter` files** are the settings format for all three distribution forms (CLI, MSBuild, Source Generator). | ||
| - **Source generator `.refitter` files** are automatically included as `AdditionalFiles` via the package props. The test project explicitly excludes `PropertyNamingPolicy.refitter` during design-time builds (`Condition="'$(DesignTimeBuild)' != 'true'"`). | ||
| - **MSBuild** package includes a custom `.targets` file that runs `RefitterGenerateTask` before `BeforeCompile`. | ||
|
|
||
| ## Commit Discipline | ||
|
|
||
| - **Commit as often as possible** in small, logical groups. Each commit should represent one coherent change (e.g., one file created, one bug fix, one adapter added). | ||
| - **Build and run tests before every commit.** The minimum check is `dotnet build -c Release src/Refitter.slnx` followed by `dotnet test --solution src/Refitter.slnx -c Release --no-build`. | ||
| - **Never commit broken code.** If tests fail, fix them before committing. If the fix requires additional changes, commit those together as a "fix: ..." commit. | ||
| - **Commit messages** follow the pattern: `type: description` where type is one of `feat`, `fix`, `refactor`, `docs`, `test`, `chore`. Keep the subject line under 50 characters when possible. | ||
|
|
||
| ## Documentation | ||
|
|
||
| - API docs are generated with DocFX from `docs/docfx_project/docfx.json`. To build locally: | ||
| ```bash | ||
| dotnet tool update -g docfx | ||
| docfx docs/docfx_project/docfx.json | ||
| ``` |
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,38 @@ | ||
| using System.Text; | ||
| using NSwag; | ||
|
|
||
| namespace Refitter.Core; | ||
|
|
||
| internal class ByEndpointInterfacePartitioning( | ||
| RefitGeneratorSettings settings) : IInterfacePartitioning | ||
| { | ||
|
|
||
| public string GetGroupKey(OpenApiOperationInfo operation) => $"{operation.Path}#{operation.Verb}"; | ||
|
|
||
| public string GetInterfaceName(string groupKey, string title, string baseOperationName) => | ||
| "I" + baseOperationName.CapitalizeFirstCharacter(); | ||
|
|
||
| public string GetInterfaceNameSuffix() => "Endpoint"; | ||
|
|
||
| public string GetMethodName(OpenApiOperationInfo operation, string interfaceName, string baseOperationName) | ||
| { | ||
| var methodName = !string.IsNullOrWhiteSpace(settings.OperationNameTemplate) | ||
| ? settings.OperationNameTemplate!.Replace("{operationName}", "Execute") | ||
| : "Execute"; | ||
|
|
||
| return methodName; | ||
| } | ||
|
|
||
| public string GetDynamicQuerystringParameterType(string interfaceName, string methodName) => | ||
| interfaceName.Substring(1).Replace("Endpoint", "QueryParams"); | ||
|
|
||
| public bool IsSingleInterface => false; | ||
|
|
||
| public void AppendInterfaceDocumentation( | ||
| OpenApiDocument document, | ||
| XmlDocumentationGenerator docGenerator, | ||
| string groupKey, | ||
| OpenApiOperationInfo representativeOperation, | ||
| StringBuilder code) => | ||
| docGenerator.AppendInterfaceDocumentationByEndpoint(representativeOperation.Operation, code); | ||
| } | ||
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,56 @@ | ||
| using System.Text; | ||
| using NSwag; | ||
|
|
||
| namespace Refitter.Core; | ||
|
|
||
| internal class ByTagInterfacePartitioning : IInterfacePartitioning | ||
| { | ||
| private readonly RefitGeneratorSettings settings; | ||
| private readonly string ungroupedTitle; | ||
|
|
||
| public ByTagInterfacePartitioning(RefitGeneratorSettings settings, OpenApiDocument document) | ||
| { | ||
| this.settings = settings; | ||
| ungroupedTitle = settings.Naming.UseOpenApiTitle | ||
| ? (document.Info?.Title ?? "ApiClient").Sanitize() | ||
| : settings.Naming.InterfaceName; | ||
| ungroupedTitle = ungroupedTitle.CapitalizeFirstCharacter(); | ||
| } | ||
|
|
||
| public string GetGroupKey(OpenApiOperationInfo operation) | ||
| { | ||
| var tag = operation.Operation.Tags.FirstOrDefault(); | ||
| return !string.IsNullOrWhiteSpace(tag) | ||
| ? tag.SanitizeControllerTag() | ||
| : ungroupedTitle; | ||
| } | ||
|
|
||
| public string GetInterfaceName(string groupKey, string title, string baseOperationName) => | ||
| $"I{groupKey.CapitalizeFirstCharacter()}"; | ||
|
|
||
| public string GetInterfaceNameSuffix() => "Api"; | ||
|
|
||
| public string GetMethodName(OpenApiOperationInfo operation, string interfaceName, string baseOperationName) | ||
| { | ||
| var methodName = baseOperationName.CapitalizeFirstCharacter(); | ||
| if (settings.OperationNameTemplate?.Contains("{operationName}") ?? false) | ||
| { | ||
| methodName = settings.OperationNameTemplate!.Replace("{operationName}", methodName); | ||
| } | ||
|
|
||
| return methodName; | ||
| } | ||
|
|
||
| public string GetDynamicQuerystringParameterType(string interfaceName, string methodName) => | ||
| methodName + "QueryParams"; | ||
|
|
||
| public bool IsSingleInterface => false; | ||
|
|
||
| public void AppendInterfaceDocumentation( | ||
| OpenApiDocument document, | ||
| XmlDocumentationGenerator docGenerator, | ||
| string groupKey, | ||
| OpenApiOperationInfo representativeOperation, | ||
| StringBuilder code) => | ||
| docGenerator.AppendInterfaceDocumentationByTag(document, groupKey, code); | ||
| } |
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,47 @@ | ||
| using System.Text; | ||
| using NSwag; | ||
|
|
||
| namespace Refitter.Core; | ||
|
|
||
| internal interface IInterfacePartitioning | ||
| { | ||
| /// <summary> | ||
| /// Gets the group key for an operation. Operations with the same key are placed in the same interface. | ||
| /// </summary> | ||
| string GetGroupKey(OpenApiOperationInfo operation); | ||
|
|
||
| /// <summary> | ||
| /// Gets the raw interface name for a group key. The InterfaceGenerator handles deduplication. | ||
| /// </summary> | ||
| string GetInterfaceName(string groupKey, string title, string baseOperationName); | ||
|
|
||
| /// <summary> | ||
| /// Gets the raw method name for an operation. The InterfaceGenerator handles deduplication. | ||
| /// </summary> | ||
| string GetMethodName(OpenApiOperationInfo operation, string interfaceName, string baseOperationName); | ||
|
|
||
| /// <summary> | ||
| /// Gets the dynamic querystring parameter type name for a method. | ||
| /// </summary> | ||
| string GetDynamicQuerystringParameterType(string interfaceName, string methodName); | ||
|
|
||
| /// <summary> | ||
| /// Gets the suffix appended to the interface name for deduplication. | ||
| /// </summary> | ||
| string GetInterfaceNameSuffix(); | ||
|
|
||
| /// <summary> | ||
| /// True if this partitioning generates exactly one interface. | ||
| /// </summary> | ||
| bool IsSingleInterface { get; } | ||
|
|
||
| /// <summary> | ||
| /// Appends interface-level documentation to the StringBuilder. | ||
| /// </summary> | ||
| void AppendInterfaceDocumentation( | ||
| OpenApiDocument document, | ||
| XmlDocumentationGenerator docGenerator, | ||
| string groupKey, | ||
| OpenApiOperationInfo representativeOperation, | ||
| StringBuilder code); | ||
| } |
This file was deleted.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: christianhelle/refitter
Length of output: 8406
Guard against empty
baseOperationNameto avoid returning an empty dynamic query typeIn
src/Refitter.Core/ByEndpointInterfacePartitioning.cs(lines 26-27),GetDynamicQuerystringParameterTypeusesinterfaceName.Substring(1) ...; ifbaseOperationNameis empty,interfaceNamecan become"I"and this expression returns""(no exception, but an empty generated type name).InterfaceGeneratorexplicitly setsbaseOperationNametostring.Emptywhenoperations.Count == 0. Add a length/empty guard and fall back to a non-empty type name (e.g.,methodName + "QueryParams"like the other partitionings).🤖 Prompt for AI Agents