Skip to content

Commit

Permalink
Move Override specification to method parameter (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
viceroypenguin authored May 9, 2024
1 parent a9f29d3 commit 3f85600
Show file tree
Hide file tree
Showing 94 changed files with 2,281 additions and 1,584 deletions.
5 changes: 2 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,7 @@ dotnet_diagnostic.CA2007.severity = none # CA2007: Consider calli
# Immediate.Handlers relies on nested types
dotnet_diagnostic.CA1034.severity = none # CA1034: Nested types should not be visible

# No need for cryptographically secure anything in this project
dotnet_diagnostic.CA5394.severity = none # CA5394: Random is an insecure random number generator

# Dispose things need disposing
dotnet_diagnostic.CA2000.severity = error # CA2000: Dispose objects before losing scope

dotnet_diagnostic.CA1716.severity = none # CA1716: Identifiers should not match keywords
37 changes: 32 additions & 5 deletions src/Immediate.Apis.Generators/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;

namespace Immediate.Apis.Generators;
Expand Down Expand Up @@ -85,19 +86,45 @@ typeSymbol is INamedTypeSymbol
},
};

public static bool IsEndpointRegistrationOverrideAttribute(this ITypeSymbol? typeSymbol) =>
public static bool IsBindingParameterAttribute([NotNullWhen(returnValue: true)] this ITypeSymbol? typeSymbol) =>
typeSymbol.IsAsParametersAttribute() || typeSymbol.IsFromXxxAttribute();

public static bool IsAsParametersAttribute(this ITypeSymbol? typeSymbol) =>
typeSymbol is INamedTypeSymbol
{
Name: "AsParametersAttribute",
ContainingNamespace:
{
Name: "Http",
ContainingNamespace:
{
Name: "AspNetCore",
ContainingNamespace:
{
Name: "Microsoft",
ContainingNamespace.IsGlobalNamespace: true,
},
},
},
};

public static bool IsFromXxxAttribute(this ITypeSymbol? typeSymbol) =>
typeSymbol is INamedTypeSymbol
{
Name: "EndpointRegistrationOverrideAttribute",
Name: "FromBodyAttribute"
or "FromFormAttribute"
or "FromHeaderAttribute"
or "FromQueryAttribute"
or "FromRouteAttribute",
ContainingNamespace:
{
Name: "Shared",
Name: "Mvc",
ContainingNamespace:
{
Name: "Apis",
Name: "AspNetCore",
ContainingNamespace:
{
Name: "Immediate",
Name: "Microsoft",
ContainingNamespace.IsGlobalNamespace: true,
},
},
Expand Down
10 changes: 9 additions & 1 deletion src/Immediate.Apis.Generators/ImmediateApisGenerator.Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ private sealed record Method
public required string ParameterAttribute { get; init; }
public required string Route { get; init; }

public required string ClassName { get; init; }
public required string? Namespace { get; init; }
public required Class Class { get; init; }
public required string ClassFullName { get; init; }
public required string ClassAsMethodName { get; init; }
public required string ParameterType { get; init; }

Expand All @@ -19,4 +21,10 @@ private sealed record Method
public required bool UseCustomization { get; init; }
public required bool UseTransformMethod { get; init; }
}

public sealed record Class
{
public required string Type { get; init; }
public required string Name { get; init; }
}
}
41 changes: 26 additions & 15 deletions src/Immediate.Apis.Generators/ImmediateApisGenerator.Transform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,15 @@ CancellationToken token

token.ThrowIfCancellationRequested();

var @namespace = symbol.ContainingNamespace.ToString().NullIf("<global namespace>");
var @class = GetClass(symbol);
var className = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var classAsMethodName = symbol.ToString().Replace(".", "_");
var parameterType = handleMethod.Parameters[0].Type
.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var parameterType = handleMethod.Parameters[0].Type;

token.ThrowIfCancellationRequested();

var parameterAttribute = GetParameterAttribute(handleMethod.Parameters[0].Type, httpMethod);
var parameterAttribute = GetParameterAttribute(handleMethod.Parameters[0], httpMethod);

token.ThrowIfCancellationRequested();

Expand All @@ -86,9 +87,11 @@ CancellationToken token
ParameterAttribute = parameterAttribute,
Route = route,

ClassName = className,
Namespace = @namespace,
Class = @class,
ClassFullName = className,
ClassAsMethodName = classAsMethodName,
ParameterType = parameterType,
ParameterType = parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),

AllowAnonymous = allowAnonymous,
Authorize = authorize,
Expand Down Expand Up @@ -165,21 +168,29 @@ m is
&& SymbolEqualityComparer.IncludeNullability.Equals(returnInnerType, paramType)
);

private static string GetParameterAttribute(ITypeSymbol parameterType, string httpMethod)
{
foreach (var a in parameterType.GetAttributes())
private static Class GetClass(INamedTypeSymbol symbol) =>
new()
{
if (a.AttributeClass.IsEndpointRegistrationOverrideAttribute())
Name = symbol.Name,
Type = symbol switch
{
if (a.ConstructorArguments.Length != 0)
return (string)a.ConstructorArguments[0].Value!;
{ TypeKind: TypeKind.Interface } => "interface",
{ IsRecord: true, TypeKind: TypeKind.Struct, } => "record struct",
{ IsRecord: true, } => "record",
{ TypeKind: TypeKind.Struct, } => "struct",
_ => "class",
},
};

if (a.NamedArguments.Length != 0)
return (string)a.NamedArguments[0].Value.Value!;
}
private static string GetParameterAttribute(IParameterSymbol parameterSymbol, string httpMethod)
{
foreach (var a in parameterSymbol.GetAttributes())
{
if (a.AttributeClass.IsBindingParameterAttribute())
return a.AttributeClass.Name[..^9];
}

if (parameterType is INamedTypeSymbol typeSymbol)
if (parameterSymbol.Type is INamedTypeSymbol typeSymbol)
{
foreach (var p in typeSymbol.GetMembers().OfType<IPropertySymbol>())
{
Expand Down
69 changes: 40 additions & 29 deletions src/Immediate.Apis.Generators/Templates/Route.sbntxt
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,50 @@ using Microsoft.AspNetCore.Mvc;

#pragma warning disable CS1591

namespace Microsoft.AspNetCore.Builder;

public static partial class {{ assembly }}RoutesBuilder
namespace Microsoft.AspNetCore.Builder
{
private static void Map{{ method.class_as_method_name }}Endpoint(IEndpointRouteBuilder app)
public static partial class {{ assembly }}RoutesBuilder
{
var endpoint = app
.{{ method.http_method }}(
"{{ method.route }}",
async (
[{{ method.parameter_attribute }}] {{ method.parameter_type }} parameters,
[FromServices] {{ method.class_name }}.Handler handler,
CancellationToken token
) =>
{
var ret = await handler.HandleAsync(parameters, token);
{{~ if method.use_transform_method ~}}
return {{ method.class_name }}.TransformResult(ret);
{{~ else ~}}
return ret;
{{~ end ~}}
}
);
{{~ if method.allow_anonymous ~}}
private static void Map{{ method.class_as_method_name }}Endpoint(IEndpointRouteBuilder app)
{
var endpoint = app
.{{ method.http_method }}(
"{{ method.route }}",
async (
[{{ method.parameter_attribute }}] {{ method.parameter_type }} parameters,
[FromServices] {{ method.class_full_name }}.Handler handler,
CancellationToken token
) =>
{
var ret = await handler.HandleAsync(parameters, token);
{{~ if method.use_transform_method ~}}
return {{ method.class_full_name }}.TransformResult(ret);
{{~ else ~}}
return ret;
{{~ end ~}}
}
);
{{~ if method.allow_anonymous ~}}

_ = endpoint.AllowAnonymous();
{{~ else if method.authorize ~}}
_ = endpoint.AllowAnonymous();
{{~ else if method.authorize ~}}

_ = endpoint.RequireAuthorization({{ if !string.empty method.authorize_policy }}"{{ method.authorize_policy }}"{{ end }});
{{~ end ~}}
{{~ if method.use_customization ~}}
_ = endpoint.RequireAuthorization({{ if !string.empty method.authorize_policy }}"{{ method.authorize_policy }}"{{ end }});
{{~ end ~}}
{{~ if method.use_customization ~}}

{{ method.class_name }}.CustomizeEndpoint(endpoint);
{{~ end ~}}
{{ method.class_full_name }}.CustomizeEndpoint(endpoint);
{{~ end ~}}
}
}
}

{{ if method.namespace -}}
namespace {{ method.namespace }}
{
{{ end }}
/// <remarks><see cref="{{ method.parameter_type }}" /> registered using <c>[{{ method.parameter_attribute }}]</c></remarks>
partial {{ method.class.type }} {{ method.class.name }};
{{ if method.namespace -}}
}
{{ end -}}
3 changes: 3 additions & 0 deletions src/Immediate.Apis.Generators/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ public static Template GetTemplate(string name)
using var reader = new StreamReader(stream);
return Template.Parse(reader.ReadToEnd());
}

public static string? NullIf(this string value, string check) =>
value.Equals(check, StringComparison.Ordinal) ? null : value;
}
54 changes: 0 additions & 54 deletions src/Immediate.Apis.Shared/EndpointRegistration.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace Immediate.Apis.FunctionalTests.Features.WeatherForecast;
[Authorize("Test")]
public static partial class Put
{
[EndpointRegistrationOverride(EndpointRegistration.AsParameters)]
public sealed record Command
{
public required DateOnly Date { get; init; }
Expand All @@ -19,6 +18,7 @@ public sealed record Command
}

private static async ValueTask Handle(
[AsParameters]
Command _,
CancellationToken token
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,34 @@

#pragma warning disable CS1591

namespace Microsoft.AspNetCore.Builder;

public static partial class TestsRoutesBuilder
namespace Microsoft.AspNetCore.Builder
{
private static void MapDummy_GetUsersQueryEndpoint(IEndpointRouteBuilder app)
public static partial class TestsRoutesBuilder
{
var endpoint = app
.MapDelete(
"/test",
async (
[AsParameters] global::Dummy.GetUsersQuery.Query parameters,
[FromServices] global::Dummy.GetUsersQuery.Handler handler,
CancellationToken token
) =>
{
var ret = await handler.HandleAsync(parameters, token);
return ret;
}
);
private static void MapDummy_GetUsersQueryEndpoint(IEndpointRouteBuilder app)
{
var endpoint = app
.MapDelete(
"/test",
async (
[AsParameters] global::Dummy.GetUsersQuery.Query parameters,
[FromServices] global::Dummy.GetUsersQuery.Handler handler,
CancellationToken token
) =>
{
var ret = await handler.HandleAsync(parameters, token);
return ret;
}
);

_ = endpoint.AllowAnonymous();
_ = endpoint.AllowAnonymous();
}
}
}

namespace Dummy
{

/// <remarks><see cref="global::Dummy.GetUsersQuery.Query" /> registered using <c>[AsParameters]</c></remarks>
partial class GetUsersQuery;
}
Loading

0 comments on commit 3f85600

Please sign in to comment.