From 3e3845d4143c771f6605d30756196d6de1e39566 Mon Sep 17 00:00:00 2001 From: Alex Macocian Date: Mon, 23 Sep 2024 13:38:20 +0200 Subject: [PATCH] Setup route map source generator --- .github/workflows/cd.yaml | 7 + .../Net.Sdk.Web.Extensions.Azure.csproj | 10 +- .../Builder.cs | 10 + .../EndpointRouteBuilder.cs | 13 ++ .../Models/SimpleRequest.cs | 5 + ...b.Extensions.SourceGenerators.Tests.csproj | 19 ++ .../Routes/SimpleRoute.cs | 17 ++ .../Constants.cs | 75 +++++++ .../MapEndpointGenerator.cs | 205 ++++++++++++++++++ ...Sdk.Web.Extensions.SourceGenerators.csproj | 32 +++ .../ResponseGenerator.cs | 121 +++++++++++ .../RouteAttributeGenerator.cs | 47 ++++ .../RouteInterfaceGenerator.cs | 43 ++++ Net.Sdk.Web.Extensions.sln | 14 +- .../Net.Sdk.Web.Extensions.csproj | 10 +- 15 files changed, 618 insertions(+), 10 deletions(-) create mode 100644 Net.Sdk.Web.Extensions.SourceGenerators.Tests/Builder.cs create mode 100644 Net.Sdk.Web.Extensions.SourceGenerators.Tests/EndpointRouteBuilder.cs create mode 100644 Net.Sdk.Web.Extensions.SourceGenerators.Tests/Models/SimpleRequest.cs create mode 100644 Net.Sdk.Web.Extensions.SourceGenerators.Tests/Net.Sdk.Web.Extensions.SourceGenerators.Tests.csproj create mode 100644 Net.Sdk.Web.Extensions.SourceGenerators.Tests/Routes/SimpleRoute.cs create mode 100644 Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs create mode 100644 Net.Sdk.Web.Extensions.SourceGenerators/MapEndpointGenerator.cs create mode 100644 Net.Sdk.Web.Extensions.SourceGenerators/Net.Sdk.Web.Extensions.SourceGenerators.csproj create mode 100644 Net.Sdk.Web.Extensions.SourceGenerators/ResponseGenerator.cs create mode 100644 Net.Sdk.Web.Extensions.SourceGenerators/RouteAttributeGenerator.cs create mode 100644 Net.Sdk.Web.Extensions.SourceGenerators/RouteInterfaceGenerator.cs diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 69d4811..90b8fed 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -24,6 +24,7 @@ jobs: Solution_Path: Net.Sdk.Web.Extensions.sln Source_Project_Path: Net.Sdk.Web.Extensions\Net.Sdk.Web.Extensions.csproj Azure_Project_path: Net.Sdk.Web.Extensions.Azure\Net.Sdk.Web.Extensions.Azure.csproj + SourceGenerators_Project_path: Net.Sdk.Web.Extensions.SourceGenerators\Net.Sdk.Web.Extensions.SourceGenerators.csproj Actions_Allow_Unsecure_Commands: true steps: @@ -57,5 +58,11 @@ jobs: - name: Package Net.Sdk.Extensions.Azure run: dotnet pack -c Release -o . $env:Azure_Project_Path + - name: Build Net.Sdk.Extensions.SourceGenerators project + run: dotnet build $env:SourceGenerators_Project_path -c $env:Configuration + + - name: Package Net.Sdk.Extensions.SourceGenerators + run: dotnet pack -c Release -o . $env:SourceGenerators_Project_path + - name: Publish run: dotnet nuget push *.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate \ No newline at end of file diff --git a/Net.Sdk.Web.Extensions.Azure/Net.Sdk.Web.Extensions.Azure.csproj b/Net.Sdk.Web.Extensions.Azure/Net.Sdk.Web.Extensions.Azure.csproj index d4ca07c..3145016 100644 --- a/Net.Sdk.Web.Extensions.Azure/Net.Sdk.Web.Extensions.Azure.csproj +++ b/Net.Sdk.Web.Extensions.Azure/Net.Sdk.Web.Extensions.Azure.csproj @@ -6,7 +6,7 @@ enable Library true - 0.7 + 0.8 Alexandru Macocian LICENSE true @@ -23,11 +23,11 @@ - - + + - - + + diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Builder.cs b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Builder.cs new file mode 100644 index 0000000..104ba4e --- /dev/null +++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Builder.cs @@ -0,0 +1,10 @@ +namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests; + +public static class Builder +{ + public static void Build() + { + EndpointRouteBuilder builder = new EndpointRouteBuilder(); + builder.MapDeleteSimpleRoute(); + } +} diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/EndpointRouteBuilder.cs b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/EndpointRouteBuilder.cs new file mode 100644 index 0000000..2da8714 --- /dev/null +++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/EndpointRouteBuilder.cs @@ -0,0 +1,13 @@ + +namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests; + +public class EndpointRouteBuilder : IEndpointRouteBuilder +{ + public IServiceProvider ServiceProvider { get; } + public ICollection DataSources { get; } + + public IApplicationBuilder CreateApplicationBuilder() + { + throw new NotImplementedException(); + } +} diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Models/SimpleRequest.cs b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Models/SimpleRequest.cs new file mode 100644 index 0000000..59d905b --- /dev/null +++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Models/SimpleRequest.cs @@ -0,0 +1,5 @@ +namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests.Models; + +public class SimpleRequest +{ +} diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Net.Sdk.Web.Extensions.SourceGenerators.Tests.csproj b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Net.Sdk.Web.Extensions.SourceGenerators.Tests.csproj new file mode 100644 index 0000000..45aad04 --- /dev/null +++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Net.Sdk.Web.Extensions.SourceGenerators.Tests.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + Library + latest + true + true + $(BaseIntermediateOutputPath)\Generated + + + + + + + + diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Routes/SimpleRoute.cs b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Routes/SimpleRoute.cs new file mode 100644 index 0000000..b0ec87d --- /dev/null +++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Routes/SimpleRoute.cs @@ -0,0 +1,17 @@ +using Net.Sdk.Web.Extensions.SourceGenerators.Tests.Models; + +namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests.Routes; + +[GenerateMapDelete(Pattern = "simple")] +public sealed class SimpleRoute : IRoute +{ + public Task HandleRequest(SimpleRequest? request, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task PreProcess(HttpContext? context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs b/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs new file mode 100644 index 0000000..da6d409 --- /dev/null +++ b/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs @@ -0,0 +1,75 @@ +namespace Net.Sdk.Web.Extensions.SourceGenerators; + +public static class Constants +{ + public const string Partial = "partial"; + public const string Public = "public"; + public const string Abstract = "abstract"; + public const string Sealed = "sealed"; + public const string Static = "static"; + public const string StringType = "string"; + public const string IntType = "int"; + public const string ObjectType = "object"; + public const string Default = "default"; + public const string Namespace = "Net.Sdk.Web"; + public const string UsingSystemThreading = "System.Threading"; + public const string UsingMicrosoftAspNetCoreHttp = "Microsoft.AspNetCore.Http"; + public const string UsingMicrosoftAspNetCoreRouting = "Microsoft.AspNetCore.Routing"; + public const string UsingMicrosoftAspNetCoreBuilder = "Microsoft.AspNetCore.Builder"; + + public const string RouteFileName = "Route"; + public const string RouteInterface = "IRoute"; + public const string RouteTypeParameter = "TRequest"; + public const string PreProcessRequestReturnType = "Task"; + public const string PreProcessRequestMethodName = "PreProcess"; + public const string HandleRequestReturnType = "Task"; + public const string HandleRequestMethodName = "HandleRequest"; + public const string HttpContextTypeName = "HttpContext?"; + public const string HttpContextParameterName = "context"; + public const string RequestParameterName = "request"; + + public const string ResponseTypeName = "Response"; + public const string IResultTypeName = "IResult"; + public const string SuccessResponseTypeName = "SuccessResponse"; + public const string FailureResponseTypeName = "FailureResponse"; + public const string ContentResponseTypeName = "ContentResponse"; + public const string JsonResponseTypeName = "JsonResponse"; + public const string StatusCodePropertyName = "StatusCode"; + public const string ErrorMessagePropertyName = "ErrorMessage"; + public const string ContentStringPropertyName = "ContentString"; + public const string ValuePropertyName = "Value"; + public const string ContentTypePropertyName = "ContentType"; + public const string OkSuccessResponse = "Ok"; + public const string CreatedSuccessResponse = "Created"; + public const string ContentContentResponse = "Content"; + public const string JsonJsonResponse = "Json"; + public const string PageContentResponse = "Page"; + public const string BadRequestFailureResponse = "BadRequest"; + public const string UnauthorizedFailureResponse = "Unauthorized"; + public const string ForbiddenFailureResponse = "Forbidden"; + public const string NotFoundFailureResponse = "NotFound"; + public const string ServerErrorFailureResponse = "ServerError"; + public const string GetResult = "GetResult"; + public const string ContentParameterName = "content"; + public const string ContentTypeParameterName = "contentType"; + public const string ValueParameterName = "value"; + public const string ErrorMessageParameterName = "errorMessage"; + + public const string CancellationTokenTypeName = "CancellationToken"; + public const string CancellationTokenParameterName = "cancellationToken"; + + public const string GetAttributeName = "GenerateMapGetAttribute"; + public const string GetAttributeShortName = "GenerateMapGet"; + public const string PostAttributeName = "GenerateMapPostAttribute"; + public const string PostAttributeShortName = "GenerateMapPost"; + public const string PutAttributeName = "GenerateMapPutAttribute"; + public const string PutAttributeShortName = "GenerateMapPut"; + public const string DeleteAttributeName = "GenerateMapDeleteAttribute"; + public const string DeleteAttributeShortName = "GenerateMapDelete"; + + public const string PatternPropertyName = "Pattern"; + + public const string EndpointRouteBuilderExtensionsName = "EndpointRouteBuilderExtensions"; + public const string IEndpointRouteBuilderTypeName = "IEndpointRouteBuilder"; + public const string BuilderParameterName = "builder"; +} diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/MapEndpointGenerator.cs b/Net.Sdk.Web.Extensions.SourceGenerators/MapEndpointGenerator.cs new file mode 100644 index 0000000..d03aed4 --- /dev/null +++ b/Net.Sdk.Web.Extensions.SourceGenerators/MapEndpointGenerator.cs @@ -0,0 +1,205 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Sybil; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Net.Sdk.Web.Extensions.SourceGenerators; + +[Generator(LanguageNames.CSharp)] +#nullable enable +public class MapEndpointGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var classDeclarations = context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (s, _) => s is ClassDeclarationSyntax, + transform: static (ctx, _) => GetFilteredClassDeclarationSyntax(ctx)).Where(static c => c is not null); + var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect()); + context.RegisterSourceOutput(compilationAndClasses, (sourceProductionContext, tuple) => Execute(tuple.Left, tuple.Right, sourceProductionContext)); + } + + private static ClassDeclarationSyntax? GetFilteredClassDeclarationSyntax(GeneratorSyntaxContext context) + { + var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node; + if (classDeclarationSyntax.AttributeLists + .SelectMany(l => l.Attributes) + .OfType() + .Any(s => s.Name.ToString() == Constants.GetAttributeName || s.Name.ToString() == Constants.GetAttributeShortName || + s.Name.ToString() == Constants.PostAttributeName || s.Name.ToString() == Constants.PostAttributeShortName || + s.Name.ToString() == Constants.PutAttributeName || s.Name.ToString() == Constants.PutAttributeShortName || + s.Name.ToString() == Constants.DeleteAttributeName || s.Name.ToString() == Constants.DeleteAttributeShortName)) + { + return classDeclarationSyntax; + } + + return default; + } + + private static void Execute(Compilation compilation, ImmutableArray classes, SourceProductionContext sourceProductionContext) + { + if (classes.IsDefaultOrEmpty) + { + return; + } + + var maybeLanguageVersion = (compilation.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions)?.LanguageVersion; + if (!maybeLanguageVersion.HasValue) + { + return; + } + + var languageVersion = maybeLanguageVersion.Value; + var builder = SyntaxBuilder.CreateCompilationUnit() + .WithUsing(Constants.UsingSystemThreading) + .WithUsing(Constants.UsingMicrosoftAspNetCoreRouting) + .WithUsing(Constants.UsingMicrosoftAspNetCoreHttp) + .WithUsing(Constants.UsingMicrosoftAspNetCoreBuilder); + + var routeUsings = new HashSet(); + + var namespaceBuilder = languageVersion >= LanguageVersion.CSharp10 ? SyntaxBuilder.CreateFileScopedNamespace(Constants.Namespace) : SyntaxBuilder.CreateNamespace(Constants.Namespace); + builder.WithNamespace(namespaceBuilder); + + var extensionClassBuilder = SyntaxBuilder.CreateClass(Constants.EndpointRouteBuilderExtensionsName) + .WithModifiers($"{Constants.Public} {Constants.Static}"); + namespaceBuilder.WithClass(extensionClassBuilder); + + foreach(var classDeclarationSyntax in classes) + { + if (classDeclarationSyntax is null) + { + continue; + } + + if (GetParentOfType(classDeclarationSyntax) is BaseNamespaceDeclarationSyntax baseNamespaceDeclarationSyntax) + { + routeUsings.Add(baseNamespaceDeclarationSyntax.Name.ToString()); + } + + var attributes = classDeclarationSyntax.AttributeLists + .SelectMany(l => l.Attributes) + .OfType().ToList(); + + if (attributes.FirstOrDefault(a => a.Name.ToString() == Constants.GetAttributeName || a.Name.ToString() == Constants.GetAttributeShortName) is AttributeSyntax getAttributeSyntax) + { + var methodBuilder = SyntaxBuilder.CreateMethod(Constants.IEndpointRouteBuilderTypeName, $"MapGet{classDeclarationSyntax.Identifier}") + .WithThisParameter(Constants.IEndpointRouteBuilderTypeName, Constants.BuilderParameterName) + .WithModifiers($"{Constants.Public} {Constants.Static}"); + + var pattern = getAttributeSyntax.ArgumentList?.Arguments.OfType().FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == Constants.PatternPropertyName)?.Expression.ToString().Trim('"'); + if (pattern is null) + { + continue; + } + + methodBuilder.WithBody(@$" + builder.MapGet(""{pattern}"", async (HttpContext context, {classDeclarationSyntax.Identifier} route) => + {{ + var request = await route.PreProcess(context, context.RequestAborted); + var response = await route.HandleRequest(request, context.RequestAborted); + return response.GetResult(); + }}); + + return builder;"); + extensionClassBuilder.WithMethod(methodBuilder); + } + else if (attributes.FirstOrDefault(a => a.Name.ToString() == Constants.PostAttributeName || a.Name.ToString() == Constants.PostAttributeShortName) is AttributeSyntax postAttributeSyntax) + { + var methodBuilder = SyntaxBuilder.CreateMethod(Constants.IEndpointRouteBuilderTypeName, $"MapPost{classDeclarationSyntax.Identifier}") + .WithThisParameter(Constants.IEndpointRouteBuilderTypeName, Constants.BuilderParameterName) + .WithModifiers($"{Constants.Public} {Constants.Static}"); + + var pattern = postAttributeSyntax.ArgumentList?.Arguments.OfType().FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == Constants.PatternPropertyName)?.Expression.ToString().Trim('"'); + if (pattern is null) + { + continue; + } + + methodBuilder.WithBody(@$" + builder.MapPost(""{pattern}"", async (HttpContext context, {classDeclarationSyntax.Identifier} route) => + {{ + var request = await route.PreProcess(context, context.RequestAborted); + var response = await route.HandleRequest(request, context.RequestAborted); + return response.GetResult(); + }}); + + return builder;"); + extensionClassBuilder.WithMethod(methodBuilder); + } + else if (attributes.FirstOrDefault(a => a.Name.ToString() == Constants.PutAttributeName || a.Name.ToString() == Constants.PutAttributeShortName) is AttributeSyntax putAttributeSyntax) + { + var methodBuilder = SyntaxBuilder.CreateMethod(Constants.IEndpointRouteBuilderTypeName, $"MapPut{classDeclarationSyntax.Identifier}") + .WithThisParameter(Constants.IEndpointRouteBuilderTypeName, Constants.BuilderParameterName) + .WithModifiers($"{Constants.Public} {Constants.Static}"); + + var pattern = putAttributeSyntax.ArgumentList?.Arguments.OfType().FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == Constants.PatternPropertyName)?.Expression.ToString().Trim('"'); + if (pattern is null) + { + continue; + } + + methodBuilder.WithBody(@$" + builder.MapPut(""{pattern}"", async (HttpContext context, {classDeclarationSyntax.Identifier} route) => + {{ + var request = await route.PreProcess(context, context.RequestAborted); + var response = await route.HandleRequest(request, context.RequestAborted); + return response.GetResult(); + }}); + + return builder;"); + extensionClassBuilder.WithMethod(methodBuilder); + } + else if (attributes.FirstOrDefault(a => a.Name.ToString() == Constants.DeleteAttributeName || a.Name.ToString() == Constants.DeleteAttributeShortName) is AttributeSyntax deleteAttributeSyntax) + { + var methodBuilder = SyntaxBuilder.CreateMethod(Constants.IEndpointRouteBuilderTypeName, $"MapDelete{classDeclarationSyntax.Identifier}") + .WithThisParameter(Constants.IEndpointRouteBuilderTypeName, Constants.BuilderParameterName) + .WithModifiers($"{Constants.Public} {Constants.Static}"); + + var pattern = deleteAttributeSyntax.ArgumentList?.Arguments.OfType().FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == Constants.PatternPropertyName)?.Expression.ToString().Trim('"'); + if (pattern is null) + { + continue; + } + + methodBuilder.WithBody(@$" + builder.MapDelete(""{pattern}"", async (HttpContext context, {classDeclarationSyntax.Identifier} route) => + {{ + var request = await route.PreProcess(context, context.RequestAborted); + var response = await route.HandleRequest(request, context.RequestAborted); + return response.GetResult(); + }}); + + return builder;"); + extensionClassBuilder.WithMethod(methodBuilder); + } + } + + foreach(var classUsing in routeUsings) + { + builder.WithUsing(classUsing); + } + + var fileSource = builder.Build().ToFullString(); + sourceProductionContext.AddSource($"{Constants.EndpointRouteBuilderExtensionsName}.g", fileSource); + } + + private static T? GetParentOfType(SyntaxNode syntaxNode) + { + if (syntaxNode.Parent is null) + { + return default; + } + + if (syntaxNode.Parent is T parentNode) + { + return parentNode; + } + + return GetParentOfType(syntaxNode.Parent); + } +} + +#nullable disable \ No newline at end of file diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/Net.Sdk.Web.Extensions.SourceGenerators.csproj b/Net.Sdk.Web.Extensions.SourceGenerators/Net.Sdk.Web.Extensions.SourceGenerators.csproj new file mode 100644 index 0000000..b2dd7d7 --- /dev/null +++ b/Net.Sdk.Web.Extensions.SourceGenerators/Net.Sdk.Web.Extensions.SourceGenerators.csproj @@ -0,0 +1,32 @@ + + + + netstandard2.0 + 0.8 + latest + true + Alexandru Macocian + Source generators extensions for Net.Sdk.Web. + true + true + true + true + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/ResponseGenerator.cs b/Net.Sdk.Web.Extensions.SourceGenerators/ResponseGenerator.cs new file mode 100644 index 0000000..5f7c9a8 --- /dev/null +++ b/Net.Sdk.Web.Extensions.SourceGenerators/ResponseGenerator.cs @@ -0,0 +1,121 @@ +using Microsoft.CodeAnalysis; +using Sybil; + +namespace Net.Sdk.Web.Extensions.SourceGenerators; + +[Generator(LanguageNames.CSharp)] +#nullable enable +public class ResponseGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(context => + { + GenerateResponse(context); + }); + } + + private static void GenerateResponse(IncrementalGeneratorPostInitializationContext context) + { + var responseBaseClassBuilder = SyntaxBuilder.CreateCompilationUnit() + .WithUsing(Constants.UsingMicrosoftAspNetCoreHttp) + .WithNamespace(SyntaxBuilder.CreateNamespace(Constants.Namespace) + .WithClass(SyntaxBuilder.CreateClass(Constants.ResponseTypeName) + .WithModifiers($"{Constants.Public} {Constants.Abstract}") + .WithMethod(SyntaxBuilder.CreateMethod(Constants.IResultTypeName, Constants.GetResult) + .WithModifier(Constants.Public) + .WithBody( + @"return this switch + { + SuccessResponse success => Results.StatusCode(success.StatusCode), + FailureResponse failure => Results.Problem( + detail: string.IsNullOrWhiteSpace(failure.ErrorMessage) is false ? failure.ErrorMessage : default, + statusCode: failure.StatusCode), + ContentResponse content => Results.Content(content.ContentString, content.ContentType), + JsonResponse json => Results.Ok(json.Value), + _ => throw new InvalidOperationException($""Unexpected response type {this.GetType().Name}"") + };")) + .WithMethod(SyntaxBuilder.CreateMethod(Constants.SuccessResponseTypeName, Constants.OkSuccessResponse) + .WithModifiers($"{Constants.Public} {Constants.Static}") + .WithExpression("new() { StatusCode = 200 };")) + .WithMethod(SyntaxBuilder.CreateMethod(Constants.SuccessResponseTypeName, Constants.CreatedSuccessResponse) + .WithModifiers($"{Constants.Public} {Constants.Static}") + .WithExpression("new() { StatusCode = 201 };")) + .WithMethod(SyntaxBuilder.CreateMethod(Constants.ContentResponseTypeName, Constants.ContentContentResponse) + .WithModifiers($"{Constants.Public} {Constants.Static}") + .WithParameter(Constants.StringType, Constants.ContentParameterName) + .WithParameter($"{Constants.StringType}?", Constants.ContentTypeParameterName, Constants.Default) + .WithExpression("new() { ContentString = content, ContentType = contentType };")) + .WithMethod(SyntaxBuilder.CreateMethod(Constants.JsonResponseTypeName, Constants.JsonJsonResponse) + .WithModifiers($"{Constants.Public} {Constants.Static}") + .WithParameter(Constants.ObjectType, Constants.ValueParameterName) + .WithExpression("new() { Value = value };")) + .WithMethod(SyntaxBuilder.CreateMethod(Constants.ContentResponseTypeName, Constants.PageContentResponse) + .WithModifiers($"{Constants.Public} {Constants.Static}") + .WithParameter(Constants.StringType, Constants.ContentParameterName) + .WithExpression(@"new() { ContentString = content, ContentType = ""text/html"" };")) + .WithMethod(SyntaxBuilder.CreateMethod(Constants.FailureResponseTypeName, Constants.BadRequestFailureResponse) + .WithModifiers($"{Constants.Public} {Constants.Static}") + .WithParameter(Constants.StringType, Constants.ErrorMessageParameterName) + .WithExpression(@"new() { StatusCode = 400, ErrorMessage = errorMessage };")) + .WithMethod(SyntaxBuilder.CreateMethod(Constants.FailureResponseTypeName, Constants.UnauthorizedFailureResponse) + .WithModifiers($"{Constants.Public} {Constants.Static}") + .WithParameter(Constants.StringType, Constants.ErrorMessageParameterName) + .WithExpression(@"new() { StatusCode = 401, ErrorMessage = errorMessage };")) + .WithMethod(SyntaxBuilder.CreateMethod(Constants.FailureResponseTypeName, Constants.ForbiddenFailureResponse) + .WithModifiers($"{Constants.Public} {Constants.Static}") + .WithParameter(Constants.StringType, Constants.ErrorMessageParameterName) + .WithExpression(@"new() { StatusCode = 403, ErrorMessage = errorMessage };")) + .WithMethod(SyntaxBuilder.CreateMethod(Constants.FailureResponseTypeName, Constants.NotFoundFailureResponse) + .WithModifiers($"{Constants.Public} {Constants.Static}") + .WithParameter(Constants.StringType, Constants.ErrorMessageParameterName) + .WithExpression(@"new() { StatusCode = 404, ErrorMessage = errorMessage };")) + .WithMethod(SyntaxBuilder.CreateMethod(Constants.FailureResponseTypeName, Constants.ServerErrorFailureResponse) + .WithModifiers($"{Constants.Public} {Constants.Static}") + .WithParameter(Constants.StringType, Constants.ErrorMessageParameterName) + .WithExpression(@"new() { StatusCode = 500, ErrorMessage = errorMessage };"))) + .WithClass(SyntaxBuilder.CreateClass(Constants.SuccessResponseTypeName) + .WithModifiers($"{Constants.Public} {Constants.Sealed}") + .WithBaseClass(Constants.ResponseTypeName) + .WithProperty(SyntaxBuilder.CreateProperty(Constants.IntType, Constants.StatusCodePropertyName) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter()) + .WithAccessor(SyntaxBuilder.CreateSetter()))) + .WithClass(SyntaxBuilder.CreateClass(Constants.FailureResponseTypeName) + .WithModifiers($"{Constants.Public} {Constants.Sealed}") + .WithBaseClass(Constants.ResponseTypeName) + .WithProperty(SyntaxBuilder.CreateProperty(Constants.IntType, Constants.StatusCodePropertyName) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter()) + .WithAccessor(SyntaxBuilder.CreateSetter())) + .WithProperty(SyntaxBuilder.CreateProperty($"{Constants.StringType}?", Constants.ErrorMessagePropertyName) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter()) + .WithAccessor(SyntaxBuilder.CreateSetter()))) + .WithClass(SyntaxBuilder.CreateClass(Constants.ContentResponseTypeName) + .WithModifiers($"{Constants.Public} {Constants.Sealed}") + .WithBaseClass(Constants.ResponseTypeName) + .WithProperty(SyntaxBuilder.CreateProperty($"{Constants.StringType}?", Constants.ContentStringPropertyName) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter()) + .WithAccessor(SyntaxBuilder.CreateSetter())) + .WithProperty(SyntaxBuilder.CreateProperty($"{Constants.StringType}?", Constants.ContentTypePropertyName) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter()) + .WithAccessor(SyntaxBuilder.CreateSetter()))) + .WithClass(SyntaxBuilder.CreateClass(Constants.JsonResponseTypeName) + .WithModifiers($"{Constants.Public} {Constants.Sealed}") + .WithBaseClass(Constants.ResponseTypeName) + .WithProperty(SyntaxBuilder.CreateProperty($"{Constants.ObjectType}?", Constants.ValuePropertyName) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter()) + .WithAccessor(SyntaxBuilder.CreateSetter()))) + ); + + var syntax = responseBaseClassBuilder.Build(); + var source = syntax.ToFullString(); + context.AddSource($"Response.g", $"#nullable enable\n{source}\n#nullable disable\n"); + } +} + +#nullable disable \ No newline at end of file diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/RouteAttributeGenerator.cs b/Net.Sdk.Web.Extensions.SourceGenerators/RouteAttributeGenerator.cs new file mode 100644 index 0000000..0c9a4c1 --- /dev/null +++ b/Net.Sdk.Web.Extensions.SourceGenerators/RouteAttributeGenerator.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.CodeAnalysis; +using Sybil; + +namespace Net.Sdk.Web.Extensions.SourceGenerators; + +[Generator(LanguageNames.CSharp)] +#nullable enable +public class RouteAttributeGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(context => + { + GenerateMapAttribute(context, Constants.GetAttributeName); + GenerateMapAttribute(context, Constants.PostAttributeName); + GenerateMapAttribute(context, Constants.PutAttributeName); + GenerateMapAttribute(context, Constants.DeleteAttributeName); + }); + } + + private static void GenerateMapAttribute(IncrementalGeneratorPostInitializationContext context, string attributeName) + { + var builder = SyntaxBuilder.CreateCompilationUnit() + .WithNamespace( + SyntaxBuilder.CreateNamespace(Constants.Namespace) + .WithClass(SyntaxBuilder.CreateClass(attributeName) + .WithModifier(Constants.Public) + .WithConstructor(SyntaxBuilder.CreateConstructor(attributeName) + .WithModifier(Constants.Public)) + .WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage") + .WithArgument(AttributeTargets.Class) + .WithArgument("Inherited", false) + .WithArgument("AllowMultiple", false)) + .WithBaseClass(nameof(Attribute)) + .WithProperty(SyntaxBuilder.CreateProperty(Constants.StringType, Constants.PatternPropertyName) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter()) + .WithAccessor(SyntaxBuilder.CreateSetter())))); + + var syntax = builder.Build(); + var source = syntax.ToFullString(); + context.AddSource($"{attributeName}.g", source); + } +} + +#nullable disable \ No newline at end of file diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/RouteInterfaceGenerator.cs b/Net.Sdk.Web.Extensions.SourceGenerators/RouteInterfaceGenerator.cs new file mode 100644 index 0000000..6ba08f6 --- /dev/null +++ b/Net.Sdk.Web.Extensions.SourceGenerators/RouteInterfaceGenerator.cs @@ -0,0 +1,43 @@ +using Microsoft.CodeAnalysis; +using Sybil; + +namespace Net.Sdk.Web.Extensions.SourceGenerators; + +[Generator(LanguageNames.CSharp)] +#nullable enable +public class RouteInterfaceGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(context => + { + GenerateInterface(context); + }); + } + + private static void GenerateInterface(IncrementalGeneratorPostInitializationContext context) + { + var builder = SyntaxBuilder.CreateCompilationUnit() + .WithNamespace(SyntaxBuilder.CreateNamespace(Constants.Namespace) + .WithInterface(SyntaxBuilder.CreateInterface(Constants.RouteInterface) + .WithTypeParameter(SyntaxBuilder.CreateTypeParameter(Constants.RouteTypeParameter)) + .WithTypeParameterConstraint(SyntaxBuilder.CreateTypeParameterConstraint(Constants.RouteTypeParameter) + .WithClass()) + .WithMethod(SyntaxBuilder.CreateMethod(Constants.PreProcessRequestReturnType, Constants.PreProcessRequestMethodName) + .WithModifier(Constants.Public) + .WithParameter(Constants.HttpContextTypeName, Constants.HttpContextParameterName) + .WithParameter(Constants.CancellationTokenTypeName, Constants.CancellationTokenParameterName) + .WithNoBody()) + .WithMethod(SyntaxBuilder.CreateMethod(Constants.HandleRequestReturnType, Constants.HandleRequestMethodName) + .WithModifier(Constants.Public) + .WithParameter($"{Constants.RouteTypeParameter}?", Constants.RequestParameterName) + .WithParameter(Constants.CancellationTokenTypeName, Constants.CancellationTokenParameterName) + .WithNoBody()))); + + var syntax = builder.Build(); + var source = syntax.ToFullString(); + context.AddSource($"{Constants.RouteFileName}.g", source); + } +} + +#nullable disable \ No newline at end of file diff --git a/Net.Sdk.Web.Extensions.sln b/Net.Sdk.Web.Extensions.sln index f53a5ac..297e396 100644 --- a/Net.Sdk.Web.Extensions.sln +++ b/Net.Sdk.Web.Extensions.sln @@ -23,7 +23,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\ci.yaml = .github\workflows\ci.yaml EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Net.Sdk.Web.Extensions.Azure", "Net.Sdk.Web.Extensions.Azure\Net.Sdk.Web.Extensions.Azure.csproj", "{F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net.Sdk.Web.Extensions.Azure", "Net.Sdk.Web.Extensions.Azure\Net.Sdk.Web.Extensions.Azure.csproj", "{F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net.Sdk.Web.Extensions.SourceGenerators", "Net.Sdk.Web.Extensions.SourceGenerators\Net.Sdk.Web.Extensions.SourceGenerators.csproj", "{A76058F7-5073-472E-B6A7-9467E5FB11D0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net.Sdk.Web.Extensions.SourceGenerators.Tests", "Net.Sdk.Web.Extensions.SourceGenerators.Tests\Net.Sdk.Web.Extensions.SourceGenerators.Tests.csproj", "{77F194BA-FB4C-427D-A0D4-0C790937C5C5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -39,6 +43,14 @@ Global {F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}.Debug|Any CPU.Build.0 = Debug|Any CPU {F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}.Release|Any CPU.ActiveCfg = Release|Any CPU {F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}.Release|Any CPU.Build.0 = Release|Any CPU + {A76058F7-5073-472E-B6A7-9467E5FB11D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A76058F7-5073-472E-B6A7-9467E5FB11D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A76058F7-5073-472E-B6A7-9467E5FB11D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A76058F7-5073-472E-B6A7-9467E5FB11D0}.Release|Any CPU.Build.0 = Release|Any CPU + {77F194BA-FB4C-427D-A0D4-0C790937C5C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77F194BA-FB4C-427D-A0D4-0C790937C5C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77F194BA-FB4C-427D-A0D4-0C790937C5C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77F194BA-FB4C-427D-A0D4-0C790937C5C5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj b/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj index 66da70c..1999831 100644 --- a/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj +++ b/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj @@ -6,7 +6,7 @@ enable Library true - 0.7.1 + 0.8 Alexandru Macocian LICENSE true @@ -25,9 +25,11 @@ - - - + + + + +