feat: first basic working version

This commit is contained in:
Peter 2023-08-01 22:08:11 +02:00
parent 79c9b848c6
commit 867804a4b0
Signed by: prskr
GPG key ID: C1DB5D2E8DB512F9
47 changed files with 925 additions and 275 deletions

View file

@ -1,5 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {}
"tools": {
"nuke.globaltool": {
"version": "6.2.1",
"commands": [
"nuke"
]
}
}
}

1
.gitignore vendored
View file

@ -134,6 +134,7 @@ $tf/
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
*.DotSettings
# TeamCity is a build add-in
_TeamCity*

4
.nuke/parameters.json Normal file
View file

@ -0,0 +1,4 @@
{
"$schema": "./build.schema.json",
"Solution": "Bismarck.NET.sln"
}

View file

@ -11,6 +11,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{C0
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PetStoreApi", "examples\PetStoreApi\PetStoreApi.csproj", "{527811DB-DB1C-40F7-AB9F-313DEFFEEA7D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bismarck.AspNetCore", "src\Bismarck.AspNetCore\Bismarck.AspNetCore.csproj", "{1A2D0476-AA20-4C52-B015-4573A84657A5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{6396E293-E7F6-46DF-B157-80597CB8D5A0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -20,6 +24,8 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6396E293-E7F6-46DF-B157-80597CB8D5A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6396E293-E7F6-46DF-B157-80597CB8D5A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1DE0284-6376-4D0C-A18D-9065544FC532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1DE0284-6376-4D0C-A18D-9065544FC532}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1DE0284-6376-4D0C-A18D-9065544FC532}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -28,9 +34,14 @@ Global
{527811DB-DB1C-40F7-AB9F-313DEFFEEA7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{527811DB-DB1C-40F7-AB9F-313DEFFEEA7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{527811DB-DB1C-40F7-AB9F-313DEFFEEA7D}.Release|Any CPU.Build.0 = Release|Any CPU
{1A2D0476-AA20-4C52-B015-4573A84657A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A2D0476-AA20-4C52-B015-4573A84657A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A2D0476-AA20-4C52-B015-4573A84657A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A2D0476-AA20-4C52-B015-4573A84657A5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A1DE0284-6376-4D0C-A18D-9065544FC532} = {C783C00E-C98E-46A0-A8F2-5AEA26A83ABD}
{527811DB-DB1C-40F7-AB9F-313DEFFEEA7D} = {C0C20B96-58A8-4ED9-964D-4DA4A0CE638E}
{1A2D0476-AA20-4C52-B015-4573A84657A5} = {C783C00E-C98E-46A0-A8F2-5AEA26A83ABD}
EndGlobalSection
EndGlobal

11
build/.editorconfig Normal file
View file

@ -0,0 +1,11 @@
[*.cs]
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
dotnet_style_require_accessibility_modifiers = never:warning
csharp_style_expression_bodied_methods = true:silent
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_indexers = true:warning
csharp_style_expression_bodied_accessors = true:warning

46
build/Build.cs Normal file
View file

@ -0,0 +1,46 @@
using System;
using System.Linq;
using Nuke.Common;
using Nuke.Common.CI;
using Nuke.Common.Execution;
using Nuke.Common.IO;
using Nuke.Common.ProjectModel;
using Nuke.Common.Tooling;
using Nuke.Common.Utilities.Collections;
using static Nuke.Common.EnvironmentInfo;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction;
class Build : NukeBuild
{
/// Support plugins are available for:
/// - JetBrains ReSharper https://nuke.build/resharper
/// - JetBrains Rider https://nuke.build/rider
/// - Microsoft VisualStudio https://nuke.build/visualstudio
/// - Microsoft VSCode https://nuke.build/vscode
public static int Main() => Execute<Build>(x => x.Compile);
[Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
Target Clean => _ => _
.Before(Restore)
.Executes(() =>
{
});
Target Restore => _ => _
.Executes(() =>
{
});
Target Compile => _ => _
.DependsOn(Restore)
.Executes(() =>
{
});
}

15
build/Configuration.cs Normal file
View file

@ -0,0 +1,15 @@
using System.ComponentModel;
using Nuke.Common.Tooling;
[TypeConverter(typeof(TypeConverter<Configuration>))]
public class Configuration : Enumeration
{
public static Configuration Debug = new Configuration { Value = nameof(Debug) };
public static Configuration Release = new Configuration { Value = nameof(Release) };
public static implicit operator string(Configuration configuration)
{
return configuration.Value;
}
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This file prevents unintended imports of unrelated MSBuild files -->
<!-- Uncomment to include parent Directory.Build.props file -->
<!--<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />-->
</Project>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This file prevents unintended imports of unrelated MSBuild files -->
<!-- Uncomment to include parent Directory.Build.targets file -->
<!--<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.targets', '$(MSBuildThisFileDirectory)../'))" />-->
</Project>

17
build/_build.csproj Normal file
View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace></RootNamespace>
<NoWarn>CS0649;CS0169</NoWarn>
<NukeRootDirectory>..</NukeRootDirectory>
<NukeScriptDirectory>..\examples\PetStoreApi</NukeScriptDirectory>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="6.2.1" />
</ItemGroup>
</Project>

View file

@ -1,12 +1,31 @@
using Microsoft.AspNetCore.Mvc;
namespace PetStoreApi.Controllers;
public class PetEndpoints : ControllerBase
using PetStoreApi;
public class PetEndpoints : IFindPetsByTags, IUpdatePet, IAddPet
{
public Task<IActionResult> OnUpdatePetAsync()
private readonly ILogger<PetEndpoints> _logger;
public PetEndpoints(ILogger<PetEndpoints> logger)
{
var p = new Pet { Id = 1 };
return Task.FromResult(Ok() as IActionResult);
_logger = logger;
}
public Task<IResult> OnFindPetsByTagsAsync(string[] tags, CancellationToken token = default)
{
_logger.LogInformation("OnFindPetsByTagsAsync: {Tags}", tags);
return Task.FromResult(Results.Empty);
}
public Task<IResult> OnUpdatePetAsync(Pet body, CancellationToken token = default)
{
_logger.LogInformation("OnUpdatePetAsync");
return Task.FromResult(Results.Empty);
}
public Task<IResult> OnAddPetAsync(Pet body, CancellationToken token = default)
{
_logger.LogInformation("OnAddPetAsync");
return Task.FromResult(Results.Empty);
}
}

View file

@ -0,0 +1,9 @@
using Bismarck.AspNetCore.Metadata;
namespace PetStoreApi.Controllers;
[MapServiceEndpoints("PetStoreApi")]
public static partial class PetEndpointsMapper
{
}

View file

@ -1,32 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace PetStoreApi.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public Task<IActionResult> Get()
{
return Task.FromResult(Ok(Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray()) as IActionResult);
}
}

View file

@ -8,18 +8,19 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Bismarck.CodeGenerator\Bismarck.CodeGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
<ProjectReference Include="..\..\src\Bismarck.AspNetCore\Bismarck.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\Bismarck.CodeGenerator\Bismarck.CodeGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="api/PetStoreApi.yaml" Service="PetStoreApi"/>
<AdditionalFiles Include="api/PetStoreApi.yaml" Service="PetStoreApi" />
</ItemGroup>
<Import Project="..\..\src\Bismarck.CodeGenerator\BismarckCodeGenerator.props"/>
<Import Project="..\..\src\Bismarck.CodeGenerator\BismarckCodeGenerator.props" />
</Project>

View file

@ -1,8 +1,29 @@
using System.Text.Json.Serialization;
using Bismarck.AspNetCore;
using PetStoreApi.Controllers;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// enable enums as JSON for Swashbuckle docs
builder.Services.AddMvcCore().AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
// enable enums as JSON for minimal API
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
builder.Services.AddEndpoints();
//builder.Services.AddHttpContextAccessor();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
@ -17,9 +38,13 @@ if (app.Environment.IsDevelopment())
}
app.UseHttpsRedirection();
app.MapEndpoints();
app.UseAuthorization();
app.MapControllers();
app.Run();
app.Run();
public partial class Program
{
}

View file

@ -1,12 +0,0 @@
namespace PetStoreApi;
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}

View file

@ -126,13 +126,12 @@ paths:
schema:
type: string
default: available
externalDocs:
url: https://dev.azure.com
x-api-type: DeclarativeRouter.Models.PetStatus
example: "available"
enum:
- available
- pending
- sold
x-bismarck-type/PetStoreApi: PetStatus
responses:
"200":
description: successful operation
@ -628,7 +627,7 @@ components:
- placed
- approved
- delivered
x-bismarck-type-name/PetStoreApi: OrderStatus
x-bismarck-type/PetStoreApi: OrderStatus
complete:
type: boolean
xml:
@ -762,6 +761,7 @@ components:
- available
- pending
- sold
x-bismarck-type/PetStoreApi: PetStatus
xml:
name: pet
ApiResponse:

7
examples/PetStoreApi/build.cmd Executable file
View file

@ -0,0 +1,7 @@
:; set -eo pipefail
:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
:; ${SCRIPT_DIR}/build.sh "$@"
:; exit $?
@ECHO OFF
powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*

View file

@ -0,0 +1,69 @@
[CmdletBinding()]
Param(
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$BuildArguments
)
Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
###########################################################################
# CONFIGURATION
###########################################################################
$BuildProjectFile = "$PSScriptRoot\..\..\build\_build.csproj"
$TempDirectory = "$PSScriptRoot\..\..\.nuke\temp"
$DotNetGlobalFile = "$PSScriptRoot\..\..\global.json"
$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
$DotNetChannel = "Current"
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1
$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
$env:DOTNET_MULTILEVEL_LOOKUP = 0
###########################################################################
# EXECUTION
###########################################################################
function ExecSafe([scriptblock] $cmd) {
& $cmd
if ($LASTEXITCODE) { exit $LASTEXITCODE }
}
# If dotnet CLI is installed globally and it matches requested version, use for execution
if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
$(dotnet --version) -and $LASTEXITCODE -eq 0) {
$env:DOTNET_EXE = (Get-Command "dotnet").Path
}
else {
# Download install script
$DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
(New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
# If global.json exists, load expected version
if (Test-Path $DotNetGlobalFile) {
$DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
$DotNetVersion = $DotNetGlobal.sdk.version
}
}
# Install by channel or version
$DotNetDirectory = "$TempDirectory\dotnet-win"
if (!(Test-Path variable:DotNetVersion)) {
ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
} else {
ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
}
$env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
}
Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)"
ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet }
ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }

62
examples/PetStoreApi/build.sh Executable file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env bash
bash --version 2>&1 | head -n 1
set -eo pipefail
SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
###########################################################################
# CONFIGURATION
###########################################################################
BUILD_PROJECT_FILE="$SCRIPT_DIR/../../build/_build.csproj"
TEMP_DIRECTORY="$SCRIPT_DIR/../../.nuke/temp"
DOTNET_GLOBAL_FILE="$SCRIPT_DIR/../../global.json"
DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
DOTNET_CHANNEL="Current"
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
export DOTNET_MULTILEVEL_LOOKUP=0
###########################################################################
# EXECUTION
###########################################################################
function FirstJsonValue {
perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
}
# If dotnet CLI is installed globally and it matches requested version, use for execution
if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
export DOTNET_EXE="$(command -v dotnet)"
else
# Download install script
DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
mkdir -p "$TEMP_DIRECTORY"
curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
chmod +x "$DOTNET_INSTALL_FILE"
# If global.json exists, load expected version
if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
if [[ "$DOTNET_VERSION" == "" ]]; then
unset DOTNET_VERSION
fi
fi
# Install by channel or version
DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
if [[ -z ${DOTNET_VERSION+x} ]]; then
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
else
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
fi
export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
fi
echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)"
"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<Folder Include="Metadata" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,5 @@
namespace Bismarck.AspNetCore;
public interface IEndpointMapper
{
}

View file

@ -0,0 +1,37 @@
using System.Collections.Immutable;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Bismarck.AspNetCore;
public static class IEndpointRegistrationExtensions
{
private static IReadOnlySet<(Type, Type)> _endpointSpecs;
public static void AddEndpoints(this IServiceCollection services, Assembly? assemblyToLoad = null)
{
assemblyToLoad ??= Assembly.GetCallingAssembly();
Type[] exportedTypes = assemblyToLoad.GetExportedTypes();
_endpointSpecs = exportedTypes
.Where(t => t.IsInterface && t.IsAssignableTo(typeof(IEndpointMapper)))
.SelectMany(epSpec =>
{
var firstImplementation = exportedTypes.FirstOrDefault(t => !t.IsInterface && t.IsAssignableTo(epSpec));
return firstImplementation is null
? Enumerable.Empty<(Type, Type)>()
: new[]
{
(epSpec, firstImplementation)
};
})
.ToImmutableHashSet();
foreach ((Type? epSpec, Type? epImpl) in _endpointSpecs)
{
services.TryAddScoped(epSpec, epImpl);
}
}
}

View file

@ -26,12 +26,13 @@
<ItemGroup>
<!-- Generator dependencies -->
<PackageReference Include="Microsoft.OpenApi" Version="1.4.4" GeneratePathProperty="true" PrivateAssets="all" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.4.4" GeneratePathProperty="true" PrivateAssets="all" />
<PackageReference Include="Microsoft.OpenApi" Version="1.4.5" GeneratePathProperty="true" PrivateAssets="all" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.4.5" GeneratePathProperty="true" PrivateAssets="all" />
<PackageReference Include="SharpYaml" Version="2.1.0" GeneratePathProperty="true" PrivateAssets="all" />
<PackageReference Include="Scriban" Version="5.5.1" GeneratePathProperty="true" PrivateAssets="all" />
</ItemGroup>
<PropertyGroup>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>

View file

@ -1,55 +1,56 @@
using System.Diagnostics;
using Bismarck.CodeGenerator.Extensions;
using Bismarck.CodeGenerator.Generators;
using Bismarck.CodeGenerator.Indexers;
using Bismarck.CodeGenerator.Models;
using Bismarck.CodeGenerator.Parsing;
using Microsoft.CodeAnalysis;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
using Scriban;
using static Bismarck.CodeGenerator.Constants;
namespace Bismarck.CodeGenerator;
[Generator]
public class ContractGenerator : ISourceGenerator
{
private static readonly IReadOnlyDictionary<string, string> TemplateNames = new Dictionary<string, string>
{
{
OperationInterface, "templates.OperationInterface.tmplcs"
}
};
private string _rootNamespace;
private readonly IDictionary<string, Template> _templates;
private readonly ModelGenerator _modelGenerator;
private readonly SchemaRegistry _registry;
private readonly OperationInterfaceIndexer _operationInterfaceIndexer;
public ContractGenerator()
{
_rootNamespace = string.Empty;
_templates = new Dictionary<string, Template>();
_modelGenerator = new ModelGenerator();
var generators = new ISchemaGenerator[]
{
new OperationInterfaceGenerator(),
new EnumGenerator(),
new ObjectGenerator()
};
_registry = new SchemaRegistry(generators);
var indexers = new ISchemaIndexer[]
{
new EnumSchemaIndexer(),
new ObjectSchemaIndexer(_registry, new EnumSchemaIndexer())
};
_operationInterfaceIndexer = new OperationInterfaceIndexer(
_registry,
indexers
);
}
public void Initialize(GeneratorInitializationContext context)
{
/*while (!Debugger.IsAttached)
context.RegisterForSyntaxNotifications(() => new EndpointsMapperReceiver());
context.RegisterForPostInitialization(initializationContext =>
{
Thread.Sleep(1000);
}*/
var assembly = typeof(ContractGenerator).Assembly;
foreach (var kv in TemplateNames)
{
using var stream = assembly.GetManifestResourceStream(typeof(ContractGenerator), kv.Value);
using var reader = new StreamReader(stream!);
_templates[kv.Key] = Template.Parse(reader.ReadToEnd());
}
initializationContext.AddSource("MapServiceEndpointsAttribute.g.cs", GetType().LoadRelativeResource("templates", "Marker.tmplcs"));
});
}
public void Execute(GeneratorExecutionContext context)
@ -59,30 +60,32 @@ public class ContractGenerator : ISourceGenerator
throw new ArgumentException("Couldn't retrieve root namespace");
}
if (context.SyntaxReceiver is not EndpointsMapperReceiver syntaxReceiver)
{
return;
}
foreach (var generatorSpec in ReadSpecs(context))
{
_operationInterfaceIndexer.IndexOperations(generatorSpec, _rootNamespace);
}
// INamedTypeSymbol endpointMappers = context.Compilation.GetTypeByMetadataName("Bismarck.AspNetCore.Metadata.MapServiceEndpointsAttribute");
var ctx = new GeneratorContext(context);
foreach ((OpenApiDocument doc, string serviceName) in ReadSpecs(context))
{
_modelGenerator.GenerateModelSchema(ctx, doc.Components.Schemas, _rootNamespace);
foreach (var path in doc.Paths)
{
foreach (var openApiOperation in path.Value.Operations)
{
openApiOperation.Value.OperationId = openApiOperation.Value.OperationId.SanitizeTypeName();
Debug.WriteLine($"Path: {path.Key}");
Debug.WriteLine($"Method: {openApiOperation.Key}");
Debug.WriteLine($"Operation id: {openApiOperation.Value.OperationId}");
_registry.GenerateSources(ctx);
var rendered = _templates[OperationInterface].Render(new
{
Namespace = _rootNamespace,
OperationName = openApiOperation.Value.OperationId
});
var epMapperGenerator = new EndpointsMapperGenerator();
context.AddSource($"I{openApiOperation.Value.OperationId}.g.cs", rendered);
}
}
}
epMapperGenerator.Generate(
ctx,
new EndpointsMapperSpec(
syntaxReceiver.GetNamespaceName(),
syntaxReceiver.ClassName,
_registry.QueryGeneratorSpecs(s => s is OperationSpec).Cast<OperationSpec>()
)
);
}
private IEnumerable<GeneratorSpec> ReadSpecs(GeneratorExecutionContext context)
@ -101,7 +104,7 @@ public class ContractGenerator : ISourceGenerator
document = reader.Read(stream, out _);
}
yield return new GeneratorSpec(document, serviceName);
yield return new GeneratorSpec(serviceName, document);
}
}
}

View file

@ -0,0 +1,47 @@
using Bismarck.CodeGenerator.Models;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
namespace Bismarck.CodeGenerator.Extensions;
internal static class OpenApiSchemaExtensions
{
internal static SchemaType Type(this OpenApiSchema schema, string nodeName, string serviceName)
{
return schema.Type switch
{
"object" => SchemaType.Object(ComplexTypeName(schema, nodeName, serviceName)),
"array" => SchemaType.Array(Type(schema.Items, nodeName, serviceName).Name),
"string" when schema.Enum.Count == 0 => SchemaType.Simple(nameof(String)),
"string" when schema.Enum.Count > 0 => SchemaType.Enum(ComplexTypeName(schema, nodeName, serviceName)),
"boolean" => SchemaType.Simple(nameof(Boolean)),
"integer" => schema.Format switch
{
"int64" => SchemaType.Simple(nameof(Int64)),
_ => SchemaType.Simple(nameof(Int32))
},
"number" => schema.Format switch
{
"float" => SchemaType.Simple(nameof(Single)),
_ => SchemaType.Simple(nameof(Double))
},
_ => SchemaType.Object(nameof(Object))
};
}
private static string ComplexTypeName(OpenApiSchema schema, string nodeName, string serviceName)
{
if (schema.Extensions.TryGetValue($"x-bismarck-type/{serviceName}", out var typeNameExtension) && typeNameExtension is OpenApiString typeNameValue)
{
return typeNameValue.Value;
}
if (schema.Reference is not null && !string.IsNullOrEmpty(schema.Reference.Id))
{
return schema.Reference.Id;
}
return nodeName;
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using Bismarck.CodeGenerator.Models;
using Microsoft.CodeAnalysis;
using Microsoft.OpenApi.Models;
namespace Bismarck.CodeGenerator;

View file

@ -0,0 +1,23 @@
using Bismarck.CodeGenerator.Extensions;
using Bismarck.CodeGenerator.Models;
using Scriban;
namespace Bismarck.CodeGenerator.Generators;
public class EndpointsMapperGenerator : ISchemaGenerator
{
private readonly Template _mapperTemplate = Template.Parse(typeof(ContractGenerator).LoadRelativeResource("templates", "EndpointMapper.tmplcs"));
public SchemaKind SupportedSchemaKind => SchemaKind.None;
public void Generate(GeneratorContext context, IGeneratorSpec spec)
{
if (spec is not EndpointsMapperSpec endpointsMapperSpec)
{
return;
}
context.AddSource(SchemaKind.None, spec.TargetName, _mapperTemplate.Render(endpointsMapperSpec));
}
}

View file

@ -1,85 +0,0 @@
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using Bismarck.CodeGenerator.Extensions;
using Bismarck.CodeGenerator.Indexers;
using Bismarck.CodeGenerator.Models;
using Microsoft.OpenApi.Models;
using Scriban;
namespace Bismarck.CodeGenerator.Generators;
internal class ModelGenerator
{
private readonly Template _modelTemplate;
private readonly IDictionary<SchemaKind, ISchemaIndexer> _schemaIndexers;
public ModelGenerator()
{
_modelTemplate =
Template.Parse(typeof(ContractGenerator).LoadRelativeResource("templates", "SchemaModel.tmplcs"));
_schemaIndexers = new Dictionary<SchemaKind, ISchemaIndexer> { { SchemaKind.Enum, new EnumSchemaIndexer() } };
}
public void GenerateModelSchema(GeneratorContext context, IDictionary<string, OpenApiSchema> schemata,
string namespaceName)
{
foreach ((string? key, OpenApiSchema? schema) in schemata)
{
var type = Type(schema, key);
var spec = new ModelSpec(namespaceName, key.SanitizeTypeName());
GenerateProperties(context, spec, new[] { key }, schema);
context.AddSource(type.Kind, spec.TargetName, _modelTemplate.Render(spec));
}
}
private void GenerateProperties(GeneratorContext context, ModelSpec modelSpec, IEnumerable<string> location,
OpenApiSchema schema)
{
var parentName = location.Last();
foreach ((string? key, OpenApiSchema? value) in schema.Properties)
{
var propertyType = Type(value, parentName);
if (_schemaIndexers.TryGetValue(propertyType.Kind, out var indexer))
{
indexer.Index(value, modelSpec.Namespace, key.SanitizeTypeName());
}
switch (propertyType.Kind)
{
case SchemaKind.Simple:
modelSpec.Properties.Add(new PropertySpec(propertyType, key.SanitizeTypeName()));
break;
case SchemaKind.Enum:
modelSpec.Properties.Add(new PropertySpec(propertyType, key.SanitizeTypeName()));
break;
}
}
}
private static PropertyType Type(OpenApiSchema schema, string parentName)
{
return schema.Type switch
{
"object" => PropertyType.Object(parentName),
"array" => PropertyType.Array(parentName),
"string" when schema.Enum.Count == 0 => PropertyType.Simple(nameof(String)),
"string" when schema.Enum.Count > 0 => PropertyType.Enum(parentName),
"boolean" => PropertyType.Simple(nameof(Boolean)),
"integer" => schema.Format switch
{
"int64" => PropertyType.Simple(nameof(Int64)),
_ => PropertyType.Simple(nameof(Int32))
},
"number" => schema.Format switch
{
"float" => PropertyType.Simple(nameof(Single)),
_ => PropertyType.Simple(nameof(Double))
},
_ => PropertyType.Object(nameof(Object))
};
}
}

View file

@ -0,0 +1,23 @@
using Bismarck.CodeGenerator.Extensions;
using Bismarck.CodeGenerator.Models;
using Scriban;
namespace Bismarck.CodeGenerator.Generators;
internal class ObjectGenerator : ISchemaGenerator
{
private readonly Template _modelTemplate = Template.Parse(typeof(ContractGenerator).LoadRelativeResource("templates", "SchemaModel.tmplcs"));
public SchemaKind SupportedSchemaKind => SchemaKind.Object;
public void Generate(GeneratorContext context, IGeneratorSpec spec)
{
if (spec is not ModelSpec modelSpec)
{
return;
}
context.AddSource(SchemaKind.Object, spec.TargetName, _modelTemplate.Render(spec));
}
}

View file

@ -0,0 +1,23 @@
using Bismarck.CodeGenerator.Extensions;
using Bismarck.CodeGenerator.Models;
using Scriban;
namespace Bismarck.CodeGenerator.Generators;
public class OperationInterfaceGenerator : ISchemaGenerator
{
private readonly Template _modelTemplate = Template.Parse(typeof(ContractGenerator).LoadRelativeResource("templates", "OperationInterface.tmplcs"));
public SchemaKind SupportedSchemaKind => SchemaKind.Operation;
public void Generate(GeneratorContext context, IGeneratorSpec spec)
{
if (spec is not OperationSpec opSpec)
{
return;
}
context.AddSource(SchemaKind.Operation, opSpec.TargetName, _modelTemplate.Render(opSpec));
}
}

View file

@ -4,8 +4,20 @@ using Microsoft.OpenApi.Models;
namespace Bismarck.CodeGenerator;
public record IndexRequest(OpenApiSchema Schema, string NodeName, string Namespace, string ServiceName)
{
public OpenApiSchema Schema { get; } = Schema;
public string NodeName { get; } = NodeName;
public string Namespace { get; } = Namespace;
public string ServiceName { get; } = ServiceName;
}
public interface ISchemaIndexer
{
SchemaKind SupportedSchemaKind { get; }
IGeneratorSpec Index(OpenApiSchema schema, string namespaceName, string name);
IGeneratorSpec Index(IndexRequest request);
}

View file

@ -2,7 +2,6 @@ using Bismarck.CodeGenerator.Extensions;
using Bismarck.CodeGenerator.Models;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
namespace Bismarck.CodeGenerator.Indexers;
@ -10,14 +9,20 @@ public class EnumSchemaIndexer : ISchemaIndexer
{
public SchemaKind SupportedSchemaKind => SchemaKind.Enum;
public IGeneratorSpec Index(OpenApiSchema schema, string namespaceName, string name)
=> new EnumSpec(namespaceName, name, MapFromAny(schema.Enum));
public IGeneratorSpec Index(IndexRequest req) => new EnumSpec(
req.Namespace,
req.Schema.Type(req.NodeName, req.ServiceName).Name,
MapFromAny(req.Schema.Enum)
);
private static IEnumerable<string> MapFromAny(IEnumerable<IOpenApiAny> enumValues) => enumValues.SelectMany(v =>
{
if (v is OpenApiString s)
{
return new[] { s.Value.SanitizeTypeName() };
return new[]
{
s.Value.SanitizeTypeName()
};
}
return Enumerable.Empty<string>();

View file

@ -9,10 +9,12 @@ namespace Bismarck.CodeGenerator.Indexers;
public class ObjectSchemaIndexer : ISchemaIndexer
{
private readonly SchemaRegistry _registry;
private readonly IDictionary<SchemaKind, ISchemaIndexer> _schemaIndexers;
public ObjectSchemaIndexer(params ISchemaIndexer[] indexers)
public ObjectSchemaIndexer(SchemaRegistry registry, params ISchemaIndexer[] indexers)
{
_registry = registry;
_schemaIndexers = indexers
.Select(i => (i.SupportedSchemaKind, i))
.ToImmutableDictionary(t => t.SupportedSchemaKind, t => t.i);
@ -20,56 +22,35 @@ public class ObjectSchemaIndexer : ISchemaIndexer
public SchemaKind SupportedSchemaKind => SchemaKind.Object;
public IGeneratorSpec Index(OpenApiSchema schema, string namespaceName, string name)
public IGeneratorSpec Index(IndexRequest req) => new ModelSpec(
req.Namespace,
req.Schema.Type(req.NodeName, req.ServiceName).Name
)
{
throw new NotImplementedException();
}
Properties = GenerateProperties(req)
};
private void GenerateProperties(GeneratorContext context, ModelSpec modelSpec, IEnumerable<string> location,
OpenApiSchema schema)
{
var parentName = location.Last();
foreach ((string? key, OpenApiSchema? value) in schema.Properties)
private IReadOnlyList<PropertySpec> GenerateProperties(IndexRequest req) => req.Schema.Properties
.SelectMany<KeyValuePair<string, OpenApiSchema>, PropertySpec>(pair =>
{
var propertyType = Type(value, parentName);
var schemaType = pair.Value.Type(req.NodeName, req.ServiceName);
if (_schemaIndexers.TryGetValue(propertyType.Kind, out var indexer))
if (_schemaIndexers.TryGetValue(schemaType.Kind, out var indexer))
{
indexer.Index(value, modelSpec.Namespace, key.SanitizeTypeName());
_registry.AddSpec(indexer.Index(new(pair.Value, pair.Key.SanitizeTypeName(), req.Namespace, req.ServiceName)));
}
switch (propertyType.Kind)
return schemaType.Kind switch
{
case SchemaKind.Simple:
modelSpec.Properties.Add(new PropertySpec(propertyType, key.SanitizeTypeName()));
break;
case SchemaKind.Enum:
modelSpec.Properties.Add(new PropertySpec(propertyType, key.SanitizeTypeName()));
break;
}
}
}
private static PropertyType Type(OpenApiSchema schema, string parentName)
{
return schema.Type switch
{
"object" => PropertyType.Object(parentName),
"array" => PropertyType.Array(parentName),
"string" when schema.Enum.Count == 0 => PropertyType.Simple(nameof(String)),
"string" when schema.Enum.Count > 0 => PropertyType.Enum(parentName),
"boolean" => PropertyType.Simple(nameof(Boolean)),
"integer" => schema.Format switch
{
"int64" => PropertyType.Simple(nameof(Int64)),
_ => PropertyType.Simple(nameof(Int32))
},
"number" => schema.Format switch
{
"float" => PropertyType.Simple(nameof(Single)),
_ => PropertyType.Simple(nameof(Double))
},
_ => PropertyType.Object(nameof(Object))
};
}
SchemaKind.Simple => new PropertySpec[]
{
new(schemaType, pair.Key.SanitizeTypeName())
},
SchemaKind.Enum => new PropertySpec[]
{
new(schemaType, pair.Key.SanitizeTypeName())
},
_ => Enumerable.Empty<PropertySpec>()
};
}).ToImmutableList();
}

View file

@ -0,0 +1,72 @@
using System.Collections.Immutable;
using Bismarck.CodeGenerator.Extensions;
using Bismarck.CodeGenerator.Models;
using Microsoft.OpenApi.Models;
namespace Bismarck.CodeGenerator.Indexers;
public class OperationInterfaceIndexer
{
private static readonly SchemaKind[] ComplexKinds = new[]
{
SchemaKind.Enum, SchemaKind.Object
};
private readonly SchemaRegistry _registry;
private readonly IDictionary<SchemaKind, ISchemaIndexer> _schemaIndexers;
public OperationInterfaceIndexer(SchemaRegistry registry, params ISchemaIndexer[] indexers)
{
_registry = registry;
_schemaIndexers = indexers
.Select(i => (Kind: i.SupportedSchemaKind, Indexer: i))
.ToImmutableDictionary(t => t.Kind, t => t.Indexer);
}
public void IndexOperations(GeneratorSpec spec, string rootNamespace)
{
foreach (var path in spec.ApiDocument.Paths)
{
foreach ((OperationType opType, OpenApiOperation? op) in path.Value.Operations)
{
op.OperationId = op.OperationId.SanitizeTypeName();
var parameters = op.Parameters
.Select(p => OperationParameter.FromOpenApiParameter(p, spec.ServiceName))
.Union(op.RequestBody switch
{
null => Enumerable.Empty<OperationParameter>(),
_ => OperationParameter.FromOpenApiBody(op.RequestBody, spec.ServiceName) switch
{
null => Enumerable.Empty<OperationParameter>(),
{ } p => new[]
{
p
}
}
})
.ToArray();
foreach (var parameter in parameters)
{
if (_schemaIndexers.TryGetValue(parameter.Type.Kind, out var indexer))
{
_registry.AddSpec(indexer.Index(new(parameter.Schema, parameter.Name, rootNamespace, spec.ServiceName)));
}
}
var opSpec = new OperationSpec(
rootNamespace,
op.OperationId,
opType,
path.Key,
parameters
);
_registry.AddSpec(opSpec);
}
}
}
}

View file

@ -2,9 +2,8 @@ using Microsoft.OpenApi.Models;
namespace Bismarck.CodeGenerator.Models;
internal record GeneratorSpec(OpenApiDocument ApiDocument, string ServiceName)
public record GeneratorSpec(string ServiceName, OpenApiDocument ApiDocument)
{
public OpenApiDocument ApiDocument { get; } = ApiDocument;
public string ServiceName { get; } = ServiceName;
public OpenApiDocument ApiDocument { get; } = ApiDocument;
}

View file

@ -3,36 +3,41 @@ namespace Bismarck.CodeGenerator.Models;
public interface IGeneratorSpec
{
string TargetName { get; }
string? LookupKey { get; }
string Namespace { get; }
SchemaKind Kind { get; }
}
public abstract record SpecBase(string Namespace, string TargetName, string? LookupKey) : IGeneratorSpec
public abstract record SpecBase(string Namespace, string TargetName) : IGeneratorSpec
{
public string TargetName { get; } = TargetName;
public string? LookupKey { get; } = LookupKey;
public string Namespace { get; } = Namespace;
public abstract SchemaKind Kind { get; }
}
public record PropertySpec(PropertyType Type, string Name)
public record PropertySpec(SchemaType Type, string Name)
{
public PropertyType Type { get; } = Type;
public SchemaType Type { get; } = Type;
public string Name { get; } = Name;
}
internal record ModelSpec(string Namespace, string TargetName, string? LookupKey = null) : SpecBase(Namespace, TargetName,
LookupKey)
internal record ModelSpec(string Namespace, string TargetName) : SpecBase(Namespace, TargetName)
{
public IList<PropertySpec> Properties { get; set; } = new List<PropertySpec>();
public IReadOnlyList<PropertySpec> Properties { get; set; } = new List<PropertySpec>();
public override SchemaKind Kind => SchemaKind.Object;
}
internal record EnumSpec(string Namespace, string TargetName, IEnumerable<string> Values, string? LookupKey = null)
: SpecBase(Namespace, TargetName, LookupKey)
internal record EnumSpec(string Namespace, string TargetName, IEnumerable<string> Values)
: SpecBase(Namespace, TargetName)
{
public IEnumerable<string> Values { get; } = Values;
public override SchemaKind Kind => SchemaKind.Enum;
}
internal record EndpointsMapperSpec(string Namespace, string TargetName, IEnumerable<OperationSpec> Operations)
: SpecBase(Namespace, TargetName)
{
public override SchemaKind Kind => SchemaKind.None;
public IEnumerable<OperationSpec> Operations { get; } = Operations;
}

View file

@ -0,0 +1,88 @@
using System.Net.Mime;
using Bismarck.CodeGenerator.Extensions;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
namespace Bismarck.CodeGenerator.Models;
public enum ParameterLocation
{
Query,
Header,
Path,
Cookie,
Body
}
public record OperationParameter(string Name, bool Required, ParameterLocation Location, SchemaType Type, OpenApiSchema Schema)
{
public string Name { get; } = Name;
public bool Required { get; } = Required;
public ParameterLocation Location { get; } = Location;
public SchemaType Type { get; } = Type;
public OpenApiSchema Schema { get; } = Schema;
public string SourceAttribute => Location switch
{
ParameterLocation.Header => "[FromHeader] ",
ParameterLocation.Query => "[FromQuery] ",
ParameterLocation.Body => "[FromBody] ",
_ => string.Empty
};
public static OperationParameter FromOpenApiParameter(OpenApiParameter p, string serviceName)
=> new(
p.Name,
p.Required,
FromOpenApiParameterLocation(p.In!.Value),
p.Schema.Type(p.Name.SanitizeTypeName(), serviceName),
p.Schema
);
public static OperationParameter? FromOpenApiBody(OpenApiRequestBody body, string serviceName)
{
if (!body.Content.TryGetValue(MediaTypeNames.Application.Json, out var val))
{
return null;
}
return new(
"body",
body.Required,
ParameterLocation.Body,
val.Schema.Type("Body", serviceName),
val.Schema
);
}
private static ParameterLocation FromOpenApiParameterLocation(Microsoft.OpenApi.Models.ParameterLocation l) => l switch
{
Microsoft.OpenApi.Models.ParameterLocation.Query => ParameterLocation.Query,
Microsoft.OpenApi.Models.ParameterLocation.Header => ParameterLocation.Header,
Microsoft.OpenApi.Models.ParameterLocation.Path => ParameterLocation.Path,
Microsoft.OpenApi.Models.ParameterLocation.Cookie => ParameterLocation.Cookie
};
}
public record OperationSpec(
string Namespace,
string TargetName,
OperationType OperationMethod,
string OperationPath,
OperationParameter[] Parameters)
: SpecBase(Namespace, TargetName)
{
public override SchemaKind Kind => SchemaKind.Operation;
public OperationType OperationMethod { get; } = OperationMethod;
public string OperationPath { get; } = OperationPath;
public OperationParameter[] Parameters { get; } = Parameters;
}

View file

@ -2,23 +2,27 @@ namespace Bismarck.CodeGenerator.Models;
public enum SchemaKind
{
None,
Simple,
Object,
Array,
Enum
List,
Enum,
Operation
}
public record PropertyType(string Name, SchemaKind Kind)
public record SchemaType(string Name, SchemaKind Kind)
{
public string Name { get; } = Name;
public SchemaKind Kind { get; } = Kind;
public static PropertyType Simple(string name) => new(name, SchemaKind.Simple);
public static SchemaType Simple(string name) => new(name, SchemaKind.Simple);
public static PropertyType Enum(string name) => new(name, SchemaKind.Enum);
public static SchemaType Enum(string name) => new(name, SchemaKind.Enum);
public static PropertyType Object(string name) => new(name, SchemaKind.Object);
public static SchemaType Object(string name) => new(name, SchemaKind.Object);
public static PropertyType Array(string name) => new($"IEnumerable<{name}>", SchemaKind.Array);
public static SchemaType Array(string name) => new($"{name}[]", SchemaKind.Array);
public static SchemaType List(string name) => new($"IEnumerable<{name}>", SchemaKind.List);
}

View file

@ -0,0 +1,40 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Bismarck.CodeGenerator.Parsing;
internal class EndpointsMapperReceiver : ISyntaxReceiver
{
private ClassDeclarationSyntax _classToAugment;
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
// Business logic to decide what we're interested in goes here
if (syntaxNode is ClassDeclarationSyntax cds && cds.AttributeLists.Any(a1 => a1.Attributes.Any(attr => attr.Name is IdentifierNameSyntax { Identifier.Text: "MapServiceEndpoints" })))
{
_classToAugment = cds;
}
}
public string ClassName => _classToAugment.Identifier.Text;
public string GetNamespaceName()
{
SyntaxNode current = _classToAugment;
while (true)
{
var parent = current.Parent;
switch (parent)
{
case FileScopedNamespaceDeclarationSyntax s:
return s.Name.ToString();
case NamespaceDeclarationSyntax s:
return s.Name.ToString();
case null:
return string.Empty;
}
current = parent;
}
}
}

View file

@ -0,0 +1,9 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"Generators": {
"commandName": "DebugRoslynComponent",
"targetProject": "../../examples/PetStoreApi/PetStoreApi.csproj"
}
}
}

View file

@ -1,27 +1,28 @@
using System.Collections.Immutable;
using Bismarck.CodeGenerator.Generators;
using Bismarck.CodeGenerator.Models;
namespace Bismarck.CodeGenerator;
public class SchemaRegistry
{
private readonly IDictionary<(SchemaKind, string), IGeneratorSpec> _registeredSpecs;
private readonly IDictionary<(SchemaKind, string), IGeneratorSpec> _schemaSpecs;
private readonly IDictionary<SchemaKind, ISchemaGenerator> _generators;
public SchemaRegistry(params ISchemaGenerator[] generators)
{
_generators = generators
.ToImmutableDictionary(g => g.SupportedSchemaKind, g => g);
_registeredSpecs = new Dictionary<(SchemaKind, string), IGeneratorSpec>();
_schemaSpecs = new Dictionary<(SchemaKind, string), IGeneratorSpec>();
}
public bool AddSpec(IGeneratorSpec spec) => _registeredSpecs.TryAdd((spec.Kind, spec.TargetName), spec);
public bool AddSpec(IGeneratorSpec spec) => _schemaSpecs.TryAdd((spec.Kind, spec.TargetName), spec);
public IEnumerable<IGeneratorSpec> QueryGeneratorSpecs(Func<IGeneratorSpec, bool> filter) => _schemaSpecs.Values.Where(filter);
public void GenerateSources(GeneratorContext context)
{
var specsByKind = _registeredSpecs.Values
var specsByKind = _schemaSpecs.Values
.GroupBy(s => s.Kind)
.ToImmutableDictionary(grp => grp.Key, grp => grp);
@ -33,11 +34,13 @@ public class SchemaRegistry
}
var generator = _generators[kindToGenerate];
foreach (var generatorSpec in specs)
{
generator.Generate(context, generatorSpec);
}
}
}
}

View file

@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using System.Threading;
namespace {{ namespace }};
public static partial class {{ target_name }}
{
public static void MapEndpoints<T>(this T app) where T : IEndpointRouteBuilder, IApplicationBuilder
{
using var scope = app.ApplicationServices.CreateScope();
{{- for operation in operations }}
if (scope.ServiceProvider.GetService<I{{ operation.target_name }}>() is not null)
{
{{- if operation.parameters.size < 1 }}
app.Map{{ operation.operation_method }}("{{ operation.operation_path }}", async (I{{ operation.target_name }} ep, CancellationToken token) => await ep.On{{ operation.target_name }}Async(token));
{{ else }}
app.Map{{ operation.operation_method }}("{{ operation.operation_path }}", async (I{{ operation.target_name }} ep,{{- for $i in 1..operation.parameters.size }} {{ operation.parameters[$i-1].type.name }} {{ operation.parameters[$i-1].name }},{{ end }} CancellationToken token) => await ep.On{{ operation.target_name }}Async({{- for $i in 1..operation.parameters.size }}{{ operation.parameters[$i-1].name }}, {{ end }}token));
{{- end }}
}
{{- end }}
}
}

View file

@ -0,0 +1,12 @@
namespace Bismarck.AspNetCore.Metadata;
[AttributeUsage(AttributeTargets.Class)]
internal class MapServiceEndpointsAttribute : Attribute
{
public MapServiceEndpointsAttribute(params string[] services)
{
Services = services;
}
public string[] Services { get; }
}

View file

@ -1,8 +1,19 @@
using Microsoft.AspNetCore.Mvc;
using Bismarck.AspNetCore;
using System.Threading;
namespace {{ namespace }};
public interface I{{ operation_name }}
public interface I{{ target_name }} : IEndpointMapper
{
Task<IActionResult> On{{ operation_name }}Async();
{{- if parameters.size < 1 }}
Task<IResult> On{{ target_name }}Async(CancellationToken token = default);
{{- else }}
Task<IResult> On{{ target_name }}Async(
{{- for $i in 1..parameters.size }}
{{ parameters[$i-1].type.name }} {{ parameters[$i-1].name }},
{{- end }}
CancellationToken token = default
);
{{- end }}
}

View file

@ -0,0 +1,7 @@
namespace Bismarck.Metadata.Attributes;
[AttributeUsage(AttributeTargets.Interface)]
public class OperationEndpointAttribute : Attribute
{
}

View file

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>