-
-
Notifications
You must be signed in to change notification settings - Fork 63
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
Changes from all commits
6dee666
c25a8c3
19c380b
4e69267
571741f
35c3d21
ecdc3eb
a474a16
17b7995
1bcdce6
7eb2e38
56492f5
ea9f24b
12dcb32
f7421e5
bc2cbcc
6ee74d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| ``` | ||
| 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()}".Sanitize(); | ||
|
|
||
| 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"); | ||
|
Comment on lines
+26
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for uses of GetDynamicQuerystringParameterType and verify baseOperationName is never empty
rg -nP -C5 'GetDynamicQuerystringParameterType|GetBaseOperationName' --type=csRepository: christianhelle/refitter Length of output: 8406 Guard against empty 🤖 Prompt for AI Agents |
||
|
|
||
| public bool IsSingleInterface => false; | ||
|
|
||
| public void AppendInterfaceDocumentation( | ||
| OpenApiDocument document, | ||
| XmlDocumentationGenerator docGenerator, | ||
| string groupKey, | ||
| OpenApiOperationInfo representativeOperation, | ||
| StringBuilder code) => | ||
| docGenerator.AppendInterfaceDocumentationByEndpoint(representativeOperation.Operation, code); | ||
| } | ||
| 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()}".Sanitize(); | ||
|
|
||
| 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); | ||
| } |
| 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.
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: 2072
🏁 Script executed:
Repository: christianhelle/refitter
Length of output: 33445
🏁 Script executed:
Repository: christianhelle/refitter
Length of output: 5011
🏁 Script executed:
Repository: christianhelle/refitter
Length of output: 947
🏁 Script executed:
Repository: christianhelle/refitter
Length of output: 9729
🏁 Script executed:
Repository: christianhelle/refitter
Length of output: 1165
🏁 Script executed:
Repository: christianhelle/refitter
Length of output: 388
🏁 Script executed:
Repository: christianhelle/refitter
Length of output: 9924
🏁 Script executed:
Repository: christianhelle/refitter
Length of output: 2973
Fix smoke-test behavior documented for v3.4 (and CLI publish is conditional).
test/smoke-tests.ps1publishessrc/Refitter/Refitter.csprojfrom source only when not running with-UseDockerand not in-UseProduction; in those modes it won’t do the source publish step../ConsoleApp/...(CI runs the script withworking-directory: test, so this resolves totest/ConsoleApp/), and it generates many v2.0/v3.0/v3.1 specs (there’stest/OpenAPI/v2.0,v3.0,v3.1), buttest/OpenAPI/v3.4doesn’t exist—so the v3.4 client generation loop will skip entirely.🤖 Prompt for AI Agents