Skip to content

Commit ddfa351

Browse files
committed
Added source generator
1 parent 4583f5f commit ddfa351

24 files changed

Lines changed: 886 additions & 7 deletions

‎README.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,40 @@ You can now use the `typedrest-codegen` command-line tool:
1313

1414
typedrest-codegen generate -f myapi.yml -o myclient/ -s MyService --generate-interfaces --generate-dtos
1515

16+
## Source generator
17+
18+
As an alternative to writing the generated code to disk you can generate the client during compilation:
19+
20+
dotnet add package TypedRest.CodeGeneration.CSharp.SourceGenerator
21+
22+
Then reference the OpenAPI/Swagger document from your project file:
23+
24+
```xml
25+
<ItemGroup>
26+
<TypedRestOpenApi Include="myapi.yml" ServiceName="MyService" Namespace="MyCompany.MyService" />
27+
</ItemGroup>
28+
```
29+
30+
The following metadata is supported. Each of them can also be set as an MSBuild property (prefixed with `TypedRest`, e.g. `$(TypedRestServiceName)`) to provide a default for all spec files in a project.
31+
32+
| Metadata | Description | Default |
33+
| -------------------- | ------------------------------------------------------------ | ------------------------------------- |
34+
| `ServiceName` | The service name to use for the entry endpoint. Required. | |
35+
| `Namespace` | The C# namespace for the endpoints. | `$(RootNamespace)`, else service name |
36+
| `DtoNamespace` | The C# namespace for the DTOs. | the endpoint namespace |
37+
| `GenerateInterfaces` | Controls whether to generate interfaces for endpoints. | `true` |
38+
| `GenerateDtos` | Controls whether to generate DTOs. | `true` |
39+
| `LangVersion` | The minimum C# version the generated DTOs must compile with. | the project's `$(LangVersion)` |
40+
41+
Note that `GenerateInterfaces` and `GenerateDtos` default to `true` here, while the command-line tool requires them to be turned on explicitly. Generated endpoints reference the DTO types by name, so turning `GenerateDtos` off means you have to provide those types yourself.
42+
43+
To inspect the generated code set `<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>`; the files are then written to `obj/`.
44+
45+
Limitations:
46+
47+
- The source generator runs inside the compiler, so it requires a .NET SDK that ships Roslyn 5.6 or newer (.NET SDK 10.0.302+). Use the command-line tool for older toolchains.
48+
- Only local `$ref`s are resolved. Bundle multi-file specs into a single document first.
49+
1650
## Custom code
1751

1852
If you want to generate clients for more complex APIs you may need to add custom code. You can do this by creating your own command-line tools and using these NuGet packages:

‎doc/docfx.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
{
66
"src": "../src",
77
"files": ["*/*.csproj"],
8-
"exclude": ["UnitTests/*", "TypedRest.CodeGeneration.Cli/*"]
8+
"exclude": ["UnitTests/*", "SmokeTest/*", "TypedRest.CodeGeneration.Cli/*", "TypedRest.CodeGeneration.CSharp.SourceGenerator/*"]
99
}
1010
],
1111
"properties": {

‎src/SmokeTest/SmokeTest.csproj‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<!--
4+
Compiles a client generated by TypedRest.CodeGeneration.CSharp.SourceGenerator. This is what covers the
5+
build/*.props and build/*.targets plumbing and the fact that the generated code actually compiles.
6+
-->
7+
8+
<!-- Project properties -->
9+
<PropertyGroup>
10+
<TargetFramework>net10.0</TargetFramework>
11+
<RootNamespace>SmokeTest</RootNamespace>
12+
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
13+
<IsPackable>False</IsPackable>
14+
<GenerateDocumentationFile>False</GenerateDocumentationFile>
15+
</PropertyGroup>
16+
17+
<!-- This project consumes the generated client, not the OpenAPI model, so drop the shared global usings -->
18+
<ItemGroup>
19+
<Using Remove="Microsoft.OpenApi;Microsoft.OpenApi.Any;Microsoft.OpenApi.Extensions;Microsoft.OpenApi.Interfaces;Microsoft.OpenApi.Models;Microsoft.OpenApi.Readers;Microsoft.OpenApi.Writers" />
20+
</ItemGroup>
21+
22+
<!-- Dependencies -->
23+
<ItemGroup>
24+
<ProjectReference Include="..\TypedRest.CodeGeneration.CSharp.SourceGenerator\TypedRest.CodeGeneration.CSharp.SourceGenerator.csproj"
25+
OutputItemType="Analyzer" ReferenceOutputAssembly="False" />
26+
</ItemGroup>
27+
<ItemGroup>
28+
<PackageReference Include="TypedRest" Version="1.6.5" />
29+
</ItemGroup>
30+
31+
<!-- Simulates what consumers get from the NuGet package -->
32+
<Import Project="..\TypedRest.CodeGeneration.CSharp.SourceGenerator\build\TypedRest.CodeGeneration.CSharp.SourceGenerator.props" />
33+
<ItemGroup>
34+
<TypedRestOpenApi Include="..\UnitTests\sample-v3.yml" ServiceName="Sample" Namespace="SmokeTest.Client" />
35+
</ItemGroup>
36+
<Import Project="..\TypedRest.CodeGeneration.CSharp.SourceGenerator\build\TypedRest.CodeGeneration.CSharp.SourceGenerator.targets" />
37+
38+
</Project>

‎src/SmokeTest/Usage.cs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using SmokeTest.Client;
2+
3+
namespace SmokeTest;
4+
5+
/// <summary>
6+
/// References the generated client, so that the compiler has to resolve the generated types.
7+
/// </summary>
8+
public static class Usage
9+
{
10+
public static ISampleClient Client(Uri uri)
11+
=> new SampleClient(uri);
12+
13+
public static async Task<Contact> GetFirstContact(ISampleClient client, CancellationToken cancellationToken = default)
14+
=> (await client.Contacts.ReadAllAsync(cancellationToken))[0];
15+
16+
public static async Task<Note> GetNote(IContactElementEndpoint contact, CancellationToken cancellationToken = default)
17+
=> await contact.Note.ReadAsync(cancellationToken);
18+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
; Shipped analyzer releases
2+
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
; Unshipped analyzer release
2+
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3+
4+
### New Rules
5+
6+
Rule ID | Category | Severity | Notes
7+
--------|----------|----------|------
8+
TRCG001 | TypedRest | Error | Missing ServiceName
9+
TRCG002 | TypedRest | Error | OpenAPI specification error
10+
TRCG003 | TypedRest | Warning | OpenAPI specification warning
11+
TRCG004 | TypedRest | Warning | Invalid LangVersion
12+
TRCG005 | TypedRest | Error | Code generation failed
13+
TRCG006 | TypedRest | Warning | Unreadable OpenAPI specification
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace TypedRest.CodeGeneration.CSharp;
2+
3+
/// <summary>
4+
/// The MSBuild property and item metadata names the generator reads.
5+
/// </summary>
6+
/// <remarks>
7+
/// These must be kept in sync with <c>build/TypedRest.CodeGeneration.CSharp.SourceGenerator.props</c>,
8+
/// which is what makes them visible to the compiler.
9+
/// </remarks>
10+
internal static class ConfigKeys
11+
{
12+
private const string PropertyPrefix = "build_property.";
13+
private const string MetadataPrefix = "build_metadata.TypedRestOpenApi.";
14+
15+
/// <summary>Set on every <c>TypedRestOpenApi</c> item, to tell them apart from other <c>AdditionalFiles</c>.</summary>
16+
public const string Marker = "IsTypedRestSpec";
17+
18+
public const string ServiceName = "ServiceName";
19+
public const string Namespace = "Namespace";
20+
public const string DtoNamespace = "DtoNamespace";
21+
public const string GenerateInterfaces = "GenerateInterfaces";
22+
public const string GenerateDtos = "GenerateDtos";
23+
public const string LangVersion = "LangVersion";
24+
25+
/// <summary>The MSBuild property the endpoint namespace falls back to.</summary>
26+
public const string RootNamespace = "RootNamespace";
27+
28+
/// <summary>Builds the key for reading the MSBuild property fallback for <paramref name="name"/>.</summary>
29+
public static string Property(string name) => PropertyPrefix + (name == RootNamespace ? name : "TypedRest" + name);
30+
31+
/// <summary>Builds the key for reading <paramref name="name"/> as <c>TypedRestOpenApi</c> item metadata.</summary>
32+
public static string Metadata(string name) => MetadataPrefix + name;
33+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace TypedRest.CodeGeneration.CSharp;
4+
5+
/// <summary>
6+
/// The diagnostics reported by <see cref="TypedRestSourceGenerator"/>.
7+
/// </summary>
8+
internal static class Diagnostics
9+
{
10+
private const string Category = "TypedRest";
11+
private const string HelpLink = "https://code-generation.typedrest.net/";
12+
13+
public static readonly DiagnosticDescriptor MissingServiceName = new(
14+
"TRCG001", "Missing ServiceName",
15+
"The TypedRestOpenApi item '{0}' has no ServiceName metadata and no $(TypedRestServiceName) fallback",
16+
Category, DiagnosticSeverity.Error, isEnabledByDefault: true, helpLinkUri: HelpLink);
17+
18+
public static readonly DiagnosticDescriptor SpecError = new(
19+
"TRCG002", "OpenAPI specification error",
20+
"Error in OpenAPI specification '{0}': {1}",
21+
Category, DiagnosticSeverity.Error, isEnabledByDefault: true, helpLinkUri: HelpLink);
22+
23+
public static readonly DiagnosticDescriptor SpecWarning = new(
24+
"TRCG003", "OpenAPI specification warning",
25+
"Warning in OpenAPI specification '{0}': {1}",
26+
Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, helpLinkUri: HelpLink);
27+
28+
public static readonly DiagnosticDescriptor InvalidLangVersion = new(
29+
"TRCG004", "Invalid LangVersion",
30+
"Invalid LangVersion '{0}'. Expected a value such as '8', '11.0' or 'latest'. Falling back to the language version of the project.",
31+
Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, helpLinkUri: HelpLink);
32+
33+
public static readonly DiagnosticDescriptor GenerationFailed = new(
34+
"TRCG005", "Code generation failed",
35+
"Failed to generate a TypedRest client from '{0}': {1}: {2}",
36+
Category, DiagnosticSeverity.Error, isEnabledByDefault: true, helpLinkUri: HelpLink);
37+
38+
public static readonly DiagnosticDescriptor UnreadableSpec = new(
39+
"TRCG006", "Unreadable OpenAPI specification",
40+
"Could not read the content of '{0}'",
41+
Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, helpLinkUri: HelpLink);
42+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Microsoft.CodeAnalysis.Diagnostics;
2+
3+
namespace TypedRest.CodeGeneration.CSharp;
4+
5+
/// <summary>
6+
/// Project-level configuration, used as a fallback for spec files that do not specify a value themselves.
7+
/// </summary>
8+
internal sealed record GlobalConfig(
9+
string? RootNamespace,
10+
string? ServiceName,
11+
string? Namespace,
12+
string? DtoNamespace,
13+
bool? GenerateInterfaces,
14+
bool? GenerateDtos,
15+
string? LangVersion)
16+
{
17+
public static GlobalConfig From(AnalyzerConfigOptions options)
18+
=> new(
19+
GetString(options, ConfigKeys.RootNamespace),
20+
GetString(options, ConfigKeys.ServiceName),
21+
GetString(options, ConfigKeys.Namespace),
22+
GetString(options, ConfigKeys.DtoNamespace),
23+
GetBool(options, ConfigKeys.GenerateInterfaces),
24+
GetBool(options, ConfigKeys.GenerateDtos),
25+
GetString(options, ConfigKeys.LangVersion));
26+
27+
private static string? GetString(AnalyzerConfigOptions options, string name)
28+
=> options.TryGetValue(ConfigKeys.Property(name), out string? value) && !string.IsNullOrWhiteSpace(value)
29+
? value
30+
: null;
31+
32+
private static bool? GetBool(AnalyzerConfigOptions options, string name)
33+
=> GetString(options, name) is {} value && bool.TryParse(value, out bool result)
34+
? result
35+
: null;
36+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// ReSharper disable once CheckNamespace
2+
namespace System.Runtime.CompilerServices;
3+
4+
/// <summary>
5+
/// Enables <c>record</c> and <c>init</c> accessors on .NET Standard 2.0.
6+
/// </summary>
7+
internal static class IsExternalInit;

0 commit comments

Comments
 (0)