From d044f9dbcc282adabd42169093916c5d981969ce Mon Sep 17 00:00:00 2001 From: David Driscoll Date: Sat, 9 Apr 2022 01:46:41 -0400 Subject: [PATCH 1/3] initial work on patch objects with source generation --- .../Operations/Rockets/EditRocket.cs | 30 +- sample/Sample.Graphql/Program.cs | 14 +- sample/Sample.Graphql/Startup.cs | 73 +++- .../Sample.Restful.Client.csproj | 2 +- sample/Sample.Restful.Client/Test1.cs | 11 +- .../Controllers/RocketController.cs | 10 + src/Analyzers/GeneratorDiagnostics.cs | 9 + src/Analyzers/InheritFromGenerator.cs | 217 +--------- src/Analyzers/PropertyTrackingGenerator.cs | 377 ++++++++++++++++++ src/Analyzers/SyntaxExtensions.cs | 221 ++++++++++ .../Conventions/ProblemDetailsConvention.cs | 2 + .../Conventions/SwashbuckleConvention.cs | 31 +- .../FluentValidationProblemDetailsFactory.cs | 126 ------ .../Validation/ValidationExceptionFilter.cs | 12 +- .../ValidationProblemDetailsValidator.cs | 4 - src/Foundation/IPropertyTracking.cs | 134 +++++++ .../AutoConfigureMediatRMutation.cs | 59 --- .../Conventions/GraphqlConvention.cs | 16 +- src/HotChocolate/RocketChocolateOptions.cs | 5 - src/HotChocolate/Types/VoidType.cs | 10 +- .../Helpers/GenerationTestResult.cs | 5 + .../Helpers/GenerationTestResults.cs | 36 ++ test/Analyzers.Tests/Helpers/GeneratorTest.cs | 106 ++++- .../PropertyTrackingGeneratorTests.cs | 345 ++++++++++++++++ test/Extensions.Tests/Extensions.Tests.csproj | 1 + .../Rockets/UpdateRocketTests.cs | 36 ++ .../Queries/mutations.graphql | 33 +- .../Rockets/UpdateRocketTests.cs | 100 +++++ test/Sample.Graphql.Tests/schema.graphql | 183 ++++----- .../Rockets/UpdateRocketTests.cs | 115 +++++- 30 files changed, 1729 insertions(+), 594 deletions(-) create mode 100644 src/Analyzers/PropertyTrackingGenerator.cs create mode 100644 src/Analyzers/SyntaxExtensions.cs delete mode 100644 src/AspNetCore/Validation/FluentValidationProblemDetailsFactory.cs create mode 100644 src/Foundation/IPropertyTracking.cs delete mode 100644 src/HotChocolate/AutoConfigureMediatRMutation.cs create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.cs diff --git a/sample/Sample.Core/Operations/Rockets/EditRocket.cs b/sample/Sample.Core/Operations/Rockets/EditRocket.cs index c759e343f..f2e5e2354 100644 --- a/sample/Sample.Core/Operations/Rockets/EditRocket.cs +++ b/sample/Sample.Core/Operations/Rockets/EditRocket.cs @@ -31,6 +31,14 @@ public record Request : IRequest public RocketType Type { get; set; } // TODO: Make generator that can be used to create a writable view model } + public partial record PatchRequest : IRequest, IPropertyTracking + { + /// + /// The rocket id + /// + public RocketId Id { get; init; } + } + private class Mapper : Profile { public Mapper() @@ -64,25 +72,39 @@ public RequestValidator() } } - private class Handler : IRequestHandler + private class Handler : PatchHandlerBase, IRequestHandler { private readonly RocketDbContext _dbContext; private readonly IMapper _mapper; - public Handler(RocketDbContext dbContext, IMapper mapper) + public Handler(RocketDbContext dbContext, IMapper mapper, IMediator mediator) : base(mediator) { _dbContext = dbContext; _mapper = mapper; } - public async Task Handle(Request request, CancellationToken cancellationToken) + private async Task GetRocket(RocketId id, CancellationToken cancellationToken) { - var rocket = await _dbContext.Rockets.FindAsync(new object[] { request.Id }, cancellationToken).ConfigureAwait(false); + var rocket = await _dbContext.Rockets.FindAsync(new object[] { id }, cancellationToken) + .ConfigureAwait(false); if (rocket == null) { throw new NotFoundException(); } + return rocket; + } + + protected override async Task GetRequest(PatchRequest patchRequest, CancellationToken cancellationToken) + { + var rocket = await GetRocket(patchRequest.Id, cancellationToken); + return _mapper.Map(_mapper.Map(rocket)); + } + + public async Task Handle(Request request, CancellationToken cancellationToken) + { + var rocket = await GetRocket(request.Id, cancellationToken); + _mapper.Map(request, rocket); _dbContext.Update(rocket); await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/sample/Sample.Graphql/Program.cs b/sample/Sample.Graphql/Program.cs index 8b4c3cb8a..275f50693 100644 --- a/sample/Sample.Graphql/Program.cs +++ b/sample/Sample.Graphql/Program.cs @@ -21,19 +21,7 @@ public static IHostBuilder CreateHostBuilder(string[] args) .LaunchWith( RocketBooster.ForDependencyContext(DependencyContext.Default), z => z - .WithConventionsFrom(GetConventions) - .Set( - new RocketChocolateOptions - { - RequestPredicate = type => - type is { IsNested: true, DeclaringType: { } } - && !( type.Name.StartsWith("Get", StringComparison.Ordinal) - || type.Name.StartsWith( - "List", StringComparison.Ordinal - ) ), - IncludeAssemblyInfoQuery = true - } - ) + .WithConventionsFrom(GetConventions) ) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); } diff --git a/sample/Sample.Graphql/Startup.cs b/sample/Sample.Graphql/Startup.cs index c6e3336e3..5e38da17f 100644 --- a/sample/Sample.Graphql/Startup.cs +++ b/sample/Sample.Graphql/Startup.cs @@ -3,9 +3,12 @@ using HotChocolate.Data.Sorting; using HotChocolate.Types; using HotChocolate.Types.Pagination; +using MediatR; using Rocket.Surgery.LaunchPad.AspNetCore; using Sample.Core.Domain; using Sample.Core.Models; +using Sample.Core.Operations.LaunchRecords; +using Sample.Core.Operations.Rockets; using Serilog; namespace Sample.Graphql; @@ -21,9 +24,7 @@ public void ConfigureServices(IServiceCollection services) // .AddDefaultTransactionScopeHandler() .AddQueryType() .AddMutationType() - .ModifyRequestOptions( - options => { options.IncludeExceptionDetails = true; } - ) + .ModifyRequestOptions(options => options.IncludeExceptionDetails = true) .AddTypeConverter(source => source.Value) .AddTypeConverter(source => new RocketId(source)) .AddTypeConverter(source => source.Value) @@ -32,6 +33,8 @@ public void ConfigureServices(IServiceCollection services) s => { s.AddType(); + s.AddType(); + s.AddType(); s.AddType(); s.AddType(); @@ -62,9 +65,67 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); - app.UseEndpoints( - endpoints => { endpoints.MapGraphQL(); } - ); + app.UseEndpoints(endpoints => endpoints.MapGraphQL()); + } +} + +[ExtendObjectType(OperationTypeNames.Mutation)] +public class RocketMutation +{ + [UseServiceScope] + public Task CreateRocket([Service] IMediator mediator, CancellationToken cancellationToken, CreateRocket.Request request) + { + return mediator.Send(request, cancellationToken); + } + + [UseServiceScope] + public Task EditRocket([Service] IMediator mediator, CancellationToken cancellationToken, EditRocket.Request request) + { + return mediator.Send(request, cancellationToken); + } + + [UseServiceScope] + public Task PatchRocket([Service] IMediator mediator, CancellationToken cancellationToken, EditRocket.PatchRequest request) + { + return mediator.Send(request, cancellationToken); + } + + [UseServiceScope] + public Task DeleteRocket([Service] IMediator mediator, CancellationToken cancellationToken, DeleteRocket.Request request) + { + return mediator.Send(request, cancellationToken); + } +} + +[ExtendObjectType(OperationTypeNames.Mutation)] +public class LaunchRecordMutation +{ + [UseServiceScope] + public Task CreateLaunchRecord( + [Service] IMediator mediator, CancellationToken cancellationToken, CreateLaunchRecord.Request request + ) + { + return mediator.Send(request, cancellationToken); + } + + [UseServiceScope] + public Task EditLaunchRecord([Service] IMediator mediator, CancellationToken cancellationToken, EditLaunchRecord.Request request) + { + return mediator.Send(request, cancellationToken); + } + + [UseServiceScope] + public Task DeleteLaunchRecord([Service] IMediator mediator, CancellationToken cancellationToken, DeleteLaunchRecord.Request request) + { + return mediator.Send(request, cancellationToken); + } +} + +[ExtendObjectType(OperationTypeNames.Mutation)] +public class MutationType : ObjectTypeExtension +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { } } diff --git a/sample/Sample.Restful.Client/Sample.Restful.Client.csproj b/sample/Sample.Restful.Client/Sample.Restful.Client.csproj index 7861938a3..fedba3b4f 100644 --- a/sample/Sample.Restful.Client/Sample.Restful.Client.csproj +++ b/sample/Sample.Restful.Client/Sample.Restful.Client.csproj @@ -22,7 +22,7 @@ ReferenceOutputAssembly="false" > /generateClientClasses:true /generateClientInterfaces:true /injectHttpClient:true /disposeHttpClient:false /generateExceptionClasses:true /wrapDtoExceptions:true /useBaseUrl:false /generateBaseUrlProperty:false /operationGenerationMode:"MultipleClientsFromFirstTagAndOperationId" /generateOptionalParameters:true /generateJsonMethods:false /enforceFlagEnums:true /parameterArrayType:"System.Collections.Generic.IEnumerable" /parameterDictionaryType:"System.Collections.Generic.IDictionary" /responseArrayType:"System.Collections.Generic.ICollection" /responseDictionaryType:"System.Collections.Generic.IDictionary" /wrapResponses:true /generateResponseClasses:true /responseClass:"Response" /requiredPropertiesMustBeDefined:true /dateType:"System.DateTimeOffset" /dateTimeType:"System.DateTimeOffset" /timeType:"System.TimeSpan" /timeSpanType:"System.TimeSpan" /arrayType:"System.Collections.ObjectModel.Collection" /arrayInstanceType:"System.Collections.ObjectModel.Collection" /dictionaryType:"System.Collections.Generic.IDictionary" /dictionaryInstanceType:"System.Collections.Generic.Dictionary" /arrayBaseType:"System.Collections.ObjectModel.Collection" /dictionaryBaseType:"System.Collections.Generic.Dictionary" /classStyle:"Poco" /generateDefaultValues:true /generateDataAnnotations:true /generateImmutableArrayProperties:true /generateImmutableDictionaryProperties:true /generateDtoTypes:true /generateOptionalPropertiesAsNullable:true + >/generateClientClasses:true /generateClientInterfaces:true /injectHttpClient:true /disposeHttpClient:false /generateExceptionClasses:true /wrapDtoExceptions:true /useBaseUrl:false /generateBaseUrlProperty:false /operationGenerationMode:"MultipleClientsFromFirstTagAndOperationId" /generateOptionalParameters:true /generateJsonMethods:false /enforceFlagEnums:true /parameterArrayType:"System.Collections.Generic.IEnumerable" /parameterDictionaryType:"System.Collections.Generic.IDictionary" /responseArrayType:"System.Collections.Generic.ICollection" /responseDictionaryType:"System.Collections.Generic.IDictionary" /wrapResponses:true /generateResponseClasses:true /responseClass:"Response" /requiredPropertiesMustBeDefined:false /dateType:"System.DateTimeOffset" /dateTimeType:"System.DateTimeOffset" /timeType:"System.TimeSpan" /timeSpanType:"System.TimeSpan" /arrayType:"System.Collections.ObjectModel.Collection" /arrayInstanceType:"System.Collections.ObjectModel.Collection" /dictionaryType:"System.Collections.Generic.IDictionary" /dictionaryInstanceType:"System.Collections.Generic.Dictionary" /arrayBaseType:"System.Collections.ObjectModel.Collection" /dictionaryBaseType:"System.Collections.Generic.Dictionary" /classStyle:"Poco" /generateDefaultValues:true /generateDataAnnotations:true /generateImmutableArrayProperties:true /generateImmutableDictionaryProperties:true /generateDtoTypes:true /generateOptionalPropertiesAsNullable:true diff --git a/sample/Sample.Restful.Client/Test1.cs b/sample/Sample.Restful.Client/Test1.cs index 64b3eb596..d350cc396 100644 --- a/sample/Sample.Restful.Client/Test1.cs +++ b/sample/Sample.Restful.Client/Test1.cs @@ -1,5 +1,12 @@ -namespace JetBrains.Annotations; +using Newtonsoft.Json; -public class Test1 +namespace Sample.Restful.Client; + +public partial class RocketClient { + partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) + { + // This is required for patching to work as expected +// settings.NullValueHandling = NullValueHandling.Ignore; + } } diff --git a/sample/Sample.Restful/Controllers/RocketController.cs b/sample/Sample.Restful/Controllers/RocketController.cs index e25189fcd..39d922b9d 100644 --- a/sample/Sample.Restful/Controllers/RocketController.cs +++ b/sample/Sample.Restful/Controllers/RocketController.cs @@ -44,6 +44,16 @@ public partial class RocketController : RestfulApiController // ReSharper disable once RouteTemplates.ParameterTypeAndConstraintsMismatch public partial Task> EditRocket([BindRequired] [FromRoute] RocketId id, [BindRequired] [FromBody] EditRocket.Request model); + /// + /// Update a given rocket + /// + /// The id of the rocket + /// The request details + /// + [HttpPatch("{id:guid}")] + // ReSharper disable once RouteTemplates.ParameterTypeAndConstraintsMismatch + public partial Task> PatchRocket([BindRequired] [FromRoute] RocketId id, [BindRequired] [FromBody] EditRocket.PatchRequest model); + /// /// Remove a rocket /// diff --git a/src/Analyzers/GeneratorDiagnostics.cs b/src/Analyzers/GeneratorDiagnostics.cs index 6a4f2094a..9867b0be8 100644 --- a/src/Analyzers/GeneratorDiagnostics.cs +++ b/src/Analyzers/GeneratorDiagnostics.cs @@ -39,4 +39,13 @@ internal static class GeneratorDiagnostics DiagnosticSeverity.Error, true ); + + public static DiagnosticDescriptor ParameterMustBeSameTypeOfObject { get; } = new( + "LPAD0005", + "The given declaration must match", + "The declaration {0} must be a {1}.", + "LaunchPad", + DiagnosticSeverity.Error, + true + ); } diff --git a/src/Analyzers/InheritFromGenerator.cs b/src/Analyzers/InheritFromGenerator.cs index 46fc68b0f..320247ef9 100644 --- a/src/Analyzers/InheritFromGenerator.cs +++ b/src/Analyzers/InheritFromGenerator.cs @@ -1,5 +1,4 @@ -using System.Collections.Concurrent; -using System.Text; +using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -328,217 +327,3 @@ node is (ClassDeclarationSyntax or RecordDeclarationSyntax) and TypeDeclarationS ); } } - -internal static class SyntaxExtensions -{ - public static TypeSyntax EnsureNullable(this TypeSyntax typeSyntax) - { - return typeSyntax is NullableTypeSyntax nts ? nts : NullableType(typeSyntax); - } - - public static TypeSyntax EnsureNotNullable(this TypeSyntax typeSyntax) - { - return typeSyntax is NullableTypeSyntax nts ? nts.ElementType : typeSyntax; - } - - public static TypeDeclarationSyntax ReparentDeclaration( - this TypeDeclarationSyntax classToNest, - SourceProductionContext context, - TypeDeclarationSyntax source - ) - { - var parent = source.Parent; - while (parent is TypeDeclarationSyntax parentSyntax) - { - classToNest = parentSyntax - .WithMembers(List()) - .WithAttributeLists(List()) - .WithConstraintClauses(List()) - .WithBaseList(null) - .AddMembers(classToNest); - - if (!parentSyntax.Modifiers.Any(z => z.IsKind(SyntaxKind.PartialKeyword))) - { - context.ReportDiagnostic( - Diagnostic.Create(GeneratorDiagnostics.MustBePartial, parentSyntax.Identifier.GetLocation(), parentSyntax.GetFullMetadataName()) - ); - } - - parent = parentSyntax.Parent; - } - - return classToNest; - } - - public static string GetFullMetadataName(this TypeDeclarationSyntax? source) - { - if (source is null) - return string.Empty; - - var sb = new StringBuilder(source.Identifier.Text); - - var parent = source.Parent; - while (parent is { }) - { - if (parent is TypeDeclarationSyntax tds) - { - sb.Insert(0, '+'); - sb.Insert(0, tds.Identifier.Text); - } - else if (parent is NamespaceDeclarationSyntax nds) - { - sb.Insert(0, '.'); - sb.Insert(0, nds.Name.ToString()); - break; - } - - parent = parent.Parent; - } - - return sb.ToString(); - } - - public static string GetFullMetadataName(this ISymbol? s) - { - if (s == null || IsRootNamespace(s)) - { - return string.Empty; - } - - var sb = new StringBuilder(s.MetadataName); - var last = s; - - s = s.ContainingSymbol; - - while (!IsRootNamespace(s)) - { - if (s is ITypeSymbol && last is ITypeSymbol) - { - sb.Insert(0, '+'); - } - else - { - sb.Insert(0, '.'); - } - - sb.Insert(0, s.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); - s = s.ContainingSymbol; - } - - return sb.ToString(); - - static bool IsRootNamespace(ISymbol symbol) - { - return symbol is INamespaceSymbol s && s.IsGlobalNamespace; - } - } - - public static string? GetSyntaxName(this TypeSyntax typeSyntax) - { - return typeSyntax switch - { - SimpleNameSyntax sns => sns.Identifier.Text, - QualifiedNameSyntax qns => qns.Right.Identifier.Text, - NullableTypeSyntax nts => nts.ElementType.GetSyntaxName() + "?", - PredefinedTypeSyntax pts => pts.Keyword.Text, - ArrayTypeSyntax ats => ats.ElementType.GetSyntaxName() + "[]", - TupleTypeSyntax tts => "(" + tts.Elements.Select(z => $"{z.Type.GetSyntaxName()}{z.Identifier.Text}") + ")", - _ => null // there might be more but for now... throw new NotSupportedException(typeSyntax.GetType().FullName) - }; - } - - public static bool ContainsAttribute(this TypeDeclarationSyntax syntax, string attributePrefixes) // string is comma separated - { - return syntax.AttributeLists.ContainsAttribute(attributePrefixes); - } - - public static bool ContainsAttribute(this AttributeListSyntax list, string attributePrefixes) // string is comma separated - { - if (list is { Attributes: { Count: 0 } }) - return false; - var names = GetNames(attributePrefixes); - - foreach (var item in list.Attributes) - { - if (item.Name.GetSyntaxName() is { } n && names.Contains(n)) - return true; - } - - return false; - } - - public static bool ContainsAttribute(this in SyntaxList list, string attributePrefixes) // string is comma separated - { - if (list is { Count: 0 }) - return false; - var names = GetNames(attributePrefixes); - - foreach (var item in list) - { - foreach (var attribute in item.Attributes) - { - if (attribute.Name.GetSyntaxName() is { } n && names.Contains(n)) - return true; - } - } - - return false; - } - - public static AttributeSyntax? GetAttribute(this TypeDeclarationSyntax syntax, string attributePrefixes) // string is comma separated - { - return syntax.AttributeLists.GetAttribute(attributePrefixes); - } - - public static AttributeSyntax? GetAttribute(this AttributeListSyntax list, string attributePrefixes) // string is comma separated - { - if (list is { Attributes: { Count: 0 } }) - return null; - var names = GetNames(attributePrefixes); - - foreach (var item in list.Attributes) - { - if (item.Name.GetSyntaxName() is { } n && names.Contains(n)) - return item; - } - - return null; - } - - public static AttributeSyntax? GetAttribute(this in SyntaxList list, string attributePrefixes) // string is comma separated - { - if (list is { Count: 0 }) - return null; - var names = GetNames(attributePrefixes); - - foreach (var item in list) - { - foreach (var attribute in item.Attributes) - { - if (attribute.Name.GetSyntaxName() is { } n && names.Contains(n)) - return attribute; - } - } - - return null; - } - - public static bool IsAttribute(this AttributeSyntax attributeSyntax, string attributePrefixes) // string is comma separated - { - var names = GetNames(attributePrefixes); - return attributeSyntax.Name.GetSyntaxName() is { } n && names.Contains(n); - } - - private static readonly ConcurrentDictionary> AttributeNames = new(); - - private static HashSet GetNames(string attributePrefixes) - { - if (!AttributeNames.TryGetValue(attributePrefixes, out var names)) - { - names = new HashSet(attributePrefixes.Split(',').SelectMany(z => new[] { z, z + "Attribute" })); - AttributeNames.TryAdd(attributePrefixes, names); - } - - return names; - } -} diff --git a/src/Analyzers/PropertyTrackingGenerator.cs b/src/Analyzers/PropertyTrackingGenerator.cs new file mode 100644 index 000000000..14adc3096 --- /dev/null +++ b/src/Analyzers/PropertyTrackingGenerator.cs @@ -0,0 +1,377 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Rocket.Surgery.LaunchPad.Analyzers; + +/// +/// A generator that is used to copy properties, fields and methods from one type onto another. +/// +[Generator] +public class PropertyTrackingGenerator : IIncrementalGenerator +{ + private static void GeneratePropertyTracking( + SourceProductionContext context, + Compilation compilation, + TypeDeclarationSyntax declaration, + INamedTypeSymbol symbol + ) + { + if (!declaration.Modifiers.Any(z => z.IsKind(SyntaxKind.PartialKeyword))) + { + context.ReportDiagnostic( + Diagnostic.Create(GeneratorDiagnostics.MustBePartial, declaration.Identifier.GetLocation(), declaration.GetFullMetadataName()) + ); + return; + } + + var targetSymbol = (INamedTypeSymbol)symbol.Interfaces.SingleOrDefault(z => z.Name.StartsWith("IPropertyTracking", StringComparison.Ordinal)) + ?.TypeArguments[0]; + var isRecord = declaration is RecordDeclarationSyntax; + + if (targetSymbol.IsRecord != isRecord) + { + context.ReportDiagnostic( + Diagnostic.Create( + GeneratorDiagnostics.ParameterMustBeSameTypeOfObject, declaration.Keyword.GetLocation(), declaration.GetFullMetadataName(), + declaration.Keyword.IsKind(SyntaxKind.ClassKeyword) ? "record" : "class" + ) + ); + return; + } + + var classToInherit = declaration + .WithMembers(List()) + .WithAttributeLists(List()) + .WithConstraintClauses(List()) + .WithBaseList(null); + + var writeableProperties = + targetSymbol.GetMembers() + .OfType() + // only works for `set`able properties not init only + .Where(z => !symbol.GetMembers(z.Name).Any()) + .Where(z => z is { IsStatic: false, IsIndexer: false, IsReadOnly: false }); + if (!targetSymbol.IsRecord) + { + // not able to use with operator, so ignore any init only properties. + writeableProperties = writeableProperties.Where(z => z is { SetMethod.IsInitOnly: false, GetMethod.IsReadOnly: false }); + } + + var changesRecord = RecordDeclaration(Token(SyntaxKind.RecordKeyword), "Changes") + .WithModifiers(SyntaxTokenList.Create(Token(SyntaxKind.PublicKeyword))) + .WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken)) + ; + var getChangedStateMethodInitializer = InitializerExpression(SyntaxKind.ObjectInitializerExpression); + var applyChangesBody = Block(); + var resetChangesBody = Block(); + + foreach (var propertySymbol in writeableProperties) + { + var type = ParseTypeName(propertySymbol.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); +// classToInherit = classToInherit.AddMembers(GenerateTrackingProperties(propertySymbol, type)); + classToInherit = classToInherit.AddMembers(GenerateTrackingProperties(propertySymbol, type)); + changesRecord = changesRecord.AddMembers( + PropertyDeclaration(PredefinedType(Token(SyntaxKind.BoolKeyword)), Identifier(propertySymbol.Name)) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithAccessorList( + AccessorList( + List( + new[] + { + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + AccessorDeclaration(SyntaxKind.InitAccessorDeclaration).WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + } + ) + ) + ) + ); + getChangedStateMethodInitializer = getChangedStateMethodInitializer.AddExpressions( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(propertySymbol.Name), + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(propertySymbol.Name), + IdentifierName("HasBeenSet") + ) + ) + ) + ); + applyChangesBody = applyChangesBody.AddStatements( + GenerateApplyChangesBodyPart(propertySymbol, IdentifierName("value"), isRecord) + ); + resetChangesBody = resetChangesBody.AddStatements( + ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(propertySymbol.Name), + IdentifierName("ResetState") + ) + ) + ) + ); + } + + var getChangedStateMethod = + MethodDeclaration( + ParseTypeName("Changes"), Identifier("GetChangedState") + ) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithBody( + Block( + SingletonList( + ReturnStatement( + ObjectCreationExpression(IdentifierName("Changes")) + .WithArgumentList(ArgumentList()) + .WithInitializer(getChangedStateMethodInitializer) + ) + ) + ) + ); + + var applyChangesMethod = MethodDeclaration(ParseTypeName(targetSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)), "ApplyChanges") + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithParameterList( + ParameterList( + SingletonSeparatedList( + Parameter(Identifier("value")).WithType( + IdentifierName(targetSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)) + ) + ) + ) + ) + .WithBody( + applyChangesBody.AddStatements( + ExpressionStatement(InvocationExpression(IdentifierName("ResetChanges"))), + ReturnStatement(IdentifierName("value")) + ) + ); + var resetChangesMethod = MethodDeclaration(IdentifierName(symbol.Name), "ResetChanges") + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithBody(resetChangesBody.AddStatements(ReturnStatement(ThisExpression()))); + var resetChangesImplementation = MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), "ResetChanges") + .WithExplicitInterfaceSpecifier( + ExplicitInterfaceSpecifier( + GenericName(Identifier("IPropertyTracking")) + .WithTypeArgumentList( + TypeArgumentList( + SingletonSeparatedList( + IdentifierName(targetSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)) + ) + ) + ) + ) + ) + .WithBody(Block().AddStatements(ExpressionStatement(InvocationExpression(IdentifierName("ResetChanges"))))); + + classToInherit = classToInherit + .AddMembers( + changesRecord, + getChangedStateMethod, + applyChangesMethod, + resetChangesMethod, + resetChangesImplementation + ); + + var cu = CompilationUnit( + List(), + List(declaration.SyntaxTree.GetCompilationUnitRoot().Usings), + List(), + SingletonList( + symbol.ContainingNamespace.IsGlobalNamespace + ? classToInherit.ReparentDeclaration(context, declaration) + : NamespaceDeclaration(ParseName(symbol.ContainingNamespace.ToDisplayString())) + .WithMembers(SingletonList(classToInherit.ReparentDeclaration(context, declaration))) + ) + ) + .WithLeadingTrivia() + .WithTrailingTrivia() + .WithLeadingTrivia(Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true))) + .WithTrailingTrivia(Trivia(NullableDirectiveTrivia(Token(SyntaxKind.RestoreKeyword), true)), CarriageReturnLineFeed); + + context.AddSource( + $"{Path.GetFileNameWithoutExtension(declaration.SyntaxTree.FilePath)}_{declaration.Identifier.Text}", + cu.NormalizeWhitespace().GetText(Encoding.UTF8) + ); + } + + private static StatementSyntax GenerateApplyChangesBodyPart( + IPropertySymbol propertySymbol, IdentifierNameSyntax valueIdentifier, bool isRecord + ) + { + return IfStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(propertySymbol.Name), + IdentifierName("HasBeenSet") + ) + ), + Block( + SingletonList( + ExpressionStatement( + isRecord + ? AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + valueIdentifier, + WithExpression( + valueIdentifier, + InitializerExpression( + SyntaxKind.WithInitializerExpression, + SingletonSeparatedList( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(propertySymbol.Name), + IdentifierName(propertySymbol.Name) + ) + ) + ) + ) + ) + : AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, valueIdentifier, IdentifierName(propertySymbol.Name)), + IdentifierName(propertySymbol.Name) + ) + ) + ) + ) + ); + } + + private static MemberDeclarationSyntax[] GenerateTrackingProperties(IPropertySymbol propertySymbol, TypeSyntax typeSyntax) + { + var fieldName = $"_{propertySymbol.Name}"; + return new MemberDeclarationSyntax[] + { + FieldDeclaration( + VariableDeclaration( + NullableType( + GenericName(Identifier("Rocket.Surgery.LaunchPad.Foundation.Assigned")) + .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(typeSyntax))) + ) + ) + .WithVariables( + SingletonSeparatedList( + VariableDeclarator( + Identifier(fieldName) + ).WithInitializer( + EqualsValueClause( + ObjectCreationExpression( + GenericName(Identifier("Rocket.Surgery.LaunchPad.Foundation.Assigned")) + .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(typeSyntax))) + ) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument(LiteralExpression(SyntaxKind.DefaultLiteralExpression, Token(SyntaxKind.DefaultKeyword))) + ) + ) + ) + ) + ) + ) + ) + ) + .WithModifiers(TokenList(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.ReadOnlyKeyword))), + PropertyDeclaration( + NullableType( + GenericName(Identifier("Rocket.Surgery.LaunchPad.Foundation.Assigned")) + .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(typeSyntax))) + ), + Identifier(propertySymbol.Name) + ) + .WithAttributeLists( + SingletonList( + AttributeList( + SingletonSeparatedList( + Attribute( + ParseName("System.Diagnostics.CodeAnalysis.AllowNull") + ) + ) + ) + ) + ) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword) + ) + ) + .WithAccessorList( + AccessorList( + List( + new[] + { + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithExpressionBody(ArrowExpressionClause(IdentifierName(fieldName))) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + AccessorDeclaration( + SyntaxKind.SetAccessorDeclaration + ) + .WithExpressionBody( + ArrowExpressionClause( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, IdentifierName(fieldName), IdentifierName("Value") + ), + BinaryExpression( + SyntaxKind.CoalesceExpression, + ConditionalAccessExpression(IdentifierName("value"), MemberBindingExpression(IdentifierName("Value"))), + LiteralExpression(SyntaxKind.DefaultLiteralExpression, Token(SyntaxKind.DefaultKeyword)) + ) + ) + ) + ) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + } + ) + ) + ) + }; + } + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var values = context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => + node is (ClassDeclarationSyntax or RecordDeclarationSyntax) and TypeDeclarationSyntax + { + BaseList: { } baseList + } && baseList.Types.Any( + z => z.Type is GenericNameSyntax qns && qns.Identifier.Text.EndsWith( + "IPropertyTracking", StringComparison.OrdinalIgnoreCase + ) + ), + static (syntaxContext, token) => ( + syntax: (TypeDeclarationSyntax)syntaxContext.Node, semanticModel: syntaxContext.SemanticModel, + symbol: syntaxContext.SemanticModel.GetDeclaredSymbol((TypeDeclarationSyntax)syntaxContext.Node, token)! + ) + ).Combine( + context.CompilationProvider + ) + .Select( + static (tuple, _) => ( + tuple.Left.syntax, + tuple.Left.semanticModel, + tuple.Left.symbol, + compilation: tuple.Right + ) + ) + .Where(x => x.symbol is not null); + + context.RegisterSourceOutput( + values, + static (productionContext, tuple) => GeneratePropertyTracking(productionContext, tuple.compilation, tuple.syntax, tuple.symbol) + ); + } +} diff --git a/src/Analyzers/SyntaxExtensions.cs b/src/Analyzers/SyntaxExtensions.cs new file mode 100644 index 000000000..1fce9b168 --- /dev/null +++ b/src/Analyzers/SyntaxExtensions.cs @@ -0,0 +1,221 @@ +using System.Collections.Concurrent; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Rocket.Surgery.LaunchPad.Analyzers; + +internal static class SyntaxExtensions +{ + public static TypeSyntax EnsureNullable(this TypeSyntax typeSyntax) + { + return typeSyntax is NullableTypeSyntax nts ? nts : SyntaxFactory.NullableType(typeSyntax); + } + + public static TypeSyntax EnsureNotNullable(this TypeSyntax typeSyntax) + { + return typeSyntax is NullableTypeSyntax nts ? nts.ElementType : typeSyntax; + } + + public static TypeDeclarationSyntax ReparentDeclaration( + this TypeDeclarationSyntax classToNest, + SourceProductionContext context, + TypeDeclarationSyntax source + ) + { + var parent = source.Parent; + while (parent is TypeDeclarationSyntax parentSyntax) + { + classToNest = parentSyntax + .WithMembers(SyntaxFactory.List()) + .WithAttributeLists(SyntaxFactory.List()) + .WithConstraintClauses(SyntaxFactory.List()) + .WithBaseList(null) + .AddMembers(classToNest); + + if (!parentSyntax.Modifiers.Any(z => z.IsKind(SyntaxKind.PartialKeyword))) + { + context.ReportDiagnostic( + Diagnostic.Create(GeneratorDiagnostics.MustBePartial, parentSyntax.Identifier.GetLocation(), parentSyntax.GetFullMetadataName()) + ); + } + + parent = parentSyntax.Parent; + } + + return classToNest; + } + + public static string GetFullMetadataName(this TypeDeclarationSyntax? source) + { + if (source is null) + return string.Empty; + + var sb = new StringBuilder(source.Identifier.Text); + + var parent = source.Parent; + while (parent is { }) + { + if (parent is TypeDeclarationSyntax tds) + { + sb.Insert(0, '+'); + sb.Insert(0, tds.Identifier.Text); + } + else if (parent is NamespaceDeclarationSyntax nds) + { + sb.Insert(0, '.'); + sb.Insert(0, nds.Name.ToString()); + break; + } + + parent = parent.Parent; + } + + return sb.ToString(); + } + + public static string GetFullMetadataName(this ISymbol? s) + { + if (s == null || IsRootNamespace(s)) + { + return string.Empty; + } + + var sb = new StringBuilder(s.MetadataName); + var last = s; + + s = s.ContainingSymbol; + + while (!IsRootNamespace(s)) + { + if (s is ITypeSymbol && last is ITypeSymbol) + { + sb.Insert(0, '+'); + } + else + { + sb.Insert(0, '.'); + } + + sb.Insert(0, s.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); + s = s.ContainingSymbol; + } + + return sb.ToString(); + + static bool IsRootNamespace(ISymbol symbol) + { + return symbol is INamespaceSymbol s && s.IsGlobalNamespace; + } + } + + public static string? GetSyntaxName(this TypeSyntax typeSyntax) + { + return typeSyntax switch + { + SimpleNameSyntax sns => sns.Identifier.Text, + QualifiedNameSyntax qns => qns.Right.Identifier.Text, + NullableTypeSyntax nts => nts.ElementType.GetSyntaxName() + "?", + PredefinedTypeSyntax pts => pts.Keyword.Text, + ArrayTypeSyntax ats => ats.ElementType.GetSyntaxName() + "[]", + TupleTypeSyntax tts => "(" + tts.Elements.Select(z => $"{z.Type.GetSyntaxName()}{z.Identifier.Text}") + ")", + _ => null // there might be more but for now... throw new NotSupportedException(typeSyntax.GetType().FullName) + }; + } + + public static bool ContainsAttribute(this TypeDeclarationSyntax syntax, string attributePrefixes) // string is comma separated + { + return syntax.AttributeLists.ContainsAttribute(attributePrefixes); + } + + public static bool ContainsAttribute(this AttributeListSyntax list, string attributePrefixes) // string is comma separated + { + if (list is { Attributes: { Count: 0 } }) + return false; + var names = GetNames(attributePrefixes); + + foreach (var item in list.Attributes) + { + if (item.Name.GetSyntaxName() is { } n && names.Contains(n)) + return true; + } + + return false; + } + + public static bool ContainsAttribute(this in SyntaxList list, string attributePrefixes) // string is comma separated + { + if (list is { Count: 0 }) + return false; + var names = GetNames(attributePrefixes); + + foreach (var item in list) + { + foreach (var attribute in item.Attributes) + { + if (attribute.Name.GetSyntaxName() is { } n && names.Contains(n)) + return true; + } + } + + return false; + } + + public static AttributeSyntax? GetAttribute(this TypeDeclarationSyntax syntax, string attributePrefixes) // string is comma separated + { + return syntax.AttributeLists.GetAttribute(attributePrefixes); + } + + public static AttributeSyntax? GetAttribute(this AttributeListSyntax list, string attributePrefixes) // string is comma separated + { + if (list is { Attributes: { Count: 0 } }) + return null; + var names = GetNames(attributePrefixes); + + foreach (var item in list.Attributes) + { + if (item.Name.GetSyntaxName() is { } n && names.Contains(n)) + return item; + } + + return null; + } + + public static AttributeSyntax? GetAttribute(this in SyntaxList list, string attributePrefixes) // string is comma separated + { + if (list is { Count: 0 }) + return null; + var names = GetNames(attributePrefixes); + + foreach (var item in list) + { + foreach (var attribute in item.Attributes) + { + if (attribute.Name.GetSyntaxName() is { } n && names.Contains(n)) + return attribute; + } + } + + return null; + } + + public static bool IsAttribute(this AttributeSyntax attributeSyntax, string attributePrefixes) // string is comma separated + { + var names = GetNames(attributePrefixes); + return attributeSyntax.Name.GetSyntaxName() is { } n && names.Contains(n); + } + + private static readonly ConcurrentDictionary> AttributeNames = new(); + + private static HashSet GetNames(string attributePrefixes) + { + if (!AttributeNames.TryGetValue(attributePrefixes, out var names)) + { + names = new HashSet(attributePrefixes.Split(',').SelectMany(z => new[] { z, z + "Attribute" })); + AttributeNames.TryAdd(attributePrefixes, names); + } + + return names; + } +} diff --git a/src/AspNetCore/Conventions/ProblemDetailsConvention.cs b/src/AspNetCore/Conventions/ProblemDetailsConvention.cs index a430d7fa6..e75e9b164 100644 --- a/src/AspNetCore/Conventions/ProblemDetailsConvention.cs +++ b/src/AspNetCore/Conventions/ProblemDetailsConvention.cs @@ -32,6 +32,8 @@ public void Register(IConventionContext context, IConfiguration configuration, I .AddProblemDetails() .AddProblemDetailsConventions(); + services.AddOptions() + .Configure(options => options.SuppressModelStateInvalidFilter = true); services.AddOptions() .Configure>( (builder, apiBehaviorOptions) => diff --git a/src/AspNetCore/Conventions/SwashbuckleConvention.cs b/src/AspNetCore/Conventions/SwashbuckleConvention.cs index f87a9f6f8..0cf6ce810 100644 --- a/src/AspNetCore/Conventions/SwashbuckleConvention.cs +++ b/src/AspNetCore/Conventions/SwashbuckleConvention.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Text; +using System.Text.Json; using FluentValidation; using MicroElements.Swashbuckle.FluentValidation.AspNetCore; using Microsoft.AspNetCore.Mvc; @@ -95,14 +96,30 @@ public void Register(IConventionContext context, IConfiguration configuration, I } ); - options.CustomSchemaIds( - type => + string nestedTypeName(Type type) + { + return type.IsNested ? schemaIdSelector(type.DeclaringType!) + type.Name : type.Name; + } + + string schemaIdSelector(Type type) + { + if (type == typeof(Severity)) return $"Validation{nameof(Severity)}"; + if (type.IsGenericType) { - if (type == typeof(Severity)) - return $"Validation{nameof(Severity)}"; - return type.IsNested ? type.DeclaringType?.Name + type.Name : type.Name; + var sb = new StringBuilder(); + var name = nestedTypeName(type); + name = name[..name.IndexOf('`', StringComparison.Ordinal)]; + sb.Append(name); + foreach (var gt in type.GetGenericArguments()) + sb.Append('_').Append(schemaIdSelector(gt)); + + return sb.ToString(); } - ); + + return nestedTypeName(type); + } + + options.CustomSchemaIds(schemaIdSelector); foreach (var item in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.xml") .Where(x => File.Exists(Path.ChangeExtension(x, "dll")))) diff --git a/src/AspNetCore/Validation/FluentValidationProblemDetailsFactory.cs b/src/AspNetCore/Validation/FluentValidationProblemDetailsFactory.cs deleted file mode 100644 index 6c7e6e7ab..000000000 --- a/src/AspNetCore/Validation/FluentValidationProblemDetailsFactory.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Diagnostics; -using FluentValidation; -using FluentValidation.Results; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.Options; - -namespace Rocket.Surgery.LaunchPad.AspNetCore.Validation; - -internal sealed class FluentValidationProblemDetailsFactory : ProblemDetailsFactory -{ - private readonly ApiBehaviorOptions _apiBehaviorOptions; - - public FluentValidationProblemDetailsFactory(IOptions apiBehavior) - { - _apiBehaviorOptions = - apiBehavior.Value ?? throw new ArgumentNullException(nameof(apiBehavior)); - } - - /// - public override ProblemDetails CreateProblemDetails( - HttpContext httpContext, - int? statusCode = null, - string? title = null, - string? type = null, - string? detail = null, - string? instance = null - ) - { - statusCode ??= 500; - - var problemDetails = new ProblemDetails - { - Status = statusCode, - Title = title, - Type = type, - Detail = detail, - Instance = instance - }; - - ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); - - return problemDetails; - } - - /// - public override ValidationProblemDetails CreateValidationProblemDetails( - HttpContext httpContext, - ModelStateDictionary modelStateDictionary, - int? statusCode = null, - string? title = null, - string? type = null, - string? detail = null, - string? instance = null - ) - { - if (modelStateDictionary == null) - { - throw new ArgumentNullException(nameof(modelStateDictionary)); - } - - statusCode ??= 400; - - ValidationProblemDetails? problemDetails = null; - - if (httpContext.Items[typeof(ValidationResult)] is ValidationResult result) - { - statusCode = 422; - problemDetails = new FluentValidationProblemDetails(result) - { - Status = 422, - Type = type, - Detail = detail, - Instance = instance - }; - } - else if (httpContext.Items[typeof(ValidationException)] is ValidationException failures) - { - statusCode = 422; - problemDetails = new FluentValidationProblemDetails(failures.Errors) - { - Status = 422, - Type = type, - Detail = detail, - Instance = instance - }; - } - - if (problemDetails == null) - { - problemDetails = new ValidationProblemDetails(modelStateDictionary) - { - Status = statusCode, - Type = type, - Detail = detail, - Instance = instance - }; - } - - if (title != null) - { - // For validation problem details, don't overwrite the default title with null. - problemDetails.Title = title; - } - - ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); - - return problemDetails; - } - - private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode) - { - problemDetails.Status ??= statusCode; - - if (_apiBehaviorOptions.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData)) - { - problemDetails.Title ??= clientErrorData.Title; - problemDetails.Type ??= clientErrorData.Link; - } - - var traceId = Activity.Current?.Id ?? httpContext.TraceIdentifier; - problemDetails.Extensions["traceId"] = traceId; - } -} diff --git a/src/AspNetCore/Validation/ValidationExceptionFilter.cs b/src/AspNetCore/Validation/ValidationExceptionFilter.cs index 61d050052..999ff8580 100644 --- a/src/AspNetCore/Validation/ValidationExceptionFilter.cs +++ b/src/AspNetCore/Validation/ValidationExceptionFilter.cs @@ -8,7 +8,7 @@ namespace Rocket.Surgery.LaunchPad.AspNetCore.Validation; /// /// Captures the validation exception /// -public class ValidationExceptionFilter : IExceptionFilter, IActionFilter, IOrderedFilter +public class ValidationExceptionFilter : IExceptionFilter, IAsyncExceptionFilter, IActionFilter { /// public void OnActionExecuting(ActionExecutingContext context) @@ -29,6 +29,13 @@ public void OnActionExecuted(ActionExecutedContext context) } } + /// + public Task OnExceptionAsync(ExceptionContext context) + { + OnException(context); + return Task.CompletedTask; + } + /// public void OnException(ExceptionContext context) { @@ -36,7 +43,4 @@ public void OnException(ExceptionContext context) context.ExceptionHandled = true; context.Result = new UnprocessableEntityObjectResult(new FluentValidationProblemDetails(validationException.Errors)); } - - - public int Order { get; } = -2000; } diff --git a/src/AspNetCore/Validation/ValidationProblemDetailsValidator.cs b/src/AspNetCore/Validation/ValidationProblemDetailsValidator.cs index 84aefaadc..948e7a845 100644 --- a/src/AspNetCore/Validation/ValidationProblemDetailsValidator.cs +++ b/src/AspNetCore/Validation/ValidationProblemDetailsValidator.cs @@ -12,7 +12,3 @@ public ValidationProblemDetailsValidator() RuleFor(x => x.Errors).NotNull(); } } - -// metrics -// health checks -// versioning diff --git a/src/Foundation/IPropertyTracking.cs b/src/Foundation/IPropertyTracking.cs new file mode 100644 index 000000000..88ce4c42d --- /dev/null +++ b/src/Foundation/IPropertyTracking.cs @@ -0,0 +1,134 @@ +using MediatR; + +namespace Rocket.Surgery.LaunchPad.Foundation; + +/// +/// Marker interface used to create a record or class that copies any setable properties for classes and init/setable record properties +/// +/// +[PublicAPI] +public interface IPropertyTracking +{ + /// + /// Method used apply changes from a given property tracking class. + /// + /// + /// For records this will return a new record instance for classes it will mutate the existing instance. + /// + /// + /// + T ApplyChanges(T state); + + /// + /// Reset the state of the property tracking to only track future changes + /// + /// + void ResetChanges(); +} + +/// +/// A helper class for tracking changes to a value +/// +/// +public class Assigned +{ + private T _value; + private bool _hasBeenSet; + + /// + /// The constructor for creating an assigned value + /// + /// + public Assigned(T value) + { + _value = value; + } + + /// + /// The underlying value + /// + public T Value + { + get => _value; + set + { + _hasBeenSet = true; + _value = value; + } + } + + /// + /// Has the value been assigned for this item + /// + public bool HasBeenSet() + { + return _hasBeenSet; + } + + /// + /// Resets this value as changed. + /// + public void ResetState() + { + _hasBeenSet = false; + } + +#pragma warning disable CA2225 + /// + /// Implicit operator for returning the underlying value + /// + /// + /// + public static implicit operator T?(Assigned? assigned) + { + return assigned == null ? default : assigned.Value; + } + + /// + /// Implicit operator for creating an assigned value + /// + /// + /// + public static implicit operator Assigned(T value) + { + return new(value); + } +#pragma warning restore CA2225 +} + +/// +/// A common handler for creating patch methods +/// +/// The request type that will be run after the patch has been applied +/// The patch object itself +/// The final result object +public abstract class PatchHandlerBase : IRequestHandler + where TRequest : IRequest + where TPatch : IPropertyTracking, IRequest +{ + private readonly IMediator _mediator; + + /// + /// The based handler using Mediator + /// + /// + protected PatchHandlerBase(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// Method used to get , database calls, etc. + /// + /// + /// + /// + protected abstract Task GetRequest(TPatch patchRequest, CancellationToken cancellationToken); + + /// + public async Task Handle(TPatch request, CancellationToken cancellationToken) + { + var underlyingRequest = await GetRequest(request, cancellationToken); + return await _mediator.Send(request.ApplyChanges(underlyingRequest), cancellationToken); + } +} diff --git a/src/HotChocolate/AutoConfigureMediatRMutation.cs b/src/HotChocolate/AutoConfigureMediatRMutation.cs deleted file mode 100644 index bbc7ae871..000000000 --- a/src/HotChocolate/AutoConfigureMediatRMutation.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Reflection; -using HotChocolate.Types; -using MediatR; -using Microsoft.Extensions.DependencyInjection; -using Rocket.Surgery.LaunchPad.HotChocolate.Types; - -namespace Rocket.Surgery.LaunchPad.HotChocolate; - -/// -/// Creates mutations from all of the given or types -/// -public class AutoConfigureMediatRMutation : ObjectTypeExtension -{ - private static void Configure(IObjectFieldDescriptor descriptor) - where TRequest : IRequest - { - var d = descriptor - .Resolve( - (context, ct) => context.Services.GetRequiredService().Send( - context.ArgumentValue("request") ?? Activator.CreateInstance(), - ct - ) - ); - if (typeof(TRequest).GetProperties() is { Length: > 0 }) - { - d.Argument("request", z => z.Type(typeof(TRequest))); - } - - if (typeof(TResponse) == typeof(Unit)) - { - d.Type(); - } - } - - private readonly IEnumerable _mediatorRequestTypes; - - /// - /// Create the given MediatR Mutation - /// - /// - public AutoConfigureMediatRMutation(IEnumerable mediatorRequestTypes) - { - _mediatorRequestTypes = mediatorRequestTypes; - } - - /// - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor.Name(OperationTypeNames.Mutation); - var method = typeof(AutoConfigureMediatRMutation).GetMethod(nameof(Configure), BindingFlags.Static | BindingFlags.NonPublic)!; - - foreach (var type in _mediatorRequestTypes) - { - var response = type.GetInterfaces().Single(z => z.IsGenericType && z.GetGenericTypeDefinition() == typeof(IRequest<>)) - .GetGenericArguments()[0]; - method.MakeGenericMethod(type, response).Invoke(null, new object?[] { descriptor.Field(type.DeclaringType!.Name) }); - } - } -} diff --git a/src/HotChocolate/Conventions/GraphqlConvention.cs b/src/HotChocolate/Conventions/GraphqlConvention.cs index d519e872d..c6975bdd0 100644 --- a/src/HotChocolate/Conventions/GraphqlConvention.cs +++ b/src/HotChocolate/Conventions/GraphqlConvention.cs @@ -1,4 +1,5 @@ -using MediatR; +using HotChocolate.Types; +using MediatR; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -7,6 +8,7 @@ using Rocket.Surgery.Conventions.Reflection; using Rocket.Surgery.LaunchPad.Foundation; using Rocket.Surgery.LaunchPad.HotChocolate.Conventions; +using Rocket.Surgery.LaunchPad.HotChocolate.Types; [assembly: Convention(typeof(GraphqlConvention))] @@ -24,7 +26,6 @@ public class GraphqlConvention : IServiceConvention /// /// The graphql convention /// - /// /// /// public GraphqlConvention( @@ -39,18 +40,11 @@ public GraphqlConvention( /// public void Register(IConventionContext context, IConfiguration configuration, IServiceCollection services) { - var types = context.AssemblyCandidateFinder.GetCandidateAssemblies("MediatR") - .SelectMany(z => z.GetTypes()) - .Where(typeof(IBaseRequest).IsAssignableFrom) - .Where(z => z is { IsAbstract: false }) - .Where(_rocketChocolateOptions.RequestPredicate) - .ToArray(); - var sb = services .AddGraphQL() - .AddErrorFilter(); + .AddErrorFilter() + .BindRuntimeType(); - sb.ConfigureSchema(c => c.AddType(new AutoConfigureMediatRMutation(types))); if (!_rocketChocolateOptions.IncludeAssemblyInfoQuery) { return; diff --git a/src/HotChocolate/RocketChocolateOptions.cs b/src/HotChocolate/RocketChocolateOptions.cs index 0d8b64a25..0d241d559 100644 --- a/src/HotChocolate/RocketChocolateOptions.cs +++ b/src/HotChocolate/RocketChocolateOptions.cs @@ -9,9 +9,4 @@ public class RocketChocolateOptions /// Include the assembly info query data automagically /// public bool IncludeAssemblyInfoQuery { get; set; } - - /// - /// A check that can be used to select specific MediatR requests that will be converted to mutations. - /// - public Func RequestPredicate { get; set; } = z => z is { IsNested: true, DeclaringType: { } }; } diff --git a/src/HotChocolate/Types/VoidType.cs b/src/HotChocolate/Types/VoidType.cs index 93e803991..baccfc9bd 100644 --- a/src/HotChocolate/Types/VoidType.cs +++ b/src/HotChocolate/Types/VoidType.cs @@ -27,32 +27,32 @@ public override bool IsInstanceOfType(IValueNode valueSyntax) /// public override object? ParseLiteral(IValueNode valueSyntax) { - return null; + return new object(); } /// public override IValueNode ParseValue(object? runtimeValue) { - return new NullValueNode(null); + return new ObjectValueNode(); } /// public override IValueNode ParseResult(object? resultValue) { - return new NullValueNode(null); + return new ObjectValueNode(); } /// public override bool TrySerialize(object? runtimeValue, out object? resultValue) { - resultValue = null; + resultValue = new object(); return true; } /// public override bool TryDeserialize(object? resultValue, out object? runtimeValue) { - runtimeValue = null; + runtimeValue = new object(); return true; } } diff --git a/test/Analyzers.Tests/Helpers/GenerationTestResult.cs b/test/Analyzers.Tests/Helpers/GenerationTestResult.cs index 015911096..3f6ba9958 100644 --- a/test/Analyzers.Tests/Helpers/GenerationTestResult.cs +++ b/test/Analyzers.Tests/Helpers/GenerationTestResult.cs @@ -60,4 +60,9 @@ public void AssertGeneratedAsExpected(string expectedValue, params string[] expe } } } + + public void EnsureDiagnosticSeverity(DiagnosticSeverity severity = DiagnosticSeverity.Warning) + { + Diagnostics.Where(x => x.Severity >= severity).Should().HaveCount(0); + } } diff --git a/test/Analyzers.Tests/Helpers/GenerationTestResults.cs b/test/Analyzers.Tests/Helpers/GenerationTestResults.cs index 870f47819..ae315ce91 100644 --- a/test/Analyzers.Tests/Helpers/GenerationTestResults.cs +++ b/test/Analyzers.Tests/Helpers/GenerationTestResults.cs @@ -1,6 +1,9 @@ using System.Collections.Immutable; +using System.Reflection; +using System.Runtime.Loader; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; namespace Analyzers.Tests.Helpers; @@ -12,6 +15,11 @@ public record GenerationTestResults( ImmutableArray ResultDiagnostics ) { + public static implicit operator CSharpCompilation(GenerationTestResults results) + { + return results.ToFinalCompilation(); + } + public bool TryGetResult(Type type, [NotNullWhen(true)] out GenerationTestResult? result) { return Results.TryGetValue(type, out result); @@ -43,4 +51,32 @@ public void AssertGeneratedAsExpected(string expectedValue, params string[] e result.AssertGeneratedAsExpected(expectedValue); } + + public void AssertCompilationWasSuccessful() + { + Assert.Empty( + InputDiagnostics + .Where(z => !z.GetMessage().Contains("does not contain a definition for")) + .Where(z => !z.GetMessage().Contains("Assuming assembly reference")) + .Where(x => x.Severity >= DiagnosticSeverity.Warning) + ); + foreach (var result in Results.Values) + { + result.EnsureDiagnosticSeverity(); + } + } + + public void AssertGenerationWasSuccessful() + { + foreach (var item in Results.Values) + { + Assert.NotNull(item.Compilation); + item.EnsureDiagnosticSeverity(); + } + } + + public CSharpCompilation ToFinalCompilation() + { + return Results.Values.Aggregate(InputCompilation, (current, item) => current.AddSyntaxTrees(item.SyntaxTrees)); + } } diff --git a/test/Analyzers.Tests/Helpers/GeneratorTest.cs b/test/Analyzers.Tests/Helpers/GeneratorTest.cs index 6fe67e499..7752d8043 100644 --- a/test/Analyzers.Tests/Helpers/GeneratorTest.cs +++ b/test/Analyzers.Tests/Helpers/GeneratorTest.cs @@ -1,7 +1,9 @@ using System.Collections.Immutable; using System.Reflection; +using System.Runtime.Loader; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Rocket.Surgery.Conventions; @@ -10,14 +12,81 @@ namespace Analyzers.Tests.Helpers; +public static class AssemblyLoadContextCompilationHelpers +{ + public static Assembly EmitInto(this CSharpCompilation compilation, AssemblyLoadContext context, string? outputName = null) + { + using var stream = new MemoryStream(); + var emitResult = compilation.Emit(stream, options: new EmitOptions(outputNameOverride: outputName)); + if (!emitResult.Success) + { + Assert.Empty(emitResult.Diagnostics); + } + + var data = stream.ToArray(); + + using var assemblyStream = new MemoryStream(data); + return context.LoadFromStream(assemblyStream); + } + +// public static MetadataReference CreateMetadataReference(this CSharpCompilation compilation) +// { +// using var stream = new MemoryStream(); +// var emitResult = compilation.Emit(stream, options: new EmitOptions(outputNameOverride: compilation.AssemblyName)); +// if (!emitResult.Success) +// { +// Assert.Empty(emitResult.Diagnostics); +// } +// +// var data = stream.ToArray(); +// +// using var assemblyStream = new MemoryStream(data); +// return MetadataReference.CreateFromStream(assemblyStream, MetadataReferenceProperties.Assembly); +// } +} + +internal class CollectibleTestAssemblyLoadContext : AssemblyLoadContext, IDisposable +{ + public CollectibleTestAssemblyLoadContext() : base(true) + { + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + return null; + } + + public void Dispose() + { + Unload(); + } +} + public abstract class GeneratorTest : LoggerTest { private readonly HashSet _metadataReferences = new(ReferenceEqualityComparer.Instance); private readonly HashSet _generators = new(); private readonly List _sources = new(); - protected GeneratorTest(ITestOutputHelper testOutputHelper, LogLevel minLevel) : base(testOutputHelper, minLevel) + protected GeneratorTest( + ITestOutputHelper testOutputHelper, + LogLevel minLevel + ) : this(testOutputHelper, new CollectibleTestAssemblyLoadContext(), minLevel) + { + } + + protected GeneratorTest( + ITestOutputHelper testOutputHelper, + AssemblyLoadContext assemblyLoadContext, + LogLevel minLevel + ) : base(testOutputHelper, minLevel) { + AssemblyLoadContext = assemblyLoadContext; + if (assemblyLoadContext is IDisposable d) + { + Disposables.Add(d); + } + AddReferences( "mscorlib.dll", "netstandard.dll", @@ -37,6 +106,8 @@ protected GeneratorTest(ITestOutputHelper testOutputHelper, LogLevel minLevel) : ); } + public AssemblyLoadContext AssemblyLoadContext { get; } + protected GeneratorTest WithGenerator(Type type) { _generators.Add(type); @@ -145,7 +216,7 @@ public async Task GenerateAsync(params string[] sources) if (Logger.IsEnabled(LogLevel.Trace) && diagnostics is { Length: > 0 }) { results = results with { ResultDiagnostics = results.ResultDiagnostics.AddRange(diagnostics) }; - Logger.LogTrace("--- Diagnostics {Count} ---", sources.Length); + Logger.LogTrace("--- Diagnostics {Count} ---", diagnostics.Length); foreach (var d in diagnostics) Logger.LogTrace(" Reference: {Name}", d.ToString()); } @@ -166,4 +237,35 @@ public async Task GenerateAsync(params string[] sources) return results with { Results = builder.ToImmutable() }; } + + public Assembly? EmitAssembly(GenerationTestResults results, string? outputName = null) + { + var compilation = results.ToFinalCompilation(); + return EmitAssembly(compilation, outputName); + } + + public Assembly? EmitAssembly(Compilation compilation, string? outputName = null) + { + Logger.LogTrace("--- Emit Assembly {OutputName}---", outputName ?? string.Empty); + + var diagnostics = compilation.GetDiagnostics(); + + if (Logger.IsEnabled(LogLevel.Trace) && diagnostics is { Length: > 0 }) + { + Logger.LogTrace("--- Diagnostics {Count} ---", diagnostics.Length); + foreach (var d in diagnostics) + Logger.LogTrace(" Reference: {Name}", d.ToString()); + } + + using var stream = new MemoryStream(); + var emitResult = compilation.Emit(stream, options: new EmitOptions(outputNameOverride: outputName)); + if (!emitResult.Success) + { + return null; + } + + var data = stream.ToArray(); + using var assemblyStream = new MemoryStream(data); + return AssemblyLoadContext.LoadFromStream(assemblyStream); + } } diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.cs b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.cs new file mode 100644 index 000000000..c9ff5aec0 --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.cs @@ -0,0 +1,345 @@ +using Analyzers.Tests.Helpers; +using ImTools; +using MediatR; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.Logging; +using Rocket.Surgery.LaunchPad.Analyzers; +using Rocket.Surgery.LaunchPad.Foundation; + +namespace Analyzers.Tests; + +public class PropertyTrackingGeneratorTests : GeneratorTest +{ + [Fact] + public async Task Should_Require_Partial_Type_Declaration() + { + var source = @" +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; + +namespace Sample.Core.Operations.Rockets +{ + public class Request : IRequest + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + } + public class PatchRocket : IPropertyTracking, IRequest + { + public Guid Id { get; init; } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + var diagnostic = output!.Diagnostics.Should().HaveCount(1).And.Subject.First(); + diagnostic.Id.Should().Be("LPAD0001"); + diagnostic.ToString().Should().Contain("Type Sample.Core.Operations.Rockets.PatchRocket must be made partial."); + } + + [Fact] + public async Task Should_Require_Partial_Parent_Type_Declaration() + { + var source = @" +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; + +namespace Sample.Core.Operations.Rockets +{ + public record Request : IRequest + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + } + public static class PublicClass + { + public partial record PatchRocket : IPropertyTracking, IRequest + { + public Guid Id { get; init; } + } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + var diagnostic = output!.Diagnostics.Should().HaveCount(1).And.Subject.First(); + diagnostic.Id.Should().Be("LPAD0001"); + diagnostic.ToString().Should().Contain("Type Sample.Core.Operations.Rockets.PublicClass must be made partial."); + } + + [Fact] + public async Task Should_Generate_Record_With_Underlying_Properties_And_Track_Changes() + { + var source = @" +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; +using Sample.Core.Operations.Rockets; + +public record Request : IRequest +{ + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } +} +public partial record PatchRocket : IPropertyTracking, IRequest +{ +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + output!.Diagnostics.Should().HaveCount(0); + + var assembly = EmitAssembly(result).Should().NotBeNull().And.Subject; + var type = assembly.DefinedTypes.FindFirst(z => z.Name == "PatchRocket"); + var serialNumberProperty = type.GetProperty("SerialNumber")!; + var typeProperty = type.GetProperty("Type")!; + var idProperty = type.GetProperty("Id")!; + var getChangesMethod = type.GetMethod("GetChangedState")!; + var changesType = getChangesMethod.ReturnType; + var serialNumberChangedProperty = changesType.GetProperty("SerialNumber")!; + var typeChangedProperty = changesType.GetProperty("Type")!; + var idChangedProperty = changesType.GetProperty("Id")!; + var instance = Activator.CreateInstance(type); + + serialNumberProperty.SetValue(instance, new Assigned("12345")); + var changes = getChangesMethod.Invoke(instance, Array.Empty()); + + var serialNumberChanged = (bool)serialNumberChangedProperty.GetValue(changes)!; + serialNumberChanged.Should().BeTrue(); + + typeProperty.SetValue(instance, new Assigned(12345)); + changes = getChangesMethod.Invoke(instance, Array.Empty()); + + var typeChanged = (bool)typeChangedProperty.GetValue(changes)!; + typeChanged.Should().BeTrue(); + + idProperty.SetValue(instance, new Assigned(Guid.NewGuid())); + changes = getChangesMethod.Invoke(instance, Array.Empty()); + + var idChanged = (bool)idChangedProperty.GetValue(changes)!; + idChanged.Should().BeTrue(); + } + + + [Fact] + public async Task Should_Generate_Class_With_Underlying_Properties_And_Track_Changes() + { + var source = @" +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; +using Sample.Core.Operations.Rockets; + +public class Request : IRequest +{ + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } +} +public partial class PatchRocket : IPropertyTracking, IRequest +{ +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + output!.Diagnostics.Should().HaveCount(0); + + var assembly = EmitAssembly(result).Should().NotBeNull().And.Subject; + var type = assembly.DefinedTypes.FindFirst(z => z.Name == "PatchRocket"); + var serialNumberProperty = type.GetProperty("SerialNumber")!; + var typeProperty = type.GetProperty("Type")!; + var getChangesMethod = type.GetMethod("GetChangedState")!; + var changesType = getChangesMethod.ReturnType; + var serialNumberChangedProperty = changesType.GetProperty("SerialNumber")!; + var typeChangedProperty = changesType.GetProperty("Type")!; + var instance = Activator.CreateInstance(type); + + serialNumberProperty.SetValue(instance, new Assigned("12345")); + var changes = getChangesMethod.Invoke(instance, Array.Empty()); + + var serialNumberChanged = (bool)serialNumberChangedProperty.GetValue(changes)!; + serialNumberChanged.Should().BeTrue(); + + typeProperty.SetValue(instance, new Assigned(12345)); + changes = getChangesMethod.Invoke(instance, Array.Empty()); + + var typeChanged = (bool)typeChangedProperty.GetValue(changes)!; + typeChanged.Should().BeTrue(); + + changesType.GetProperty("Id").Should().BeNull(); + type.GetProperty("Id").Should().BeNull(); + } + + [Fact] + public async Task Should_Require_Same_Type_As_Record() + { + var source = @" +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; + +namespace Sample.Core.Operations.Rockets +{ + public record Request : IRequest + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + } + public partial class PatchRocket : IPropertyTracking, IRequest + { + public Guid Id { get; init; } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + var diagnostic = output!.Diagnostics.Should().HaveCount(1).And.Subject.First(); + diagnostic.Id.Should().Be("LPAD0005"); + diagnostic.ToString().Should().Contain("The declaration Sample.Core.Operations.Rockets.PatchRocket must be a record."); + } + + [Fact] + public async Task Should_Require_Same_Type_As_Class() + { + var source = @" +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; + +namespace Sample.Core.Operations.Rockets +{ + public class Request : IRequest + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + } + public partial record PatchRocket : IPropertyTracking, IRequest + { + public Guid Id { get; init; } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + var diagnostic = output!.Diagnostics.Should().HaveCount(1).And.Subject.First(); + diagnostic.Id.Should().Be("LPAD0005"); + diagnostic.ToString().Should().Contain("The declaration Sample.Core.Operations.Rockets.PatchRocket must be a class."); + } + + public PropertyTrackingGeneratorTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, LogLevel.Trace) + { + WithGenerator(); + AddReferences(typeof(IPropertyTracking<>), typeof(IMediator), typeof(IBaseRequest)); + AddSources( + @" +using System; +namespace Sample.Core.Operations.Rockets +{ + public class RocketModel + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + } +} +" + ); + } + + [Theory] + [InlineData("SerialNumber", "12345")] + [InlineData("Type", 12345)] + public async Task Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes(string property, object value) + { + var source = @" +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; +using Sample.Core.Operations.Rockets; + +public record Request : IRequest +{ + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } +} +public partial record PatchRocket : IPropertyTracking, IRequest +{ +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + output!.Diagnostics.Should().HaveCount(0); + + var assembly = EmitAssembly(result).Should().NotBeNull().And.Subject; + var type = assembly.DefinedTypes.FindFirst(z => z.Name == "PatchRocket"); + var applyChangesMethod = type.GetMethod("ApplyChanges")!; + var propertyUnderTest = type.GetProperty(property)!; + var requestType = assembly.DefinedTypes.FindFirst(z => z.Name == "Request"); + var requestPropertyUnderTest = requestType.GetProperty(property)!; + var otherRequestProperties = requestType.GetProperties().Where(z => z.Name != property); + var request = Activator.CreateInstance(requestType); + var instance = Activator.CreateInstance(type); + + var currentPropertyValues = otherRequestProperties.Select(z => z.GetValue(request)).ToArray(); + + var assignedType = typeof(Assigned<>).MakeGenericType(value.GetType()); + propertyUnderTest.SetValue(instance, Activator.CreateInstance(assignedType, value)); + request = applyChangesMethod.Invoke(instance, new[] { request }); + var r = requestPropertyUnderTest.GetValue(request); + r.Should().Be(value); + currentPropertyValues.Should().ContainInOrder(otherRequestProperties.Select(z => z.GetValue(request)).ToArray()); + } + + [Theory] + [InlineData("SerialNumber", "12345")] + [InlineData("Type", 12345)] + public async Task Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes(string property, object value) + { + var source = @" +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; +using Sample.Core.Operations.Rockets; + +public class Request : IRequest +{ + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } +} +public partial class PatchRocket : IPropertyTracking, IRequest +{ + public Guid Id { get; init; } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + output!.Diagnostics.Should().HaveCount(0); + + var assembly = EmitAssembly(result).Should().NotBeNull().And.Subject; + var type = assembly.DefinedTypes.FindFirst(z => z.Name == "PatchRocket"); + var applyChangesMethod = type.GetMethod("ApplyChanges")!; + var propertyUnderTest = type.GetProperty(property)!; + var requestType = assembly.DefinedTypes.FindFirst(z => z.Name == "Request"); + var requestPropertyUnderTest = requestType.GetProperty(property)!; + var otherRequestProperties = requestType.GetProperties().Where(z => z.Name != property); + var request = Activator.CreateInstance(requestType); + var instance = Activator.CreateInstance(type); + + var currentPropertyValues = otherRequestProperties.Select(z => z.GetValue(request)).ToArray(); + + var assignedType = typeof(Assigned<>).MakeGenericType(value.GetType()); + propertyUnderTest.SetValue(instance, Activator.CreateInstance(assignedType, value)); + applyChangesMethod.Invoke(instance, new[] { request }); + var r = requestPropertyUnderTest.GetValue(request); + r.Should().Be(value); + currentPropertyValues.Should().ContainInOrder(otherRequestProperties.Select(z => z.GetValue(request)).ToArray()); + } +} diff --git a/test/Extensions.Tests/Extensions.Tests.csproj b/test/Extensions.Tests/Extensions.Tests.csproj index 65d935774..70be8c877 100644 --- a/test/Extensions.Tests/Extensions.Tests.csproj +++ b/test/Extensions.Tests/Extensions.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/test/Sample.Core.Tests/Rockets/UpdateRocketTests.cs b/test/Sample.Core.Tests/Rockets/UpdateRocketTests.cs index 5e3367fc6..833e089bc 100644 --- a/test/Sample.Core.Tests/Rockets/UpdateRocketTests.cs +++ b/test/Sample.Core.Tests/Rockets/UpdateRocketTests.cs @@ -49,6 +49,42 @@ public async Task Should_Update_A_Rocket() response.Sn.Should().Be("43210987654321"); } + [Fact] + public async Task Should_Patch_A_Rocket() + { + var rocket = await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var rocket = new ReadyRocket + { + Type = RocketType.Falcon9, + SerialNumber = "12345678901234" + }; + z.Add(rocket); + + await z.SaveChangesAsync(); + return rocket; + } + ); + + var request = new EditRocket.PatchRequest + { + Id = rocket.Id, +// Type = RocketType.FalconHeavy, + SerialNumber = string.Join("", rocket.SerialNumber.Reverse()) + }.ResetChanges() with + { + Type = RocketType.FalconHeavy + }; + var response = await ServiceProvider.WithScoped().Invoke( + mediator => mediator.Send(request) + ); + + response.Type.Should().Be(RocketType.FalconHeavy); + response.Sn.Should().Be("12345678901234"); + } + public UpdateRocketTests(ITestOutputHelper outputHelper) : base(outputHelper, LogLevel.Trace) { } diff --git a/test/Sample.Graphql.Tests/Queries/mutations.graphql b/test/Sample.Graphql.Tests/Queries/mutations.graphql index 7632a59ef..e1b776523 100644 --- a/test/Sample.Graphql.Tests/Queries/mutations.graphql +++ b/test/Sample.Graphql.Tests/Queries/mutations.graphql @@ -1,25 +1,32 @@ -mutation CreateRocket($req: CreateRocketRequest) { - CreateRocket(request: $req) { +mutation CreateRocket($req: CreateRocketRequest!) { + createRocket(request: $req) { id } } -mutation UpdateRocket($req: EditRocketRequest) { - EditRocket(request: $req) { +mutation UpdateRocket($req: EditRocketRequest!) { + editRocket(request: $req) { id type - serialNumber: sn + serialNumber: sn } } -mutation DeleteRocket($req: DeleteRocketRequest) { - DeleteRocket(request: $req) +mutation PatchRocket($req: EditRocketPatchRequestInput!) { + patchRocket(request: $req) { + id + type + serialNumber: sn + } +} +mutation DeleteRocket($req: DeleteRocketRequest!) { + deleteRocket(request: $req) } -mutation CreateLaunchRecord($req: CreateLaunchRecordRequest) { - CreateLaunchRecord(request: $req) { +mutation CreateLaunchRecord($req: CreateLaunchRecordRequest!) { + createLaunchRecord(request: $req) { id } } -mutation UpdateLaunchRecord($req: EditLaunchRecordRequest) { - EditLaunchRecord(request: $req) { +mutation UpdateLaunchRecord($req: EditLaunchRecordRequest!) { + editLaunchRecord(request: $req) { id partner scheduledLaunchDate @@ -28,6 +35,6 @@ mutation UpdateLaunchRecord($req: EditLaunchRecordRequest) { rocketType } } -mutation DeleteLaunchRecord($req: DeleteLaunchRecordRequest) { - DeleteLaunchRecord(request: $req) +mutation DeleteLaunchRecord($req: DeleteLaunchRecordRequest!) { + deleteLaunchRecord(request: $req) } diff --git a/test/Sample.Graphql.Tests/Rockets/UpdateRocketTests.cs b/test/Sample.Graphql.Tests/Rockets/UpdateRocketTests.cs index 94bb76579..f37487457 100644 --- a/test/Sample.Graphql.Tests/Rockets/UpdateRocketTests.cs +++ b/test/Sample.Graphql.Tests/Rockets/UpdateRocketTests.cs @@ -40,6 +40,106 @@ public async Task Should_Update_A_Rocket() u.IsSuccessResult().Should().Be(true); } + [Fact] + public async Task Should_Patch_A_Rocket_SerialNumber() + { + var client = Factory.Services.GetRequiredService(); + + var rocket = await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var rocket = new ReadyRocket + { + Type = CoreRocketType.AtlasV, + SerialNumber = "12345678901234" + }; + z.Add(rocket); + + await z.SaveChangesAsync(); + return rocket; + } + ); + + var u = await client.PatchRocket.ExecuteAsync( + new EditRocketPatchRequestInput + { + Id = rocket.Id.Value, + SerialNumber = new() { Value = "123456789012345" } + } + ); + u.EnsureNoErrors(); + + u.Data!.PatchRocket.Type.Should().Be(RocketType.AtlasV); + u.Data!.PatchRocket.SerialNumber.Should().Be("123456789012345"); + } + + [Fact] + public async Task Should_Fail_To_Patch_A_Null_Rocket_SerialNumber() + { + var client = Factory.Services.GetRequiredService(); + + var rocket = await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var rocket = new ReadyRocket + { + Type = CoreRocketType.AtlasV, + SerialNumber = "12345678901234" + }; + z.Add(rocket); + + await z.SaveChangesAsync(); + return rocket; + } + ); + + + Func>> u = () => client.PatchRocket.ExecuteAsync( + new EditRocketPatchRequestInput + { + Id = rocket.Id.Value, + SerialNumber = new() { Value = null } + } + ); + await u.Should().ThrowAsync(); + } + + [Fact] + public async Task Should_Patch_A_Rocket_Type() + { + var client = Factory.Services.GetRequiredService(); + + var rocket = await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var rocket = new ReadyRocket + { + Type = CoreRocketType.AtlasV, + SerialNumber = "12345678901234" + }; + z.Add(rocket); + + await z.SaveChangesAsync(); + return rocket; + } + ); + + var u = await client.PatchRocket.ExecuteAsync( + new EditRocketPatchRequestInput + { + Id = rocket.Id.Value, + Type = new() { Value = RocketType.FalconHeavy } + } + ); + u.EnsureNoErrors(); + + u.Data!.PatchRocket.Type.Should().Be(RocketType.FalconHeavy); + u.Data!.PatchRocket.SerialNumber.Should().Be("12345678901234"); + } + public UpdateRocketTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } diff --git a/test/Sample.Graphql.Tests/schema.graphql b/test/Sample.Graphql.Tests/schema.graphql index a9b480973..796ae8676 100644 --- a/test/Sample.Graphql.Tests/schema.graphql +++ b/test/Sample.Graphql.Tests/schema.graphql @@ -3,6 +3,8 @@ mutation: Mutation } +scalar Void + scalar UUID "Represents a time zone - a mapping between UTC and local time.\nA time zone maps UTC instants to local times - or, equivalently, to the offset from UTC at any particular instant." @@ -44,25 +46,21 @@ scalar Period "A LocalDateTime in a specific time zone and with a particular offset to distinguish between otherwise-ambiguous instants.\nA ZonedDateTime is global, in that it maps to a single Instant." scalar ZonedDateTime -"Returns assembly information for the given application" +"GraphQL operations are hierarchical and composed, describing a tree of information.\nWhile Scalar types describe the leaf values of these hierarchical operations,\nObjects describe the intermediate levels.\n \nGraphQL Objects represent a list of named fields, each of which yield a value of a\nspecific type. Object values should be serialized as ordered maps, where the selected\nfield names (or aliases) are the keys and the result of evaluating the field is the value,\nordered by the order in which they appear in the selection set.\n \nAll fields defined within an Object type must not have a name which begins\nwith \"__\" (two underscores), as this is used exclusively by\nGraphQL’s introspection system." type Query { launchRecords(skip: Int take: Int "Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: LaunchRecordFilterInput order: [LaunchRecordSortInput!]): LaunchRecordCollectionSegment rockets(skip: Int take: Int "Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ReadyRocketFilterInput order: [ReadyRocketSortInput!]): ReadyRocketCollectionSegment - "Get the assembly version information" - version: AssemblyInfo! } "GraphQL operations are hierarchical and composed, describing a tree of information.\nWhile Scalar types describe the leaf values of these hierarchical operations,\nObjects describe the intermediate levels.\n \nGraphQL Objects represent a list of named fields, each of which yield a value of a\nspecific type. Object values should be serialized as ordered maps, where the selected\nfield names (or aliases) are the keys and the result of evaluating the field is the value,\nordered by the order in which they appear in the selection set.\n \nAll fields defined within an Object type must not have a name which begins\nwith \"__\" (two underscores), as this is used exclusively by\nGraphQL’s introspection system." type Mutation { - CreateRocket(request: CreateRocketRequest): CreateRocketResponse - DeleteRocket(request: DeleteRocketRequest): Void - EditRocket(request: EditRocketRequest): RocketModel - GetRocket(request: GetRocketRequest): RocketModel - GetRocketLaunchRecord(request: GetRocketLaunchRecordRequest): LaunchRecordModel - CreateLaunchRecord(request: CreateLaunchRecordRequest): CreateLaunchRecordResponse - DeleteLaunchRecord(request: DeleteLaunchRecordRequest): Void - EditLaunchRecord(request: EditLaunchRecordRequest): LaunchRecordModel - GetLaunchRecord(request: GetLaunchRecordRequest): LaunchRecordModel + createRocket(request: CreateRocketRequest!): CreateRocketResponse! + editRocket(request: EditRocketRequest!): RocketModel! + patchRocket(request: EditRocketPatchRequestInput!): RocketModel! + deleteRocket(request: DeleteRocketRequest!): Void! + createLaunchRecord(request: CreateLaunchRecordRequest!): CreateLaunchRecordResponse! + editLaunchRecord(request: EditLaunchRecordRequest!): LaunchRecordModel! + deleteLaunchRecord(request: DeleteLaunchRecordRequest!): Void! } "A rocket in inventory" @@ -83,8 +81,6 @@ type LaunchRecord { scheduledLaunchDate: DateTime! } -scalar Void - input LaunchRecordFilterInput { and: [LaunchRecordFilterInput!] or: [LaunchRecordFilterInput!] @@ -250,18 +246,42 @@ type CollectionSegmentInfo { hasPreviousPage: Boolean! } -"The identifier of the rocket that was created" -type CreateRocketResponse { - "The rocket id" - id: UUID! +"The available rocket types" +enum RocketType { + "Your best bet" + FALCON9 + "For those huge payloads" + FALCON_HEAVY + "We stole our competitors rocket platform!" + ATLAS_V } -"The operation to create a new rocket record" -input CreateRocketRequest { - "The serial number of the rocket" - serialNumber: String! - "The type of rocket" - type: RocketType! +"The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." +scalar Long + +"The `DateTime` scalar represents an ISO-8601 compliant date time type." +scalar DateTime + +"Create a launch record" +input CreateLaunchRecordRequest { + "The rocket to use" + rocketId: UUID! + "The launch partner" + partner: String + "The launch partners payload" + payload: String + "The payload weight" + payloadWeightKg: Float! + "The actual launch date" + actualLaunchDate: Instant + "The intended launch date" + scheduledLaunchDate: Instant! +} + +"The launch record creation response" +type CreateLaunchRecordResponse { + "The id of the new launch record" + id: UUID! } "The request to remove a rocket from the system" @@ -270,6 +290,23 @@ input DeleteRocketRequest { id: UUID! } +input EditRocketPatchRequestInput { + "The rocket id" + id: UUID! + serialNumber: AssignedOfStringInput! + type: AssignedOfRocketTypeInput! +} + +"The edit operation to update a rocket" +input EditRocketRequest { + "The rocket id" + id: UUID! + "The serial number of the rocket" + serialNumber: String! + "The type of the rocket" + type: RocketType! +} + "The details of a given rocket" type RocketModel { "The unique rocket identifier" @@ -280,18 +317,16 @@ type RocketModel { type: RocketType! } -"The edit operation to update a rocket" -input EditRocketRequest { - "The rocket id" - id: UUID! +"The operation to create a new rocket record" +input CreateRocketRequest { "The serial number of the rocket" serialNumber: String! - "The type of the rocket" + "The type of rocket" type: RocketType! } -"Request to fetch information about a rocket" -input GetRocketRequest { +"The identifier of the rocket that was created" +type CreateRocketResponse { "The rocket id" id: UUID! } @@ -316,41 +351,6 @@ type LaunchRecordModel { rocketType: RocketType! } -input GetRocketLaunchRecordRequest { - "The rocket id" - id: UUID! - "The launch record id" - launchRecordId: UUID! -} - -"The launch record creation response" -type CreateLaunchRecordResponse { - "The id of the new launch record" - id: UUID! -} - -"Create a launch record" -input CreateLaunchRecordRequest { - "The rocket to use" - rocketId: UUID! - "The launch partner" - partner: String - "The launch partners payload" - payload: String - "The payload weight" - payloadWeightKg: Float! - "The actual launch date" - actualLaunchDate: Instant - "The intended launch date" - scheduledLaunchDate: Instant! -} - -"The request to delete a launch record" -input DeleteLaunchRecordRequest { - "The launch record to delete" - id: UUID! -} - "The launch record update request" input EditLaunchRecordRequest { "The launch record to update" @@ -369,55 +369,16 @@ input EditLaunchRecordRequest { rocketId: UUID! } -"The request to get a launch record" -input GetLaunchRecordRequest { - "The launch record to find" +"The request to delete a launch record" +input DeleteLaunchRecordRequest { + "The launch record to delete" id: UUID! } -"The available rocket types" -enum RocketType { - "Your best bet" - FALCON9 - "For those huge payloads" - FALCON_HEAVY - "We stole our competitors rocket platform!" - ATLAS_V +input AssignedOfRocketTypeInput { + value: RocketType! } -"The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." -scalar Long - -"The `DateTime` scalar represents an ISO-8601 compliant date time type." -scalar DateTime - -"The assembly info object" -type AssemblyInfo { - "The assembly version" - version: String - "The assembly created date" - created: Instant - "The assembly updated date" - updated: Instant - "The assembly company" - company: String - "The configuration the assembly was built with" - configuration: String - "The assembly copyright" - copyright: String - "The assembly description" - description: String - "The assembly product" - product: String - "The assembly title" - title: String - "The assembly trademark" - trademark: String - "The assembly metadata" - metadata: [KeyValuePairOfStringAndString!]! -} - -type KeyValuePairOfStringAndString { - key: String! +input AssignedOfStringInput { value: String! } \ No newline at end of file diff --git a/test/Sample.Restful.Tests/Rockets/UpdateRocketTests.cs b/test/Sample.Restful.Tests/Rockets/UpdateRocketTests.cs index 052374851..092c031e6 100644 --- a/test/Sample.Restful.Tests/Rockets/UpdateRocketTests.cs +++ b/test/Sample.Restful.Tests/Rockets/UpdateRocketTests.cs @@ -6,6 +6,7 @@ using Xunit; using Xunit.Abstractions; using RocketType = Sample.Core.Domain.RocketType; +using ClientRocketType = Sample.Restful.Client.RocketType; namespace Sample.Restful.Tests.Rockets; @@ -36,16 +37,120 @@ public async Task Should_Update_A_Rocket() rocket.Id.Value, new EditRocketRequest { - Type = Client.RocketType.FalconHeavy, + Type = ClientRocketType.FalconHeavy, SerialNumber = string.Join("", rocket.SerialNumber.Reverse()) } ); ( u.StatusCode is >= 200 and < 300 ).Should().BeTrue(); - // var response = await client.GetRocketAsync(rocket.Id); - // - // response.Type.Should().Be(RocketType.FalconHeavy); - // response.Sn.Should().Be("43210987654321"); + var response = await client.GetRocketAsync(rocket.Id.Value); + + response.Result.Type.Should().Be(ClientRocketType.FalconHeavy); + response.Result.Sn.Should().Be("43210987654321"); + } + + [Fact] + public async Task Should_Patch_A_Rocket_SerialNumber() + { + var client = new RocketClient(Factory.CreateClient()); + + var rocket = await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var rocket = new ReadyRocket + { + Type = RocketType.AtlasV, + SerialNumber = "12345678901234" + }; + z.Add(rocket); + + await z.SaveChangesAsync(); + return rocket; + } + ); + + var u = await client.PatchRocketAsync( + rocket.Id.Value, + new EditRocketPatchRequest + { + SerialNumber = new() { Value = "123456789012345" } + } + ); + ( u.StatusCode is >= 200 and < 300 ).Should().BeTrue(); + + var response = await client.GetRocketAsync(rocket.Id.Value); + + response.Result.Type.Should().Be(ClientRocketType.AtlasV); + response.Result.Sn.Should().Be("123456789012345"); + } + + [Fact] + public async Task Should_Fail_To_Patch_A_Null_Rocket_SerialNumber() + { + var client = new RocketClient(Factory.CreateClient()); + + var rocket = await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var rocket = new ReadyRocket + { + Type = RocketType.AtlasV, + SerialNumber = "12345678901234" + }; + z.Add(rocket); + + await z.SaveChangesAsync(); + return rocket; + } + ); + + Func>> action = () => client.PatchRocketAsync( + rocket.Id.Value, + new EditRocketPatchRequest + { + SerialNumber = new() { Value = null } + } + ); + var result = ( await action.Should().ThrowAsync>() ).And.Result; + result.Errors.Should().HaveCount(1); + result.Errors["SerialNumber"][0].ErrorMessage.Should().Be("'Serial Number' must not be empty."); + } + + [Fact] + public async Task Should_Patch_A_Rocket_Type() + { + var client = new RocketClient(Factory.CreateClient()); + + var rocket = await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var rocket = new ReadyRocket + { + Type = RocketType.AtlasV, + SerialNumber = "12345678901234" + }; + z.Add(rocket); + + await z.SaveChangesAsync(); + return rocket; + } + ); + + var u = await client.PatchRocketAsync( + rocket.Id.Value, + new EditRocketPatchRequest + { + Type = new() { Value = ClientRocketType.FalconHeavy } + } + ); + ( u.StatusCode is >= 200 and < 300 ).Should().BeTrue(); + + var response = await client.GetRocketAsync(rocket.Id.Value); + response.Result.Type.Should().Be(ClientRocketType.FalconHeavy); + response.Result.Sn.Should().Be("12345678901234"); } public UpdateRocketTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) From 803815f07e80e4a006f8ffb10bfafc51e681a96d Mon Sep 17 00:00:00 2001 From: David Driscoll Date: Wed, 13 Apr 2022 00:54:08 -0400 Subject: [PATCH 2/3] Added source generator to generator patch objects for graphql queries. --- .../LaunchRecords/EditLaunchRecord.cs | 37 +- .../Operations/Rockets/EditRocket.cs | 4 +- sample/Sample.Graphql/Startup.cs | 98 ++- .../Controllers/LaunchRecordController.cs | 10 + ...raphqlOptionalPropertyTrackingGenerator.cs | 328 ++++++++++ src/Analyzers/PropertyTrackingGenerator.cs | 196 +++--- src/Analyzers/SyntaxExtensions.cs | 5 +- src/Foundation/Assigned.cs | 159 +++++ src/Foundation/IPropertyTracking.cs | 109 ---- src/Foundation/PatchRequestHandler.cs | 40 ++ src/HotChocolate/GraphqlExtensions.cs | 103 ++- src/HotChocolate/IPropertyTracking.cs | 24 + test/Analyzers.Tests/Analyzers.Tests.csproj | 1 + ...y=SerialNumber_value=12345.00.verified.txt | 5 + ...erialNumber_value=12345.01.verified.cs.txt | 35 ++ ..._property=Type_value=12345.00.verified.txt | 5 + ...operty=Type_value=12345.01.verified.cs.txt | 35 ++ ...y=SerialNumber_value=12345.00.verified.txt | 5 + ...erialNumber_value=12345.01.verified.cs.txt | 35 ++ ..._property=Type_value=12345.00.verified.txt | 5 + ...operty=Type_value=12345.01.verified.cs.txt | 35 ++ ...y=SerialNumber_value=12345.00.verified.txt | 5 + ...erialNumber_value=12345.01.verified.cs.txt | 35 ++ ..._property=Type_value=12345.00.verified.txt | 5 + ...operty=Type_value=12345.01.verified.cs.txt | 35 ++ ...y=SerialNumber_value=12345.00.verified.txt | 5 + ...erialNumber_value=12345.01.verified.cs.txt | 42 ++ ..._property=Type_value=12345.00.verified.txt | 5 + ...operty=Type_value=12345.01.verified.cs.txt | 42 ++ ...al_Parent_Type_Declaration.00.verified.txt | 34 + ...Parent_Type_Declaration.01.received.cs.txt | 41 ++ ...Parent_Type_Declaration.01.verified.cs.txt | 41 ++ ...uire_Partial_Type_Declaration.verified.txt | 34 + ...ld_Require_Same_Type_As_Class.verified.txt | 34 + ...d_Require_Same_Type_As_Record.verified.txt | 34 + ...le_Builtin_Struct_Property.00.verified.txt | 5 + ...Builtin_Struct_Property.01.verified.cs.txt | 38 ++ ...rt_Nullable_Class_Property.00.verified.txt | 5 + ...Nullable_Class_Property.01.received.cs.txt | 38 ++ ...Nullable_Class_Property.01.verified.cs.txt | 38 ++ ...ble_Custom_Struct_Property.00.received.txt | 5 + ..._Custom_Struct_Property.01.received.cs.txt | 46 ++ ...ort_Nullable_Enum_Property.00.verified.txt | 5 + ..._Nullable_Enum_Property.01.verified.cs.txt | 46 ++ ...t_Nullable_Struct_Property.00.verified.txt | 5 + ...ullable_Struct_Property.01.verified.cs.txt | 38 ++ ...lOptionalPropertyTrackingGeneratorTests.cs | 586 ++++++++++++++++++ test/Analyzers.Tests/Helpers/GeneratorTest.cs | 6 + ...rate_With_Method_For_Class.00.verified.txt | 5 + ...e_With_Method_For_Class.01.verified.cs.txt | 19 + ...ate_With_Method_For_Record.00.verified.txt | 5 + ..._With_Method_For_Record.01.verified.cs.txt | 19 + ...d_For_Record_That_Inherits.00.verified.txt | 5 + ...or_Record_That_Inherits.01.verified.cs.txt | 17 + ...ple_With_Method_For_Record.00.verified.txt | 5 + ..._With_Method_For_Record.01.verified.cs.txt | 22 + ...al_Parent_Type_Declaration.00.verified.txt | 34 + ...Parent_Type_Declaration.01.verified.cs.txt | 19 + ...uire_Partial_Type_Declaration.verified.txt | 34 + .../InheritFromGeneratorTests.cs | 13 + ...y=SerialNumber_value=12345.00.verified.txt | 5 + ...erialNumber_value=12345.01.verified.cs.txt | 50 ++ ..._property=Type_value=12345.00.verified.txt | 5 + ...operty=Type_value=12345.01.verified.cs.txt | 50 ++ ...operties_And_Track_Changes.00.verified.txt | 5 + ...rties_And_Track_Changes.01.verified.cs.txt | 50 ++ ...y=SerialNumber_value=12345.00.verified.txt | 5 + ...erialNumber_value=12345.01.verified.cs.txt | 59 ++ ..._property=Type_value=12345.00.verified.txt | 5 + ...operty=Type_value=12345.01.verified.cs.txt | 59 ++ ...operties_And_Track_Changes.00.verified.txt | 5 + ...rties_And_Track_Changes.01.verified.cs.txt | 59 ++ ...al_Parent_Type_Declaration.00.verified.txt | 34 + ...Parent_Type_Declaration.01.verified.cs.txt | 56 ++ ...uire_Partial_Type_Declaration.verified.txt | 34 + ...ld_Require_Same_Type_As_Class.verified.txt | 34 + ...d_Require_Same_Type_As_Record.verified.txt | 34 + ...rt_Nullable_Class_Property.00.verified.txt | 5 + ...Nullable_Class_Property.01.verified.cs.txt | 53 ++ ...t_Nullable_Struct_Property.00.verified.txt | 5 + ...ullable_Struct_Property.01.verified.cs.txt | 53 ++ .../PropertyTrackingGeneratorTests.cs | 107 ++-- .../Rockets/UpdateRocketTests.cs | 22 +- test/Sample.Graphql.Tests/schema.graphql | 32 +- 84 files changed, 3245 insertions(+), 305 deletions(-) create mode 100644 src/Analyzers/GraphqlOptionalPropertyTrackingGenerator.cs create mode 100644 src/Foundation/Assigned.cs create mode 100644 src/Foundation/PatchRequestHandler.cs create mode 100644 src/HotChocolate/IPropertyTracking.cs create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=Type_value=12345.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=Type_value=12345.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.received.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Type_Declaration.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Class.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Record.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Builtin_Struct_Property.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Builtin_Struct_Property.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.01.received.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Custom_Struct_Property.00.received.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Custom_Struct_Property.01.received.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Enum_Property.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Enum_Property.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.00.verified.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/GraphqlOptionalPropertyTrackingGeneratorTests.cs create mode 100644 test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Class.00.verified.txt create mode 100644 test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Class.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record.00.verified.txt create mode 100644 test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record_That_Inherits.00.verified.txt create mode 100644 test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record_That_Inherits.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/InheritFromGeneratorTests.Should_Inherit_Multiple_With_Method_For_Record.00.verified.txt create mode 100644 test/Analyzers.Tests/InheritFromGeneratorTests.Should_Inherit_Multiple_With_Method_For_Record.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/InheritFromGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.00.verified.txt create mode 100644 test/Analyzers.Tests/InheritFromGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/InheritFromGeneratorTests.Should_Require_Partial_Type_Declaration.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.00.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.00.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Track_Changes.00.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Track_Changes.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.00.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.00.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Track_Changes.00.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Track_Changes.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.00.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Partial_Type_Declaration.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Class.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Record.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.00.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.01.verified.cs.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.00.verified.txt create mode 100644 test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.01.verified.cs.txt diff --git a/sample/Sample.Core/Operations/LaunchRecords/EditLaunchRecord.cs b/sample/Sample.Core/Operations/LaunchRecords/EditLaunchRecord.cs index 4bf29c68b..80d845fb4 100644 --- a/sample/Sample.Core/Operations/LaunchRecords/EditLaunchRecord.cs +++ b/sample/Sample.Core/Operations/LaunchRecords/EditLaunchRecord.cs @@ -6,6 +6,7 @@ using Rocket.Surgery.LaunchPad.Foundation; using Sample.Core.Domain; using Sample.Core.Models; +using Sample.Core.Operations.Rockets; namespace Sample.Core.Operations.LaunchRecords; @@ -53,6 +54,14 @@ public partial record Request : IRequest public RocketId RocketId { get; set; } // TODO: Make generator that can be used to create a writable view model } + public partial record PatchRequest : IRequest, IPropertyTracking + { + /// + /// The rocket id + /// + public LaunchRecordId Id { get; init; } + } + private class Mapper : Profile { public Mapper() @@ -94,29 +103,41 @@ public Validator() } } - private class Handler : IRequestHandler + private class Handler : PatchRequestHandler, IRequestHandler { private readonly RocketDbContext _dbContext; private readonly IMapper _mapper; - public Handler(RocketDbContext dbContext, IMapper mapper) + public Handler(RocketDbContext dbContext, IMapper mapper, IMediator mediator) : base(mediator) { _dbContext = dbContext; _mapper = mapper; } - public async Task Handle(Request request, CancellationToken cancellationToken) + private async Task GetLaunchRecord(LaunchRecordId id, CancellationToken cancellationToken) { - var rocket - = await _dbContext.LaunchRecords - .Include(z => z.Rocket) - .FirstOrDefaultAsync(z => z.Id == request.Id, cancellationToken) - .ConfigureAwait(false); + var rocket = await _dbContext.LaunchRecords + .Include(z => z.Rocket) + .FirstOrDefaultAsync(z => z.Id == id, cancellationToken) + .ConfigureAwait(false); if (rocket == null) { throw new NotFoundException(); } + return rocket; + } + + protected override async Task GetRequest(PatchRequest patchRequest, CancellationToken cancellationToken) + { + var rocket = await GetLaunchRecord(patchRequest.Id, cancellationToken); + return _mapper.Map(_mapper.Map(rocket)); + } + + public async Task Handle(Request request, CancellationToken cancellationToken) + { + var rocket = await GetLaunchRecord(request.Id, cancellationToken); + _mapper.Map(request, rocket); _dbContext.Update(rocket); await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/sample/Sample.Core/Operations/Rockets/EditRocket.cs b/sample/Sample.Core/Operations/Rockets/EditRocket.cs index f2e5e2354..69c7c4019 100644 --- a/sample/Sample.Core/Operations/Rockets/EditRocket.cs +++ b/sample/Sample.Core/Operations/Rockets/EditRocket.cs @@ -72,12 +72,12 @@ public RequestValidator() } } - private class Handler : PatchHandlerBase, IRequestHandler + private class RequestHandler : PatchRequestHandler, IRequestHandler { private readonly RocketDbContext _dbContext; private readonly IMapper _mapper; - public Handler(RocketDbContext dbContext, IMapper mapper, IMediator mediator) : base(mediator) + public RequestHandler(RocketDbContext dbContext, IMapper mapper, IMediator mediator) : base(mediator) { _dbContext = dbContext; _mapper = mapper; diff --git a/sample/Sample.Graphql/Startup.cs b/sample/Sample.Graphql/Startup.cs index 5e38da17f..24a9444c2 100644 --- a/sample/Sample.Graphql/Startup.cs +++ b/sample/Sample.Graphql/Startup.cs @@ -1,10 +1,20 @@ -using HotChocolate; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq.Expressions; +using System.Reflection; +using HotChocolate; +using HotChocolate.Configuration; using HotChocolate.Data.Filters; using HotChocolate.Data.Sorting; using HotChocolate.Types; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Definitions; using HotChocolate.Types.Pagination; +using HotChocolate.Utilities; using MediatR; using Rocket.Surgery.LaunchPad.AspNetCore; +using Rocket.Surgery.LaunchPad.Foundation; +using Rocket.Surgery.LaunchPad.HotChocolate; using Sample.Core.Domain; using Sample.Core.Models; using Sample.Core.Operations.LaunchRecords; @@ -21,14 +31,12 @@ public void ConfigureServices(IServiceCollection services) { services .AddGraphQLServer() + .ConfigureStronglyTypedId() + .ConfigureStronglyTypedId() // .AddDefaultTransactionScopeHandler() .AddQueryType() .AddMutationType() .ModifyRequestOptions(options => options.IncludeExceptionDetails = true) - .AddTypeConverter(source => source.Value) - .AddTypeConverter(source => new RocketId(source)) - .AddTypeConverter(source => source.Value) - .AddTypeConverter(source => new LaunchRecordId(source)) .ConfigureSchema( s => { @@ -37,17 +45,11 @@ public void ConfigureServices(IServiceCollection services) s.AddType(); s.AddType(); s.AddType(); - - s.BindClrType(); - s.BindClrType(); - s.BindRuntimeType(ScalarNames.UUID); - s.BindRuntimeType(ScalarNames.UUID); } ) .AddSorting() .AddFiltering() - .AddProjections() - .AddConvention(); + .AddProjections(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -69,6 +71,62 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) } } +internal class StronglyTypedIdChangeTypeProvider : IChangeTypeProvider +{ + private readonly Dictionary<(Type, Type), ChangeType> _typeMap = new(); + + public void AddTypeConversion(Type strongIdType) + { + var underlyingType = strongIdType.GetProperty("Value")!.PropertyType; + var value = Expression.Parameter(typeof(object), "value"); + // .AddTypeConverter(source => source.Value) + + if (!_typeMap.ContainsKey(( strongIdType, underlyingType ))) + { + _typeMap.Add( + ( strongIdType, underlyingType ), + Expression.Lambda( + Expression.Convert(Expression.Property(Expression.Convert(value, strongIdType), "Value"), typeof(object)), false, value + ).Compile() + ); + } + + if (!_typeMap.ContainsKey(( underlyingType, strongIdType ))) + { + _typeMap.Add( + ( underlyingType, strongIdType ), + Expression.Lambda( + Expression.Convert( + Expression.New(strongIdType.GetConstructor(new[] { underlyingType })!, Expression.Convert(value, underlyingType)), typeof(object) + ), false, value + ).Compile() + ); + } + } + + public bool TryCreateConverter(Type source, Type target, ChangeTypeProvider root, [NotNullWhen(true)] out ChangeType? converter) + { + if (_typeMap.TryGetValue(( source, target ), out var @delegate)) + { + converter = input => input is null ? default : @delegate(input); + return true; + } + + converter = null; + return false; + } +} + +public partial record EditRocketPatchRequest : IOptionalTracking +{ + public RocketId Id { get; init; } +} + +public partial record EditLaunchRecordPatchRequest : IOptionalTracking +{ + public LaunchRecordId Id { get; init; } +} + [ExtendObjectType(OperationTypeNames.Mutation)] public class RocketMutation { @@ -85,9 +143,9 @@ public Task EditRocket([Service] IMediator mediator, CancellationTo } [UseServiceScope] - public Task PatchRocket([Service] IMediator mediator, CancellationToken cancellationToken, EditRocket.PatchRequest request) + public Task PatchRocket([Service] IMediator mediator, CancellationToken cancellationToken, EditRocketPatchRequest request) { - return mediator.Send(request, cancellationToken); + return mediator.Send(request.Create(), cancellationToken); } [UseServiceScope] @@ -115,17 +173,15 @@ public Task EditLaunchRecord([Service] IMediator mediator, Ca } [UseServiceScope] - public Task DeleteLaunchRecord([Service] IMediator mediator, CancellationToken cancellationToken, DeleteLaunchRecord.Request request) + public Task PatchLaunchRecord([Service] IMediator mediator, CancellationToken cancellationToken, EditLaunchRecordPatchRequest request) { - return mediator.Send(request, cancellationToken); + return mediator.Send(request.Create(), cancellationToken); } -} -[ExtendObjectType(OperationTypeNames.Mutation)] -public class MutationType : ObjectTypeExtension -{ - protected override void Configure(IObjectTypeDescriptor descriptor) + [UseServiceScope] + public Task DeleteLaunchRecord([Service] IMediator mediator, CancellationToken cancellationToken, DeleteLaunchRecord.Request request) { + return mediator.Send(request, cancellationToken); } } diff --git a/sample/Sample.Restful/Controllers/LaunchRecordController.cs b/sample/Sample.Restful/Controllers/LaunchRecordController.cs index c0f13b685..1fc9704b2 100644 --- a/sample/Sample.Restful/Controllers/LaunchRecordController.cs +++ b/sample/Sample.Restful/Controllers/LaunchRecordController.cs @@ -44,6 +44,16 @@ public partial class LaunchRecordController : RestfulApiController // ReSharper disable once RouteTemplates.ParameterTypeAndConstraintsMismatch public partial Task EditLaunchRecord([BindRequired] [FromRoute] LaunchRecordId id, EditLaunchRecord.Request model); + /// + /// Update a given launch record + /// + /// The id of the launch record + /// The request details + /// + [HttpPatch("{id:guid}")] + // ReSharper disable once RouteTemplates.ParameterTypeAndConstraintsMismatch + public partial Task PatchLaunchRecord([BindRequired] [FromRoute] LaunchRecordId id, EditLaunchRecord.PatchRequest model); + /// /// Remove a launch record /// diff --git a/src/Analyzers/GraphqlOptionalPropertyTrackingGenerator.cs b/src/Analyzers/GraphqlOptionalPropertyTrackingGenerator.cs new file mode 100644 index 000000000..3b8574d74 --- /dev/null +++ b/src/Analyzers/GraphqlOptionalPropertyTrackingGenerator.cs @@ -0,0 +1,328 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Rocket.Surgery.LaunchPad.Analyzers; + +/// +/// A generator that is used to copy properties, fields and methods from one type onto another. +/// +[Generator] +public class GraphqlOptionalPropertyTrackingGenerator : IIncrementalGenerator +{ + private static void GeneratePropertyTracking( + SourceProductionContext context, + TypeDeclarationSyntax declaration, + INamedTypeSymbol symbol, + INamedTypeSymbol targetSymbol + ) + { + if (!declaration.Modifiers.Any(z => z.IsKind(SyntaxKind.PartialKeyword))) + { + context.ReportDiagnostic( + Diagnostic.Create(GeneratorDiagnostics.MustBePartial, declaration.Identifier.GetLocation(), declaration.GetFullMetadataName()) + ); + return; + } + + var isRecord = declaration is RecordDeclarationSyntax; + + if (targetSymbol.IsRecord != isRecord) + { + context.ReportDiagnostic( + Diagnostic.Create( + GeneratorDiagnostics.ParameterMustBeSameTypeOfObject, declaration.Keyword.GetLocation(), declaration.GetFullMetadataName(), + declaration.Keyword.IsKind(SyntaxKind.ClassKeyword) ? "record" : "class" + ) + ); + return; + } + + var classToInherit = declaration + .WithMembers(List()) + .WithAttributeLists(List()) + .WithConstraintClauses(List()) + .WithBaseList(null); + + var writeableProperties = + targetSymbol.GetMembers() + .OfType() + .Where(z => z is { IsStatic: false, IsIndexer: false, IsReadOnly: false }); + if (!targetSymbol.IsRecord) + { + // not able to use with operator, so ignore any init only properties. + writeableProperties = writeableProperties.Where(z => z is { SetMethod.IsInitOnly: false, GetMethod.IsReadOnly: false }); + } + + + writeableProperties = writeableProperties + // only works for `set`able properties not init only + .Where(z => !symbol.GetMembers(z.Name).Any()) + .ToArray(); + var existingMembers = targetSymbol.GetMembers() + .OfType() + .Where(z => z is { IsStatic: false, IsIndexer: false, IsReadOnly: false }) + .Where(z => symbol.GetMembers(z.Name).Any()) + .Except(writeableProperties); + + var createBody = Block() + .AddStatements( + LocalDeclarationStatement( + VariableDeclaration( + IdentifierName(Identifier(TriviaList(), SyntaxKind.VarKeyword, "var", "var", TriviaList())) + ).WithVariables( + SingletonSeparatedList( + VariableDeclarator(Identifier("value")) + .WithInitializer( + EqualsValueClause( + ObjectCreationExpression( + IdentifierName(Identifier(targetSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))) + ) + .WithInitializer( + InitializerExpression( + SyntaxKind.ObjectInitializerExpression, + SeparatedList( + existingMembers + .Select( + z => + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(z.Name), + IdentifierName(z.Name) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ); + + var namespaces = new HashSet(); + + static void AddNamespacesFromPropertyType(HashSet namespaces, ITypeSymbol symbol) + { + namespaces.Add(symbol.ContainingNamespace.GetFullMetadataName()); + if (symbol is INamedTypeSymbol namedTypeSymbol) + { + if (namedTypeSymbol.IsGenericType) + { + foreach (var genericType in namedTypeSymbol.TypeArguments) + { + AddNamespacesFromPropertyType(namespaces, genericType); + } + } + } + } + + foreach (var propertySymbol in writeableProperties) + { + TypeSyntax type; + ITypeSymbol propertyType; + propertyType = propertySymbol.Type is INamedTypeSymbol + { + Name: "Assigned", ContainingAssembly.Name: "Rocket.Surgery.LaunchPad.Foundation" + } namedTypeSymbol + ? namedTypeSymbol.TypeArguments[0] + : propertySymbol.Type; + type = ParseTypeName(propertyType.ToDisplayString(NullableFlowState.MaybeNull, SymbolDisplayFormat.MinimallyQualifiedFormat)); + if (propertyType is { TypeKind: TypeKind.Struct or TypeKind.Enum } && type is not NullableTypeSyntax) + { + type = NullableType(type); + } + + AddNamespacesFromPropertyType(namespaces, propertyType); + +// classToInherit = classToInherit.AddMembers(GenerateTrackingProperties(propertySymbol, type)); + classToInherit = classToInherit.AddMembers(GenerateTrackingProperties(propertySymbol, type)); + createBody = createBody.AddStatements( + GenerateApplyChangesBodyPart(propertySymbol, IdentifierName("value"), isRecord) + ); + } + + var createMethod = MethodDeclaration( + ParseTypeName(targetSymbol.ToDisplayString(NullableFlowState.NotNull, SymbolDisplayFormat.FullyQualifiedFormat)), "Create" + ) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithBody( + createBody.AddStatements( + ReturnStatement(IdentifierName("value")) + ) + ); + + classToInherit = classToInherit.AddMembers(createMethod); + + var cu = CompilationUnit( + List(), + List( + declaration.SyntaxTree.GetCompilationUnitRoot().Usings.AddRange( + namespaces + .Where(z => !string.IsNullOrWhiteSpace(z)) + .Select(z => UsingDirective(ParseName(z))) + ) + ), + List(), + SingletonList( + symbol.ContainingNamespace.IsGlobalNamespace + ? classToInherit.ReparentDeclaration(context, declaration) + : NamespaceDeclaration(ParseName(symbol.ContainingNamespace.ToDisplayString())) + .WithMembers(SingletonList(classToInherit.ReparentDeclaration(context, declaration))) + ) + ) + .WithLeadingTrivia() + .WithTrailingTrivia() + .WithLeadingTrivia(Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true))) + .WithTrailingTrivia(Trivia(NullableDirectiveTrivia(Token(SyntaxKind.RestoreKeyword), true)), CarriageReturnLineFeed); + + context.AddSource( + $"{Path.GetFileNameWithoutExtension(declaration.SyntaxTree.FilePath)}_{declaration.Identifier.Text}", + cu.NormalizeWhitespace().GetText(Encoding.UTF8) + ); + } + + private static StatementSyntax GenerateApplyChangesBodyPart( + IPropertySymbol propertySymbol, IdentifierNameSyntax valueIdentifier, bool isRecord + ) + { + return IfStatement( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(propertySymbol.Name), + IdentifierName("HasValue") + ), + Block( + SingletonList( + ExpressionStatement( + GenerateAssignmentExpression( + propertySymbol, valueIdentifier, isRecord, + propertySymbol.NullableAnnotation == NullableAnnotation.NotAnnotated && propertySymbol.Type.TypeKind == TypeKind.Struct + ? BinaryExpression( + SyntaxKind.CoalesceExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(propertySymbol.Name), + IdentifierName("Value") + ), + LiteralExpression( + SyntaxKind.DefaultLiteralExpression, + Token(SyntaxKind.DefaultKeyword) + ) + ) + : MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(propertySymbol.Name), + IdentifierName("Value") + ) + ) + ) + ) + ) + ); + } + + private static AssignmentExpressionSyntax GenerateAssignmentExpression( + IPropertySymbol propertySymbol, IdentifierNameSyntax valueIdentifier, bool isRecord, ExpressionSyntax valueExpression + ) + { + return isRecord + ? AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + valueIdentifier, + WithExpression( + valueIdentifier, + InitializerExpression( + SyntaxKind.WithInitializerExpression, + SingletonSeparatedList( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(propertySymbol.Name), + valueExpression + ) + ) + ) + ) + ) + : AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, valueIdentifier, IdentifierName(propertySymbol.Name)), + valueExpression + ); + } + + private static MemberDeclarationSyntax[] GenerateTrackingProperties(IPropertySymbol propertySymbol, TypeSyntax typeSyntax) + { + var type = GenericName(Identifier("HotChocolate.Optional")) + .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(typeSyntax))); + var fieldName = $"_{propertySymbol.Name}"; + return new MemberDeclarationSyntax[] + { + PropertyDeclaration(type, Identifier(propertySymbol.Name)) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword) + ) + ) + .WithAccessorList( + AccessorList( + List( + new[] + { + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + } + ) + ) + ) + }; + } + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var values = context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => + node is (ClassDeclarationSyntax or RecordDeclarationSyntax) and TypeDeclarationSyntax + { + BaseList: { } baseList + } && baseList.Types.Any( + z => z.Type is GenericNameSyntax qns && qns.Identifier.Text.EndsWith("IOptionalTracking", StringComparison.Ordinal) + ), + static (syntaxContext, token) => ( + syntax: (TypeDeclarationSyntax)syntaxContext.Node, semanticModel: syntaxContext.SemanticModel, + symbol: syntaxContext.SemanticModel.GetDeclaredSymbol((TypeDeclarationSyntax)syntaxContext.Node, token)! + ) + ) + .Select( + (tuple, token) => + { + var interfaceSymbol = tuple.symbol + .Interfaces.FirstOrDefault( + z => z.Name.StartsWith("IOptionalTracking", StringComparison.Ordinal) + ); + var targetSymbol = interfaceSymbol?.ContainingAssembly.Name == "Rocket.Surgery.LaunchPad.HotChocolate" + ? (INamedTypeSymbol?)interfaceSymbol.TypeArguments[0] + : null; + return ( + tuple.symbol, + tuple.syntax, + tuple.semanticModel, + interfaceSymbol, + targetSymbol + ); + } + ) + .Where(x => x.symbol is not null && x.targetSymbol is not null); + + context.RegisterSourceOutput( + values, + static (productionContext, tuple) => GeneratePropertyTracking(productionContext, tuple.syntax, tuple.symbol, tuple.targetSymbol!) + ); + } +} diff --git a/src/Analyzers/PropertyTrackingGenerator.cs b/src/Analyzers/PropertyTrackingGenerator.cs index 14adc3096..f727b2a07 100644 --- a/src/Analyzers/PropertyTrackingGenerator.cs +++ b/src/Analyzers/PropertyTrackingGenerator.cs @@ -15,9 +15,9 @@ public class PropertyTrackingGenerator : IIncrementalGenerator { private static void GeneratePropertyTracking( SourceProductionContext context, - Compilation compilation, TypeDeclarationSyntax declaration, - INamedTypeSymbol symbol + INamedTypeSymbol symbol, + INamedTypeSymbol targetSymbol ) { if (!declaration.Modifiers.Any(z => z.IsKind(SyntaxKind.PartialKeyword))) @@ -28,8 +28,6 @@ INamedTypeSymbol symbol return; } - var targetSymbol = (INamedTypeSymbol)symbol.Interfaces.SingleOrDefault(z => z.Name.StartsWith("IPropertyTracking", StringComparison.Ordinal)) - ?.TypeArguments[0]; var isRecord = declaration is RecordDeclarationSyntax; if (targetSymbol.IsRecord != isRecord) @@ -69,11 +67,28 @@ INamedTypeSymbol symbol var getChangedStateMethodInitializer = InitializerExpression(SyntaxKind.ObjectInitializerExpression); var applyChangesBody = Block(); var resetChangesBody = Block(); + var namespaces = new HashSet(); + + static void AddNamespacesFromPropertyType(HashSet namespaces, ITypeSymbol symbol) + { + namespaces.Add(symbol.ContainingNamespace.GetFullMetadataName()); + if (symbol is INamedTypeSymbol namedTypeSymbol) + { + if (namedTypeSymbol.IsGenericType) + { + foreach (var genericType in namedTypeSymbol.TypeArguments) + { + AddNamespacesFromPropertyType(namespaces, genericType); + } + } + } + } foreach (var propertySymbol in writeableProperties) { var type = ParseTypeName(propertySymbol.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); -// classToInherit = classToInherit.AddMembers(GenerateTrackingProperties(propertySymbol, type)); + AddNamespacesFromPropertyType(namespaces, propertySymbol.Type); + classToInherit = classToInherit.AddMembers(GenerateTrackingProperties(propertySymbol, type)); changesRecord = changesRecord.AddMembers( PropertyDeclaration(PredefinedType(Token(SyntaxKind.BoolKeyword)), Identifier(propertySymbol.Name)) @@ -108,12 +123,26 @@ INamedTypeSymbol symbol ); resetChangesBody = resetChangesBody.AddStatements( ExpressionStatement( - InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName(propertySymbol.Name), - IdentifierName("ResetState") - ) + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(propertySymbol.Name), + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + GenericName(Identifier("Rocket.Surgery.LaunchPad.Foundation.Assigned")) + .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(type))), + IdentifierName("Empty") + ) + ) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + IdentifierName(propertySymbol.Name) + ) + ) + ) + ) ) ) ); @@ -136,13 +165,13 @@ INamedTypeSymbol symbol ) ); - var applyChangesMethod = MethodDeclaration(ParseTypeName(targetSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)), "ApplyChanges") + var applyChangesMethod = MethodDeclaration(ParseTypeName(targetSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), "ApplyChanges") .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) .WithParameterList( ParameterList( SingletonSeparatedList( Parameter(Identifier("value")).WithType( - IdentifierName(targetSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)) + IdentifierName(targetSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)) ) ) ) @@ -163,7 +192,7 @@ INamedTypeSymbol symbol .WithTypeArgumentList( TypeArgumentList( SingletonSeparatedList( - IdentifierName(targetSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)) + IdentifierName(targetSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)) ) ) ) @@ -182,7 +211,13 @@ INamedTypeSymbol symbol var cu = CompilationUnit( List(), - List(declaration.SyntaxTree.GetCompilationUnitRoot().Usings), + List( + declaration.SyntaxTree.GetCompilationUnitRoot().Usings.AddRange( + namespaces + .Where(z => !string.IsNullOrWhiteSpace(z)) + .Select(z => UsingDirective(ParseName(z))) + ) + ), List(), SingletonList( symbol.ContainingNamespace.IsGlobalNamespace @@ -248,57 +283,11 @@ private static StatementSyntax GenerateApplyChangesBodyPart( private static MemberDeclarationSyntax[] GenerateTrackingProperties(IPropertySymbol propertySymbol, TypeSyntax typeSyntax) { - var fieldName = $"_{propertySymbol.Name}"; + var type = GenericName(Identifier("Rocket.Surgery.LaunchPad.Foundation.Assigned")) + .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(typeSyntax))); return new MemberDeclarationSyntax[] { - FieldDeclaration( - VariableDeclaration( - NullableType( - GenericName(Identifier("Rocket.Surgery.LaunchPad.Foundation.Assigned")) - .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(typeSyntax))) - ) - ) - .WithVariables( - SingletonSeparatedList( - VariableDeclarator( - Identifier(fieldName) - ).WithInitializer( - EqualsValueClause( - ObjectCreationExpression( - GenericName(Identifier("Rocket.Surgery.LaunchPad.Foundation.Assigned")) - .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(typeSyntax))) - ) - .WithArgumentList( - ArgumentList( - SingletonSeparatedList( - Argument(LiteralExpression(SyntaxKind.DefaultLiteralExpression, Token(SyntaxKind.DefaultKeyword))) - ) - ) - ) - ) - ) - ) - ) - ) - .WithModifiers(TokenList(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.ReadOnlyKeyword))), - PropertyDeclaration( - NullableType( - GenericName(Identifier("Rocket.Surgery.LaunchPad.Foundation.Assigned")) - .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(typeSyntax))) - ), - Identifier(propertySymbol.Name) - ) - .WithAttributeLists( - SingletonList( - AttributeList( - SingletonSeparatedList( - Attribute( - ParseName("System.Diagnostics.CodeAnalysis.AllowNull") - ) - ) - ) - ) - ) + PropertyDeclaration(type, Identifier(propertySymbol.Name)) .WithModifiers( TokenList( Token(SyntaxKind.PublicKeyword) @@ -309,32 +298,34 @@ private static MemberDeclarationSyntax[] GenerateTrackingProperties(IPropertySym List( new[] { - AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) - .WithExpressionBody(ArrowExpressionClause(IdentifierName(fieldName))) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), - AccessorDeclaration( - SyntaxKind.SetAccessorDeclaration - ) - .WithExpressionBody( - ArrowExpressionClause( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, IdentifierName(fieldName), IdentifierName("Value") - ), - BinaryExpression( - SyntaxKind.CoalesceExpression, - ConditionalAccessExpression(IdentifierName("value"), MemberBindingExpression(IdentifierName("Value"))), - LiteralExpression(SyntaxKind.DefaultLiteralExpression, Token(SyntaxKind.DefaultKeyword)) - ) + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + } + ) + ) + ).WithInitializer( + EqualsValueClause( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + type, + IdentifierName("Empty") + ) + ) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + LiteralExpression( + SyntaxKind.DefaultLiteralExpression, + Token(SyntaxKind.DefaultKeyword) ) ) ) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) - } - ) + ) + ) ) - ) + ).WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) }; } @@ -348,30 +339,37 @@ node is (ClassDeclarationSyntax or RecordDeclarationSyntax) and TypeDeclarationS { BaseList: { } baseList } && baseList.Types.Any( - z => z.Type is GenericNameSyntax qns && qns.Identifier.Text.EndsWith( - "IPropertyTracking", StringComparison.OrdinalIgnoreCase - ) + z => z.Type is GenericNameSyntax qns && qns.Identifier.Text.EndsWith("IPropertyTracking", StringComparison.Ordinal) ), static (syntaxContext, token) => ( syntax: (TypeDeclarationSyntax)syntaxContext.Node, semanticModel: syntaxContext.SemanticModel, symbol: syntaxContext.SemanticModel.GetDeclaredSymbol((TypeDeclarationSyntax)syntaxContext.Node, token)! ) - ).Combine( - context.CompilationProvider ) .Select( - static (tuple, _) => ( - tuple.Left.syntax, - tuple.Left.semanticModel, - tuple.Left.symbol, - compilation: tuple.Right - ) + (tuple, token) => + { + var interfaceSymbol = tuple.symbol + .Interfaces.FirstOrDefault( + z => z.Name.StartsWith("IPropertyTracking", StringComparison.Ordinal) + ); + var targetSymbol = interfaceSymbol?.ContainingAssembly.Name == "Rocket.Surgery.LaunchPad.Foundation" + ? (INamedTypeSymbol?)interfaceSymbol.TypeArguments[0] + : null; + return ( + tuple.symbol, + tuple.syntax, + tuple.semanticModel, + interfaceSymbol, + targetSymbol + ); + } ) - .Where(x => x.symbol is not null); + .Where(x => x.symbol is not null && x.targetSymbol is not null); context.RegisterSourceOutput( values, - static (productionContext, tuple) => GeneratePropertyTracking(productionContext, tuple.compilation, tuple.syntax, tuple.symbol) + static (productionContext, tuple) => GeneratePropertyTracking(productionContext, tuple.syntax, tuple.symbol, tuple.targetSymbol!) ); } } diff --git a/src/Analyzers/SyntaxExtensions.cs b/src/Analyzers/SyntaxExtensions.cs index 1fce9b168..7c1b3adea 100644 --- a/src/Analyzers/SyntaxExtensions.cs +++ b/src/Analyzers/SyntaxExtensions.cs @@ -8,9 +8,10 @@ namespace Rocket.Surgery.LaunchPad.Analyzers; internal static class SyntaxExtensions { - public static TypeSyntax EnsureNullable(this TypeSyntax typeSyntax) + public static TypeSyntax EnsureNullable(this TypeSyntax typeSyntax, NullableAnnotation? annotation = null) { - return typeSyntax is NullableTypeSyntax nts ? nts : SyntaxFactory.NullableType(typeSyntax); + if (annotation.HasValue && annotation == NullableAnnotation.Annotated) return typeSyntax; + return typeSyntax as NullableTypeSyntax ?? SyntaxFactory.NullableType(typeSyntax); } public static TypeSyntax EnsureNotNullable(this TypeSyntax typeSyntax) diff --git a/src/Foundation/Assigned.cs b/src/Foundation/Assigned.cs new file mode 100644 index 000000000..1a8748126 --- /dev/null +++ b/src/Foundation/Assigned.cs @@ -0,0 +1,159 @@ +namespace Rocket.Surgery.LaunchPad.Foundation; + +/// +/// The Assigned type is used to differentiate between not set and set input values. +/// +[PublicAPI] +public class Assigned : IEquatable> +{ + /// + /// Operator call through to Equals + /// + /// The left parameter + /// The right parameter + /// + /// true if both values are equal. + /// + public static bool operator ==(Assigned left, Assigned right) + { + return left.Equals(right); + } + + /// + /// Operator call through to Equals + /// + /// The left parameter + /// The right parameter + /// + /// true if both values are not equal. + /// + public static bool operator !=(Assigned left, Assigned right) + { + return !left.Equals(right); + } + + /// + /// Implicitly creates a new from + /// the given value. + /// + /// The value. + public static implicit operator Assigned(T value) + { + return new(value); + } + + /// + /// Implicitly gets the Assigned value. + /// + [return: MaybeNull] + public static implicit operator T(Assigned assigned) + { + return assigned.Value; + } + + /// + /// Creates an empty Assigned that provides a default value. + /// + /// The default value. + public static Assigned Empty(T? defaultValue = default) + { + return new(defaultValue, false); + } + + private readonly bool _hasValue; + + /// + /// Initializes a new instance of the struct. + /// + /// The actual value. + public Assigned(T? value) + { + Value = value; + _hasValue = true; + } + + private Assigned(T? value, bool hasValue) + { + Value = value; + _hasValue = hasValue; + } + + /// + /// The name value. + /// + [MaybeNull] + public T? Value { get; } + + /// + /// true if the Assigned was explicitly set. + /// + /// + public bool HasBeenSet() + { + return _hasValue; + } + + /// + /// Provides the name string. + /// + /// The name string value + public override string ToString() + { + return _hasValue ? Value?.ToString() ?? "null" : "unspecified"; + } + + /// + /// Compares this value to another value. + /// + /// + /// The second for comparison. + /// + /// + /// true if both values are equal. + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return !_hasValue; + } + + return obj is Assigned n && Equals(n); + } + + /// + /// Serves as a hash function for a object. + /// + /// + /// A hash code for this instance that is suitable for use in hashing + /// algorithms and data structures such as a hash table. + /// + public override int GetHashCode() + { + return _hasValue ? Value?.GetHashCode() ?? 0 : 0; + } + + /// + /// Compares this value to another value. + /// + /// + /// The second for comparison. + /// + /// + /// true if both values are equal. + /// + public bool Equals(Assigned other) + { + if (!_hasValue && !other._hasValue) + { + return true; + } + + if (_hasValue != other._hasValue) + { + return false; + } + + return Equals(Value, other.Value); + } +} diff --git a/src/Foundation/IPropertyTracking.cs b/src/Foundation/IPropertyTracking.cs index 88ce4c42d..5ebe26f16 100644 --- a/src/Foundation/IPropertyTracking.cs +++ b/src/Foundation/IPropertyTracking.cs @@ -1,5 +1,3 @@ -using MediatR; - namespace Rocket.Surgery.LaunchPad.Foundation; /// @@ -25,110 +23,3 @@ public interface IPropertyTracking /// void ResetChanges(); } - -/// -/// A helper class for tracking changes to a value -/// -/// -public class Assigned -{ - private T _value; - private bool _hasBeenSet; - - /// - /// The constructor for creating an assigned value - /// - /// - public Assigned(T value) - { - _value = value; - } - - /// - /// The underlying value - /// - public T Value - { - get => _value; - set - { - _hasBeenSet = true; - _value = value; - } - } - - /// - /// Has the value been assigned for this item - /// - public bool HasBeenSet() - { - return _hasBeenSet; - } - - /// - /// Resets this value as changed. - /// - public void ResetState() - { - _hasBeenSet = false; - } - -#pragma warning disable CA2225 - /// - /// Implicit operator for returning the underlying value - /// - /// - /// - public static implicit operator T?(Assigned? assigned) - { - return assigned == null ? default : assigned.Value; - } - - /// - /// Implicit operator for creating an assigned value - /// - /// - /// - public static implicit operator Assigned(T value) - { - return new(value); - } -#pragma warning restore CA2225 -} - -/// -/// A common handler for creating patch methods -/// -/// The request type that will be run after the patch has been applied -/// The patch object itself -/// The final result object -public abstract class PatchHandlerBase : IRequestHandler - where TRequest : IRequest - where TPatch : IPropertyTracking, IRequest -{ - private readonly IMediator _mediator; - - /// - /// The based handler using Mediator - /// - /// - protected PatchHandlerBase(IMediator mediator) - { - _mediator = mediator; - } - - /// - /// Method used to get , database calls, etc. - /// - /// - /// - /// - protected abstract Task GetRequest(TPatch patchRequest, CancellationToken cancellationToken); - - /// - public async Task Handle(TPatch request, CancellationToken cancellationToken) - { - var underlyingRequest = await GetRequest(request, cancellationToken); - return await _mediator.Send(request.ApplyChanges(underlyingRequest), cancellationToken); - } -} diff --git a/src/Foundation/PatchRequestHandler.cs b/src/Foundation/PatchRequestHandler.cs new file mode 100644 index 000000000..2e92016c0 --- /dev/null +++ b/src/Foundation/PatchRequestHandler.cs @@ -0,0 +1,40 @@ +using MediatR; + +namespace Rocket.Surgery.LaunchPad.Foundation; + +/// +/// A common handler for creating patch methods +/// +/// The request type that will be run after the patch has been applied +/// The patch object itself +/// The final result object +public abstract class PatchRequestHandler : IRequestHandler + where TRequest : IRequest + where TPatch : IPropertyTracking, IRequest +{ + private readonly IMediator _mediator; + + /// + /// The based handler using Mediator + /// + /// + protected PatchRequestHandler(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// Method used to get , database calls, etc. + /// + /// + /// + /// + protected abstract Task GetRequest(TPatch patchRequest, CancellationToken cancellationToken); + + /// + public virtual async Task Handle(TPatch request, CancellationToken cancellationToken) + { + var underlyingRequest = await GetRequest(request, cancellationToken); + return await _mediator.Send(request.ApplyChanges(underlyingRequest), cancellationToken); + } +} diff --git a/src/HotChocolate/GraphqlExtensions.cs b/src/HotChocolate/GraphqlExtensions.cs index e39c78d38..19b53f4b7 100644 --- a/src/HotChocolate/GraphqlExtensions.cs +++ b/src/HotChocolate/GraphqlExtensions.cs @@ -1,4 +1,13 @@ -using HotChocolate; +using System.Linq.Expressions; +using System.Reflection; +using HotChocolate; +using HotChocolate.Data.Filters; +using HotChocolate.Data.Sorting; +using HotChocolate.Execution.Configuration; +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; +using HotChocolate.Utilities; +using Microsoft.Extensions.DependencyInjection; using Rocket.Surgery.LaunchPad.Foundation; namespace Rocket.Surgery.LaunchPad.HotChocolate; @@ -33,4 +42,96 @@ public static IErrorBuilder WithProblemDetails(this IErrorBuilder error, IProble return error; } + + + /// + /// Configures a generated strongly typed id type with the given graphql schema type + /// + /// + /// Adds converters, binding and filters. + /// + /// + /// + /// + /// + public static IRequestExecutorBuilder ConfigureStronglyTypedId(this IRequestExecutorBuilder builder) + where TSchemaType : INamedType + { + AddTypeConversion(builder); + builder.BindRuntimeType(); + builder.AddConvention>(); + return builder; + } + + + private static readonly MethodInfo AddTypeConverterMethod = typeof(RequestExecutorBuilderExtensions) + .GetMethods() + .Single( + z => z.Name == "AddTypeConverter" + && z.ReturnType == typeof(IRequestExecutorBuilder) + && z.IsGenericMethod + && z.GetGenericMethodDefinition().GetGenericArguments().Length == 2 + && z.GetParameters().Length == 2 + ); + + private static void AddTypeConversion(IRequestExecutorBuilder builder) + { + var underlyingType = typeof(TStrongType).GetProperty("Value")!.PropertyType; + + { + var value = Expression.Parameter(typeof(TStrongType), "value"); + var delegateType = typeof(ChangeType<,>).MakeGenericType(typeof(TStrongType), underlyingType); + + AddTypeConverterMethod.MakeGenericMethod(typeof(TStrongType), underlyingType) + .Invoke( + null, + new object[] { builder, Expression.Lambda(delegateType, Expression.Property(value, "Value"), false, value).Compile() } + ); + } + + { + var value = Expression.Parameter(underlyingType, "value"); + var delegateType = typeof(ChangeType<,>).MakeGenericType(underlyingType, typeof(TStrongType)); + + var constructor = typeof(TStrongType).GetConstructor(new[] { underlyingType })!; + AddTypeConverterMethod.MakeGenericMethod(underlyingType, typeof(TStrongType)) + .Invoke( + null, + new object[] { builder, Expression.Lambda(delegateType, Expression.New(constructor, value), false, value).Compile() } + ); + } + } + + private class StronglyTypedIdFilterConventionExtension : FilterConventionExtension + where TSchemaType : INamedType + { + protected override void Configure(IFilterConventionDescriptor descriptor) + { + base.Configure(descriptor); + + descriptor + .BindRuntimeType>(); + } + } + + private class StronglyTypedIdFilter : FilterInputType, IComparableOperationFilterInputType + where TSchemaType : INamedType + { + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.Operation(DefaultFilterOperations.Equals) + .Type(typeof(TSchemaType)); + + descriptor.Operation(DefaultFilterOperations.NotEquals) + .Type(typeof(TSchemaType)); + + descriptor.Operation(DefaultFilterOperations.In) + .Type(typeof(ListType)); + + descriptor.Operation(DefaultFilterOperations.NotIn) + .Type(typeof(ListType)); + + descriptor.AllowAnd(false).AllowOr(false); + } + } } diff --git a/src/HotChocolate/IPropertyTracking.cs b/src/HotChocolate/IPropertyTracking.cs new file mode 100644 index 000000000..7a818d459 --- /dev/null +++ b/src/HotChocolate/IPropertyTracking.cs @@ -0,0 +1,24 @@ +using HotChocolate; +using Rocket.Surgery.LaunchPad.Foundation; + +namespace Rocket.Surgery.LaunchPad.HotChocolate; + +/// +/// Marker interface used to create a record or class that copies any setable properties for classes and init/setable record properties +/// +/// +/// This supports properties that are , converting them to +/// +/// +[PublicAPI] +public interface IOptionalTracking where T : new() +{ + /// + /// Method used to create the request from this given object + /// + /// + /// For records this will return a new record instance for classes it will mutate the existing instance. + /// + /// + T Create(); +} diff --git a/test/Analyzers.Tests/Analyzers.Tests.csproj b/test/Analyzers.Tests/Analyzers.Tests.csproj index 0d456bfb8..1a3526bf9 100644 --- a/test/Analyzers.Tests/Analyzers.Tests.csproj +++ b/test/Analyzers.Tests/Analyzers.Tests.csproj @@ -12,5 +12,6 @@ + diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt new file mode 100644 index 000000000..d4a28bcf1 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt @@ -0,0 +1,35 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test2_PatchGraphRocket.cs +#nullable enable +using System; +using NodaTime; + +public partial class PatchGraphRocket +{ + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Sample.Core.Operations.Rockets.PatchRocket Create() + { + var value = new global::Sample.Core.Operations.Rockets.PatchRocket{Id = Id}; + if (SerialNumber.HasValue) + { + value.SerialNumber = SerialNumber.Value; + } + + if (Type.HasValue) + { + value.Type = Type.Value; + } + + if (PlannedDate.HasValue) + { + value.PlannedDate = PlannedDate.Value; + } + + return value; + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.00.verified.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt new file mode 100644 index 000000000..d4a28bcf1 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt @@ -0,0 +1,35 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test2_PatchGraphRocket.cs +#nullable enable +using System; +using NodaTime; + +public partial class PatchGraphRocket +{ + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Sample.Core.Operations.Rockets.PatchRocket Create() + { + var value = new global::Sample.Core.Operations.Rockets.PatchRocket{Id = Id}; + if (SerialNumber.HasValue) + { + value.SerialNumber = SerialNumber.Value; + } + + if (Type.HasValue) + { + value.Type = Type.Value; + } + + if (PlannedDate.HasValue) + { + value.PlannedDate = PlannedDate.Value; + } + + return value; + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt new file mode 100644 index 000000000..dfeb8ffd9 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt @@ -0,0 +1,35 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test1_PatchGraphRocket.cs +#nullable enable +using System; +using NodaTime; + +public partial class PatchGraphRocket +{ + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Request Create() + { + var value = new global::Request{Id = Id}; + if (SerialNumber.HasValue) + { + value.SerialNumber = SerialNumber.Value; + } + + if (Type.HasValue) + { + value.Type = Type.Value ?? default; + } + + if (PlannedDate.HasValue) + { + value.PlannedDate = PlannedDate.Value ?? default; + } + + return value; + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=Type_value=12345.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=Type_value=12345.00.verified.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=Type_value=12345.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt new file mode 100644 index 000000000..dfeb8ffd9 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt @@ -0,0 +1,35 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test1_PatchGraphRocket.cs +#nullable enable +using System; +using NodaTime; + +public partial class PatchGraphRocket +{ + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Request Create() + { + var value = new global::Request{Id = Id}; + if (SerialNumber.HasValue) + { + value.SerialNumber = SerialNumber.Value; + } + + if (Type.HasValue) + { + value.Type = Type.Value ?? default; + } + + if (PlannedDate.HasValue) + { + value.PlannedDate = PlannedDate.Value ?? default; + } + + return value; + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt new file mode 100644 index 000000000..a65e1a840 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt @@ -0,0 +1,35 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test2_PatchGraphRocket.cs +#nullable enable +using System; +using NodaTime; + +public partial record PatchGraphRocket +{ + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Sample.Core.Operations.Rockets.PatchRocket Create() + { + var value = new global::Sample.Core.Operations.Rockets.PatchRocket{Id = Id}; + if (SerialNumber.HasValue) + { + value = value with {SerialNumber = SerialNumber.Value}; + } + + if (Type.HasValue) + { + value = value with {Type = Type.Value}; + } + + if (PlannedDate.HasValue) + { + value = value with {PlannedDate = PlannedDate.Value}; + } + + return value; + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.00.verified.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt new file mode 100644 index 000000000..a65e1a840 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt @@ -0,0 +1,35 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test2_PatchGraphRocket.cs +#nullable enable +using System; +using NodaTime; + +public partial record PatchGraphRocket +{ + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Sample.Core.Operations.Rockets.PatchRocket Create() + { + var value = new global::Sample.Core.Operations.Rockets.PatchRocket{Id = Id}; + if (SerialNumber.HasValue) + { + value = value with {SerialNumber = SerialNumber.Value}; + } + + if (Type.HasValue) + { + value = value with {Type = Type.Value}; + } + + if (PlannedDate.HasValue) + { + value = value with {PlannedDate = PlannedDate.Value}; + } + + return value; + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt new file mode 100644 index 000000000..d0d244ed3 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=SerialNumber_value=12345.01.verified.cs.txt @@ -0,0 +1,42 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test1_PatchGraphRocket.cs +#nullable enable +using System; +using NodaTime; + +public partial record PatchGraphRocket +{ + public HotChocolate.Optional Id { get; set; } + + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Request Create() + { + var value = new global::Request{}; + if (Id.HasValue) + { + value = value with {Id = Id.Value ?? default}; + } + + if (SerialNumber.HasValue) + { + value = value with {SerialNumber = SerialNumber.Value}; + } + + if (Type.HasValue) + { + value = value with {Type = Type.Value ?? default}; + } + + if (PlannedDate.HasValue) + { + value = value with {PlannedDate = PlannedDate.Value ?? default}; + } + + return value; + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=Type_value=12345.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=Type_value=12345.00.verified.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=Type_value=12345.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt new file mode 100644 index 000000000..d0d244ed3 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Create_property=Type_value=12345.01.verified.cs.txt @@ -0,0 +1,42 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test1_PatchGraphRocket.cs +#nullable enable +using System; +using NodaTime; + +public partial record PatchGraphRocket +{ + public HotChocolate.Optional Id { get; set; } + + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Request Create() + { + var value = new global::Request{}; + if (Id.HasValue) + { + value = value with {Id = Id.Value ?? default}; + } + + if (SerialNumber.HasValue) + { + value = value with {SerialNumber = SerialNumber.Value}; + } + + if (Type.HasValue) + { + value = value with {Type = Type.Value ?? default}; + } + + if (PlannedDate.HasValue) + { + value = value with {PlannedDate = PlannedDate.Value ?? default}; + } + + return value; + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.00.verified.txt new file mode 100644 index 000000000..384633cbf --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.00.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0001, + Title: Type must be made partial, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (10,24)-(10,35), + Description: , + HelpLink: , + MessageFormat: Type {0} must be made partial., + Message: Type Sample.Core.Operations.Rockets.PublicClass must be made partial., + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [ + { + Id: LPAD0001, + Title: Type must be made partial, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (10,24)-(10,35), + Description: , + HelpLink: , + MessageFormat: Type {0} must be made partial., + Message: Type Sample.Core.Operations.Rockets.PublicClass must be made partial., + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.received.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.received.cs.txt new file mode 100644 index 000000000..cdec108a9 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.received.cs.txt @@ -0,0 +1,41 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test1_PatchGraphRocket.cs +#nullable enable +using System; +using NodaTime; + +namespace Sample.Core.Operations.Rockets +{ + public static class PublicClass + { + public partial record PatchGraphRocket + { + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Sample.Core.Operations.Rockets.Request Create() + { + var value = new global::Sample.Core.Operations.Rockets.Request{Id = Id}; + if (SerialNumber.HasValue) + { + value = value with {SerialNumber = SerialNumber.Value}; + } + + if (Type.HasValue) + { + value = value with {Type = Type.Value ?? default}; + } + + if (PlannedDate.HasValue) + { + value = value with {PlannedDate = PlannedDate.Value ?? default}; + } + + return value; + } + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.verified.cs.txt new file mode 100644 index 000000000..d787062c1 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.verified.cs.txt @@ -0,0 +1,41 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test1_PatchGraphRocket.cs +#nullable enable +using System; +using NodaTime; + +namespace Sample.Core.Operations.Rockets +{ + public static class PublicClass + { + public partial record PatchGraphRocket + { + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Sample.Core.Operations.Rockets.Request Create() + { + var value = new global::Sample.Core.Operations.Rockets.Request{Id = Id}; + if (SerialNumber.HasValue) + { + value = value with {SerialNumber = SerialNumber.Value}; + } + + if (Type.HasValue) + { + value = value with {Type = Type.Value ?? default}; + } + + if (PlannedDate.HasValue) + { + value = value with {PlannedDate = PlannedDate.Value ?? default}; + } + + return value; + } + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Type_Declaration.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Type_Declaration.verified.txt new file mode 100644 index 000000000..6f466fe12 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Partial_Type_Declaration.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0001, + Title: Type must be made partial, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (10,17)-(10,33), + Description: , + HelpLink: , + MessageFormat: Type {0} must be made partial., + Message: Type Sample.Core.Operations.Rockets.PatchGraphRocket must be made partial., + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [ + { + Id: LPAD0001, + Title: Type must be made partial, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (10,17)-(10,33), + Description: , + HelpLink: , + MessageFormat: Type {0} must be made partial., + Message: Type Sample.Core.Operations.Rockets.PatchGraphRocket must be made partial., + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Class.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Class.verified.txt new file mode 100644 index 000000000..1a59ae4bf --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Class.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0005, + Title: The given declaration must match, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (10,19)-(10,25), + Description: , + HelpLink: , + MessageFormat: The declaration {0} must be a {1}., + Message: The declaration Sample.Core.Operations.Rockets.PatchGraphRocket must be a class., + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [ + { + Id: LPAD0005, + Title: The given declaration must match, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (10,19)-(10,25), + Description: , + HelpLink: , + MessageFormat: The declaration {0} must be a {1}., + Message: The declaration Sample.Core.Operations.Rockets.PatchGraphRocket must be a class., + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Record.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Record.verified.txt new file mode 100644 index 000000000..d7e393a99 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Record.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0005, + Title: The given declaration must match, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (10,19)-(10,24), + Description: , + HelpLink: , + MessageFormat: The declaration {0} must be a {1}., + Message: The declaration Sample.Core.Operations.Rockets.PatchGraphRocket must be a record., + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [ + { + Id: LPAD0005, + Title: The given declaration must match, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (10,19)-(10,24), + Description: , + HelpLink: , + MessageFormat: The declaration {0} must be a {1}., + Message: The declaration Sample.Core.Operations.Rockets.PatchGraphRocket must be a record., + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Builtin_Struct_Property.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Builtin_Struct_Property.00.verified.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Builtin_Struct_Property.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Builtin_Struct_Property.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Builtin_Struct_Property.01.verified.cs.txt new file mode 100644 index 000000000..076c0155a --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Builtin_Struct_Property.01.verified.cs.txt @@ -0,0 +1,38 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; +using NodaTime; + +namespace Sample.Core.Operations.Rockets +{ + public partial record PatchRocket + { + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Sample.Core.Operations.Rockets.Request Create() + { + var value = new global::Sample.Core.Operations.Rockets.Request{Id = Id}; + if (SerialNumber.HasValue) + { + value = value with {SerialNumber = SerialNumber.Value}; + } + + if (Type.HasValue) + { + value = value with {Type = Type.Value}; + } + + if (PlannedDate.HasValue) + { + value = value with {PlannedDate = PlannedDate.Value ?? default}; + } + + return value; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.00.verified.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.01.received.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.01.received.cs.txt new file mode 100644 index 000000000..4527a9725 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.01.received.cs.txt @@ -0,0 +1,38 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; +using NodaTime; + +namespace Sample.Core.Operations.Rockets +{ + public partial record PatchRocket + { + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Sample.Core.Operations.Rockets.Request Create() + { + var value = new global::Sample.Core.Operations.Rockets.Request{Id = Id}; + if (SerialNumber.HasValue) + { + value = value with {SerialNumber = SerialNumber.Value}; + } + + if (Type.HasValue) + { + value = value with {Type = Type.Value ?? default}; + } + + if (PlannedDate.HasValue) + { + value = value with {PlannedDate = PlannedDate.Value ?? default}; + } + + return value; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.01.verified.cs.txt new file mode 100644 index 000000000..3c4648cff --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.01.verified.cs.txt @@ -0,0 +1,38 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; +using NodaTime; + +namespace Sample.Core.Operations.Rockets +{ + public partial record PatchRocket + { + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Sample.Core.Operations.Rockets.Request Create() + { + var value = new global::Sample.Core.Operations.Rockets.Request{Id = Id}; + if (SerialNumber.HasValue) + { + value = value with {SerialNumber = SerialNumber.Value}; + } + + if (Type.HasValue) + { + value = value with {Type = Type.Value ?? default}; + } + + if (PlannedDate.HasValue) + { + value = value with {PlannedDate = PlannedDate.Value ?? default}; + } + + return value; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Custom_Struct_Property.00.received.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Custom_Struct_Property.00.received.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Custom_Struct_Property.00.received.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Custom_Struct_Property.01.received.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Custom_Struct_Property.01.received.cs.txt new file mode 100644 index 000000000..e127f4da5 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Custom_Struct_Property.01.received.cs.txt @@ -0,0 +1,46 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test1_PatchRocketUnderTest.cs +#nullable enable +using Sample.Core.Operations.Rockets; +using System; +using NodaTime; + +namespace Sample.Core.Operations.Rockets +{ + public partial record PatchRocketUnderTest + { + public HotChocolate.Optional Id { get; set; } + + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Sample.Core.Operations.Rockets.PatchRocket Create() + { + var value = new global::Sample.Core.Operations.Rockets.PatchRocket{}; + if (Id.HasValue) + { + value = value with {Id = Id.Value}; + } + + if (SerialNumber.HasValue) + { + value = value with {SerialNumber = SerialNumber.Value}; + } + + if (Type.HasValue) + { + value = value with {Type = Type.Value}; + } + + if (PlannedDate.HasValue) + { + value = value with {PlannedDate = PlannedDate.Value}; + } + + return value; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Enum_Property.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Enum_Property.00.verified.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Enum_Property.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Enum_Property.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Enum_Property.01.verified.cs.txt new file mode 100644 index 000000000..ce07f8121 --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Enum_Property.01.verified.cs.txt @@ -0,0 +1,46 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test1_PatchRocketUnderTest.cs +#nullable enable +using System; +using Sample.Core.Operations.Rockets; +using NodaTime; + +namespace Sample.Core.Operations.Rockets +{ + public partial record PatchRocketUnderTest + { + public HotChocolate.Optional Id { get; set; } + + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Sample.Core.Operations.Rockets.Request Create() + { + var value = new global::Sample.Core.Operations.Rockets.Request{}; + if (Id.HasValue) + { + value = value with {Id = Id.Value}; + } + + if (SerialNumber.HasValue) + { + value = value with {SerialNumber = SerialNumber.Value}; + } + + if (Type.HasValue) + { + value = value with {Type = Type.Value}; + } + + if (PlannedDate.HasValue) + { + value = value with {PlannedDate = PlannedDate.Value}; + } + + return value; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.00.verified.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.00.verified.txt new file mode 100644 index 000000000..4aaa47e6e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.01.verified.cs.txt b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.01.verified.cs.txt new file mode 100644 index 000000000..e96e2a0aa --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.01.verified.cs.txt @@ -0,0 +1,38 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.GraphqlOptionalPropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; +using NodaTime; + +namespace Sample.Core.Operations.Rockets +{ + public partial record PatchRocket + { + public HotChocolate.Optional SerialNumber { get; set; } + + public HotChocolate.Optional Type { get; set; } + + public HotChocolate.Optional PlannedDate { get; set; } + + public global::Sample.Core.Operations.Rockets.Request Create() + { + var value = new global::Sample.Core.Operations.Rockets.Request{Id = Id}; + if (SerialNumber.HasValue) + { + value = value with {SerialNumber = SerialNumber.Value}; + } + + if (Type.HasValue) + { + value = value with {Type = Type.Value ?? default}; + } + + if (PlannedDate.HasValue) + { + value = value with {PlannedDate = PlannedDate.Value}; + } + + return value; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/GraphqlOptionalPropertyTrackingGeneratorTests.cs b/test/Analyzers.Tests/GraphqlOptionalPropertyTrackingGeneratorTests.cs new file mode 100644 index 000000000..ac6cb909e --- /dev/null +++ b/test/Analyzers.Tests/GraphqlOptionalPropertyTrackingGeneratorTests.cs @@ -0,0 +1,586 @@ +using Analyzers.Tests.Helpers; +using HotChocolate; +using ImTools; +using MediatR; +using Microsoft.Extensions.Logging; +using NodaTime; +using Rocket.Surgery.LaunchPad.Analyzers; +using Rocket.Surgery.LaunchPad.Foundation; +using Rocket.Surgery.LaunchPad.HotChocolate; + +namespace Analyzers.Tests; + +[UsesVerify] +public class GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests : GeneratorTest +{ + [Fact] + public async Task Should_Require_Partial_Type_Declaration() + { + var source = @" +namespace Sample.Core.Operations.Rockets +{ + public class Request : IRequest + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + public Instant PlannedDate { get; set; } + } + public class PatchGraphRocket : IOptionalTracking, IRequest + { + public Guid Id { get; init; } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + var diagnostic = output!.Diagnostics.Should().HaveCount(1).And.Subject.First(); + diagnostic.Id.Should().Be("LPAD0001"); + diagnostic.ToString().Should().Contain("Type Sample.Core.Operations.Rockets.PatchGraphRocket must be made partial."); + + await Verify(result); + } + + [Fact] + public async Task Should_Require_Partial_Parent_Type_Declaration() + { + var source = @" +namespace Sample.Core.Operations.Rockets +{ + public record Request : IRequest + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + public Instant PlannedDate { get; set; } + } + public static class PublicClass + { + public partial record PatchGraphRocket : IOptionalTracking, IRequest + { + public Guid Id { get; init; } + } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + var diagnostic = output!.Diagnostics.Should().HaveCount(1).And.Subject.First(); + diagnostic.Id.Should().Be("LPAD0001"); + diagnostic.ToString().Should().Contain("Type Sample.Core.Operations.Rockets.PublicClass must be made partial."); + + await Verify(result); + } + + [Fact] + public async Task Should_Require_Same_Type_As_Record() + { + var source = @" +namespace Sample.Core.Operations.Rockets +{ + public record Request : IRequest + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + public Instant PlannedDate { get; set; } + } + public partial class PatchGraphRocket : IOptionalTracking, IRequest + { + public Guid Id { get; init; } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + var diagnostic = output!.Diagnostics.Should().HaveCount(1).And.Subject.First(); + diagnostic.Id.Should().Be("LPAD0005"); + diagnostic.ToString().Should().Contain("The declaration Sample.Core.Operations.Rockets.PatchGraphRocket must be a record."); + + await Verify(result); + } + + [Fact] + public async Task Should_Require_Same_Type_As_Class() + { + var source = @" +namespace Sample.Core.Operations.Rockets +{ + public class Request : IRequest + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + public Instant PlannedDate { get; set; } + } + public partial record PatchGraphRocket : IOptionalTracking, IRequest + { + public Guid Id { get; init; } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + var diagnostic = output!.Diagnostics.Should().HaveCount(1).And.Subject.First(); + diagnostic.Id.Should().Be("LPAD0005"); + diagnostic.ToString().Should().Contain("The declaration Sample.Core.Operations.Rockets.PatchGraphRocket must be a class."); + + await Verify(result); + } + + [Fact] + public async Task Should_Support_Nullable_Class_Property() + { + var source = @" +namespace Sample.Core.Operations.Rockets +{ + public record Request : IRequest + { + public Guid Id { get; init; } + public string? SerialNumber { get; set; } = null!; + public int Type { get; set; } + public Instant PlannedDate { get; set; } + } + public partial record PatchRocket : IOptionalTracking, IRequest + { + public Guid Id { get; init; } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + + await Verify(result); + } + + [Fact] + public async Task Should_Support_Nullable_Builtin_Struct_Property() + { + var source = @" +namespace Sample.Core.Operations.Rockets +{ + public record Request : IRequest + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int? Type { get; set; } + public Instant PlannedDate { get; set; } + } + public partial record PatchRocket : IOptionalTracking, IRequest + { + public Guid Id { get; init; } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + + await Verify(result); + } + + [Fact] + public async Task Should_Support_Nullable_Struct_Property() + { + var source = @" +namespace Sample.Core.Operations.Rockets +{ + public record Request : IRequest + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + public Instant? PlannedDate { get; set; } + } + public partial record PatchRocket : IOptionalTracking, IRequest + { + public Guid Id { get; init; } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + + await Verify(result); + } + + [Fact] + public async Task Should_Support_Nullable_Enum_Property() + { + var source = @" +namespace Sample.Core.Operations.Rockets +{ + public enum RocketType { Falcon9, FalconHeavy, AtlasV } + + public record Request : IRequest + { + public RocketId Id { get; init; } + public string SerialNumber { get; set; } = null!; + public RocketType Type { get; set; } + public Instant? PlannedDate { get; set; } + } + public partial record PatchRocketUnderTest : IOptionalTracking + { + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + + await Verify(result); + } + + public GraphqlOptionalGraphqlOptionalPropertyTrackingGeneratorTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, LogLevel.Trace) + { + WithGenerator(); + AddReferences(typeof(IOptionalTracking<>), typeof(Optional<>), typeof(Instant), typeof(IPropertyTracking<>), typeof(IMediator), typeof(IBaseRequest)); + AddSources( + @" +global using System; +global using MediatR; +global using NodaTime; +global using Rocket.Surgery.LaunchPad.Foundation; +global using Rocket.Surgery.LaunchPad.HotChocolate; +global using Sample.Core.Operations.Rockets; +namespace Sample.Core.Operations.Rockets +{ + public class RocketModel + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + } +} +" + ); + } + + [Theory] + [InlineData("SerialNumber", "12345")] + [InlineData("Type", 12345)] + public async Task Should_Generate_Record_With_Underlying_Properties_And_Create(string property, object value) + { + var valueType = value.GetType(); + if (value.GetType().IsValueType) + { + valueType = typeof(Nullable<>).MakeGenericType(value.GetType()); + } + + var source = @" +public record Request : IRequest +{ + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + public Instant PlannedDate { get; set; } +} +public partial record PatchGraphRocket : IOptionalTracking, IRequest +{ +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + output!.Diagnostics.Should().HaveCount(0); + + var assembly = EmitAssembly(result).Should().NotBeNull().And.Subject; + var type = assembly.DefinedTypes.FindFirst(z => z.Name == "PatchGraphRocket"); + var applyChangesMethod = type.GetMethod("Create")!; + var propertyUnderTest = type.GetProperty(property)!; + var requestType = assembly.DefinedTypes.FindFirst(z => z.Name == "Request"); + var requestPropertyUnderTest = requestType.GetProperty(property)!; + var instance = Activator.CreateInstance(type); + + var assignedType = typeof(Optional<>).MakeGenericType(valueType); + propertyUnderTest.SetValue(instance, Activator.CreateInstance(assignedType, value)); + var request = applyChangesMethod.Invoke(instance, Array.Empty()); + var r = requestPropertyUnderTest.GetValue(request); + r.Should().Be(value); + + await Verify(result).UseParameters(property, value); + } + + [Theory] + [InlineData("SerialNumber", "12345")] + [InlineData("Type", 12345)] + public async Task Should_Generate_Class_With_Underlying_Properties_And_Create(string property, object value) + { + var valueType = value.GetType(); + if (value.GetType().IsValueType) + { + valueType = typeof(Nullable<>).MakeGenericType(value.GetType()); + } + + var source = @" +public class Request : IRequest +{ + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + public Instant PlannedDate { get; set; } +} +public partial class PatchGraphRocket : IOptionalTracking, IRequest +{ + public Guid Id { get; init; } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + output!.Diagnostics.Should().HaveCount(0); + + var assembly = EmitAssembly(result).Should().NotBeNull().And.Subject; + var type = assembly.DefinedTypes.FindFirst(z => z.Name == "PatchGraphRocket"); + var applyChangesMethod = type.GetMethod("Create")!; + var propertyUnderTest = type.GetProperty(property)!; + var requestType = assembly.DefinedTypes.FindFirst(z => z.Name == "Request"); + var requestPropertyUnderTest = requestType.GetProperty(property)!; + var instance = Activator.CreateInstance(type); + + var assignedType = typeof(Optional<>).MakeGenericType(valueType); + propertyUnderTest.SetValue(instance, Activator.CreateInstance(assignedType, value)); + var request = applyChangesMethod.Invoke(instance, Array.Empty()); + var r = requestPropertyUnderTest.GetValue(request); + r.Should().Be(value); + + await Verify(result).UseParameters(property, value); + } + + + [Theory] + [InlineData("SerialNumber", "12345")] + [InlineData("Type", 12345)] + public async Task Should_Generate_Record_With_Underlying_IPropertyTracking_Properties_And_Create(string property, object value) + { + var valueType = value.GetType(); + if (value.GetType().IsValueType) + { + valueType = typeof(Nullable<>).MakeGenericType(value.GetType()); + } + + AddPatchRocketModel(RocketModelType.Record); + var source = @" +public record Request : IRequest +{ + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + public Instant PlannedDate { get; set; } +} +public partial record PatchGraphRocket : IOptionalTracking +{ + public Guid Id { get; init; } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + output!.Diagnostics.Should().HaveCount(0); + + var assembly = EmitAssembly(result).Should().NotBeNull().And.Subject; + var type = assembly.DefinedTypes.FindFirst(z => z.Name == "PatchGraphRocket"); + var applyChangesMethod = type.GetMethod("Create")!; + var propertyUnderTest = type.GetProperty(property)!; + var requestType = assembly.DefinedTypes.FindFirst(z => z.Name == "PatchRocket"); + var requestPropertyUnderTest = requestType.GetProperty(property)!; + var instance = Activator.CreateInstance(type); + + var optionalType = typeof(Optional<>).MakeGenericType(valueType); + var assignedType = typeof(Assigned<>).MakeGenericType(value.GetType()); + var assignedPropertyUnderTest = assignedType.GetProperty("Value")!; + propertyUnderTest.SetValue(instance, Activator.CreateInstance(optionalType, value)); + var request = applyChangesMethod.Invoke(instance, Array.Empty()); + var r = requestPropertyUnderTest.GetValue(request); + assignedPropertyUnderTest.GetValue(r).Should().Be(value); + + await Verify(result).UseParameters(property, value); + } + + [Theory] + [InlineData("SerialNumber", "12345")] + [InlineData("Type", 12345)] + public async Task Should_Generate_Class_With_Underlying_IPropertyTracking_Properties_And_Create(string property, object value) + { + var valueType = value.GetType(); + if (value.GetType().IsValueType) + { + valueType = typeof(Nullable<>).MakeGenericType(value.GetType()); + } + + AddPatchRocketModel(RocketModelType.Class); + var source = @" +public class Request : IRequest +{ + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int Type { get; set; } + public Instant PlannedDate { get; set; } +} +public partial class PatchGraphRocket : IOptionalTracking +{ + public Guid Id { get; init; } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + output!.Diagnostics.Should().HaveCount(0); + + var assembly = EmitAssembly(result).Should().NotBeNull().And.Subject; + var type = assembly.DefinedTypes.FindFirst(z => z.Name == "PatchGraphRocket"); + var applyChangesMethod = type.GetMethod("Create")!; + var propertyUnderTest = type.GetProperty(property)!; + var requestType = assembly.DefinedTypes.FindFirst(z => z.Name == "PatchRocket"); + var requestPropertyUnderTest = requestType.GetProperty(property)!; + var instance = Activator.CreateInstance(type); + + var optionalType = typeof(Optional<>).MakeGenericType(valueType); + var assignedType = typeof(Assigned<>).MakeGenericType(value.GetType()); + var assignedPropertyUnderTest = assignedType.GetProperty("Value")!; + propertyUnderTest.SetValue(instance, Activator.CreateInstance(optionalType, value)); + var request = applyChangesMethod.Invoke(instance, Array.Empty()); + var r = requestPropertyUnderTest.GetValue(request); + assignedPropertyUnderTest.GetValue(r).Should().Be(value); + + await Verify(result).UseParameters(property, value); + } + + private enum RocketModelType { Record, Class } + + private void AddPatchRocketModel(RocketModelType type) + { + if (type == RocketModelType.Record) + { + AddSources( + @" + +namespace Sample.Core.Operations.Rockets +{ + public partial record PatchRocket : IPropertyTracking, IRequest + { + public Guid Id { get; init; } + } + public partial record PatchRocket + { + public Rocket.Surgery.LaunchPad.Foundation.Assigned SerialNumber { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned Type { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned PlannedDate { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + + public record Changes + { + public bool SerialNumber { get; init; } + + public bool Type { get; init; } + + public bool PlannedDate { get; init; } + } + + public Changes GetChangedState() + { + return new Changes() + {SerialNumber = SerialNumber.HasBeenSet(), Type = Type.HasBeenSet(), PlannedDate = PlannedDate.HasBeenSet()}; + } + + public Request ApplyChanges(Request value) + { + if (SerialNumber.HasBeenSet()) + { + value = value with {SerialNumber = SerialNumber}; + } + + if (Type.HasBeenSet()) + { + value = value with {Type = Type}; + } + + if (PlannedDate.HasBeenSet()) + { + value.PlannedDate = PlannedDate; + } + + ResetChanges(); + return value; + } + + public PatchRocket ResetChanges() + { + SerialNumber = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(SerialNumber); + Type = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Type); + PlannedDate = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(PlannedDate); + return this; + } + + void IPropertyTracking.ResetChanges() + { + ResetChanges(); + } + } +}" + ); + } + else if (type == RocketModelType.Class) + { + AddSources( + @" +namespace Sample.Core.Operations.Rockets + { + public partial class PatchRocket : IPropertyTracking, IRequest + { + public Guid Id { get; init; } + } + + public partial class PatchRocket + { + public Rocket.Surgery.LaunchPad.Foundation.Assigned SerialNumber { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned Type { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned PlannedDate { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + + public record Changes + { + public bool SerialNumber { get; init; } + + public bool Type { get; init; } + + public bool PlannedDate { get; init; } + } + + public Changes GetChangedState() + { + return new Changes() + {SerialNumber = SerialNumber.HasBeenSet(), Type = Type.HasBeenSet(), PlannedDate = PlannedDate.HasBeenSet()}; + } + + public Request ApplyChanges(Request value) + { + if (SerialNumber.HasBeenSet()) + { + value.SerialNumber = SerialNumber; + } + + if (Type.HasBeenSet()) + { + value.Type = Type; + } + + if (PlannedDate.HasBeenSet()) + { + value.PlannedDate = PlannedDate; + } + + ResetChanges(); + return value; + } + + public PatchRocket ResetChanges() + { + SerialNumber = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(SerialNumber); + Type = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Type); + PlannedDate = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(PlannedDate); + return this; + } + + void IPropertyTracking.ResetChanges() + { + ResetChanges(); + } + } +} +" + ); + } + } +} diff --git a/test/Analyzers.Tests/Helpers/GeneratorTest.cs b/test/Analyzers.Tests/Helpers/GeneratorTest.cs index 7752d8043..9aa39cfb8 100644 --- a/test/Analyzers.Tests/Helpers/GeneratorTest.cs +++ b/test/Analyzers.Tests/Helpers/GeneratorTest.cs @@ -164,6 +164,12 @@ protected GeneratorTest AddReferences(params Assembly[] references) return this; } + protected GeneratorTest RemoveReference(Predicate predicate) + { + _metadataReferences.RemoveWhere(predicate); + return this; + } + protected GeneratorTest AddSources(params string[] sources) { _sources.AddRange(sources); diff --git a/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Class.00.verified.txt b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Class.00.verified.txt new file mode 100644 index 000000000..fae80ede8 --- /dev/null +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Class.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.InheritFromGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Class.01.verified.cs.txt b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Class.01.verified.cs.txt new file mode 100644 index 000000000..0f099dcbd --- /dev/null +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Class.01.verified.cs.txt @@ -0,0 +1,19 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.InheritFromGenerator/Test0_Request.cs +#nullable enable +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; + +namespace Sample.Core.Operations.Rockets +{ + public static partial class CreateRocket + { + public partial class Request + { + public string SerialNumber { get; set; } + + public Request With(Model value) => new Request{Id = this.Id, SerialNumber = value.SerialNumber}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record.00.verified.txt b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record.00.verified.txt new file mode 100644 index 000000000..fae80ede8 --- /dev/null +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.InheritFromGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record.01.verified.cs.txt b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record.01.verified.cs.txt new file mode 100644 index 000000000..bff5e3ca7 --- /dev/null +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record.01.verified.cs.txt @@ -0,0 +1,19 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.InheritFromGenerator/Test0_Request.cs +#nullable enable +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; + +namespace Sample.Core.Operations.Rockets +{ + public static partial class CreateRocket + { + public partial record Request + { + public string SerialNumber { get; set; } + + public Request With(Model value) => this with {SerialNumber = value.SerialNumber}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record_That_Inherits.00.verified.txt b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record_That_Inherits.00.verified.txt new file mode 100644 index 000000000..fae80ede8 --- /dev/null +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record_That_Inherits.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.InheritFromGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record_That_Inherits.01.verified.cs.txt b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record_That_Inherits.01.verified.cs.txt new file mode 100644 index 000000000..554f47c1d --- /dev/null +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Generate_With_Method_For_Record_That_Inherits.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.InheritFromGenerator/Test0_Request.cs +#nullable enable +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; + +namespace Sample.Core.Operations.Rockets +{ + public static partial class CreateRocket + { + public partial record Request + { + public Request With(Model value) => this with {SerialNumber = value.SerialNumber}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Inherit_Multiple_With_Method_For_Record.00.verified.txt b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Inherit_Multiple_With_Method_For_Record.00.verified.txt new file mode 100644 index 000000000..fae80ede8 --- /dev/null +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Inherit_Multiple_With_Method_For_Record.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.InheritFromGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Inherit_Multiple_With_Method_For_Record.01.verified.cs.txt b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Inherit_Multiple_With_Method_For_Record.01.verified.cs.txt new file mode 100644 index 000000000..264ef0314 --- /dev/null +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Inherit_Multiple_With_Method_For_Record.01.verified.cs.txt @@ -0,0 +1,22 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.InheritFromGenerator/Test0_Request.cs +#nullable enable +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; + +namespace Sample.Core.Operations.Rockets +{ + public static partial class CreateRocket + { + public partial record Request + { + public string SerialNumber { get; set; } + + public Request With(Model value) => this with {SerialNumber = value.SerialNumber}; + public string OtherNumber { get; set; } + + public Request With(Other value) => this with {OtherNumber = value.OtherNumber}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.00.verified.txt b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.00.verified.txt new file mode 100644 index 000000000..643428c5e --- /dev/null +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.00.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0001, + Title: Type must be made partial, + Severity: Error, + WarningLevel: 0, + Location: Test0.cs: (7,24)-(7,36), + Description: , + HelpLink: , + MessageFormat: Type {0} must be made partial., + Message: Type Sample.Core.Operations.Rockets.CreateRocket must be made partial., + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.InheritFromGenerator: [ + { + Id: LPAD0001, + Title: Type must be made partial, + Severity: Error, + WarningLevel: 0, + Location: Test0.cs: (7,24)-(7,36), + Description: , + HelpLink: , + MessageFormat: Type {0} must be made partial., + Message: Type Sample.Core.Operations.Rockets.CreateRocket must be made partial., + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.verified.cs.txt b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.verified.cs.txt new file mode 100644 index 000000000..6a02ef21d --- /dev/null +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.verified.cs.txt @@ -0,0 +1,19 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.InheritFromGenerator/Test0_Request.cs +#nullable enable +using System; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; + +namespace Sample.Core.Operations.Rockets +{ + public static class CreateRocket + { + public partial record Request + { + public string SerialNumber { get; set; } + + public Request With(Model value) => this with {SerialNumber = value.SerialNumber}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Require_Partial_Type_Declaration.verified.txt b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Require_Partial_Type_Declaration.verified.txt new file mode 100644 index 000000000..14ec71245 --- /dev/null +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.Should_Require_Partial_Type_Declaration.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0001, + Title: Type must be made partial, + Severity: Error, + WarningLevel: 0, + Location: Test0.cs: (15,22)-(15,29), + Description: , + HelpLink: , + MessageFormat: Type {0} must be made partial., + Message: Type Sample.Core.Operations.Rockets.CreateRocket+Request must be made partial., + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.InheritFromGenerator: [ + { + Id: LPAD0001, + Title: Type must be made partial, + Severity: Error, + WarningLevel: 0, + Location: Test0.cs: (15,22)-(15,29), + Description: , + HelpLink: , + MessageFormat: Type {0} must be made partial., + Message: Type Sample.Core.Operations.Rockets.CreateRocket+Request must be made partial., + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/InheritFromGeneratorTests.cs b/test/Analyzers.Tests/InheritFromGeneratorTests.cs index 994bce9f9..c15668f8a 100644 --- a/test/Analyzers.Tests/InheritFromGeneratorTests.cs +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.cs @@ -8,6 +8,7 @@ namespace Analyzers.Tests; +[UsesVerify] public class InheritFromGeneratorTests : GeneratorTest { [Fact] @@ -42,6 +43,8 @@ public partial record Response {} var diagnostic = output!.Diagnostics.Should().HaveCount(1).And.Subject.First(); diagnostic.Id.Should().Be("LPAD0001"); diagnostic.ToString().Should().Contain("Type Sample.Core.Operations.Rockets.CreateRocket+Request must be made partial."); + + await Verify(result); } [Fact] @@ -76,6 +79,8 @@ public partial record Response {} var diagnostic = output!.Diagnostics.Should().HaveCount(1).And.Subject.First(); diagnostic.Id.Should().Be("LPAD0001"); diagnostic.ToString().Should().Contain("Type Sample.Core.Operations.Rockets.CreateRocket must be made partial."); + + await Verify(result); } [Fact] @@ -130,6 +135,8 @@ public partial record Request var result = await GenerateAsync(source); result.EnsureDiagnosticSeverity(); result.AssertGeneratedAsExpected(expected); + + await Verify(result); } [Fact] @@ -193,6 +200,8 @@ public partial record Request var result = await GenerateAsync(source); result.EnsureDiagnosticSeverity(); result.AssertGeneratedAsExpected(expected); + + await Verify(result); } [Fact] @@ -245,6 +254,8 @@ public partial record Request var result = await GenerateAsync(source); result.EnsureDiagnosticSeverity(); result.AssertGeneratedAsExpected(expected); + + await Verify(result); } [Fact] @@ -299,6 +310,8 @@ public partial class Request var result = await GenerateAsync(source); result.EnsureDiagnosticSeverity(); result.AssertGeneratedAsExpected(expected); + + await Verify(result); } public InheritFromGeneratorTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, LogLevel.Trace) diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.00.verified.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.00.verified.txt new file mode 100644 index 000000000..ebf90d64e --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.01.verified.cs.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.01.verified.cs.txt new file mode 100644 index 000000000..669e1bedb --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.01.verified.cs.txt @@ -0,0 +1,50 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; + +public partial class PatchRocket +{ + public Rocket.Surgery.LaunchPad.Foundation.Assigned SerialNumber { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned Type { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public record Changes + { + public bool SerialNumber { get; init; } + + public bool Type { get; init; } + } + + public Changes GetChangedState() + { + return new Changes() + {SerialNumber = SerialNumber.HasBeenSet(), Type = Type.HasBeenSet()}; + } + + public global::Request ApplyChanges(global::Request value) + { + if (SerialNumber.HasBeenSet()) + { + value.SerialNumber = SerialNumber; + } + + if (Type.HasBeenSet()) + { + value.Type = Type; + } + + ResetChanges(); + return value; + } + + public PatchRocket ResetChanges() + { + SerialNumber = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(SerialNumber); + Type = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Type); + return this; + } + + void IPropertyTracking.ResetChanges() + { + ResetChanges(); + } +} +#nullable restore diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.00.verified.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.00.verified.txt new file mode 100644 index 000000000..ebf90d64e --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.01.verified.cs.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.01.verified.cs.txt new file mode 100644 index 000000000..669e1bedb --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.01.verified.cs.txt @@ -0,0 +1,50 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; + +public partial class PatchRocket +{ + public Rocket.Surgery.LaunchPad.Foundation.Assigned SerialNumber { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned Type { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public record Changes + { + public bool SerialNumber { get; init; } + + public bool Type { get; init; } + } + + public Changes GetChangedState() + { + return new Changes() + {SerialNumber = SerialNumber.HasBeenSet(), Type = Type.HasBeenSet()}; + } + + public global::Request ApplyChanges(global::Request value) + { + if (SerialNumber.HasBeenSet()) + { + value.SerialNumber = SerialNumber; + } + + if (Type.HasBeenSet()) + { + value.Type = Type; + } + + ResetChanges(); + return value; + } + + public PatchRocket ResetChanges() + { + SerialNumber = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(SerialNumber); + Type = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Type); + return this; + } + + void IPropertyTracking.ResetChanges() + { + ResetChanges(); + } +} +#nullable restore diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Track_Changes.00.verified.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Track_Changes.00.verified.txt new file mode 100644 index 000000000..ebf90d64e --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Track_Changes.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Track_Changes.01.verified.cs.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Track_Changes.01.verified.cs.txt new file mode 100644 index 000000000..669e1bedb --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Class_With_Underlying_Properties_And_Track_Changes.01.verified.cs.txt @@ -0,0 +1,50 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; + +public partial class PatchRocket +{ + public Rocket.Surgery.LaunchPad.Foundation.Assigned SerialNumber { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned Type { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public record Changes + { + public bool SerialNumber { get; init; } + + public bool Type { get; init; } + } + + public Changes GetChangedState() + { + return new Changes() + {SerialNumber = SerialNumber.HasBeenSet(), Type = Type.HasBeenSet()}; + } + + public global::Request ApplyChanges(global::Request value) + { + if (SerialNumber.HasBeenSet()) + { + value.SerialNumber = SerialNumber; + } + + if (Type.HasBeenSet()) + { + value.Type = Type; + } + + ResetChanges(); + return value; + } + + public PatchRocket ResetChanges() + { + SerialNumber = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(SerialNumber); + Type = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Type); + return this; + } + + void IPropertyTracking.ResetChanges() + { + ResetChanges(); + } +} +#nullable restore diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.00.verified.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.00.verified.txt new file mode 100644 index 000000000..ebf90d64e --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.01.verified.cs.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.01.verified.cs.txt new file mode 100644 index 000000000..3fa348f63 --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=SerialNumber_value=12345.01.verified.cs.txt @@ -0,0 +1,59 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; + +public partial record PatchRocket +{ + public Rocket.Surgery.LaunchPad.Foundation.Assigned Id { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned SerialNumber { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned Type { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public record Changes + { + public bool Id { get; init; } + + public bool SerialNumber { get; init; } + + public bool Type { get; init; } + } + + public Changes GetChangedState() + { + return new Changes() + {Id = Id.HasBeenSet(), SerialNumber = SerialNumber.HasBeenSet(), Type = Type.HasBeenSet()}; + } + + public global::Request ApplyChanges(global::Request value) + { + if (Id.HasBeenSet()) + { + value = value with {Id = Id}; + } + + if (SerialNumber.HasBeenSet()) + { + value = value with {SerialNumber = SerialNumber}; + } + + if (Type.HasBeenSet()) + { + value = value with {Type = Type}; + } + + ResetChanges(); + return value; + } + + public PatchRocket ResetChanges() + { + Id = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Id); + SerialNumber = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(SerialNumber); + Type = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Type); + return this; + } + + void IPropertyTracking.ResetChanges() + { + ResetChanges(); + } +} +#nullable restore diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.00.verified.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.00.verified.txt new file mode 100644 index 000000000..ebf90d64e --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.01.verified.cs.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.01.verified.cs.txt new file mode 100644 index 000000000..3fa348f63 --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes_property=Type_value=12345.01.verified.cs.txt @@ -0,0 +1,59 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; + +public partial record PatchRocket +{ + public Rocket.Surgery.LaunchPad.Foundation.Assigned Id { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned SerialNumber { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned Type { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public record Changes + { + public bool Id { get; init; } + + public bool SerialNumber { get; init; } + + public bool Type { get; init; } + } + + public Changes GetChangedState() + { + return new Changes() + {Id = Id.HasBeenSet(), SerialNumber = SerialNumber.HasBeenSet(), Type = Type.HasBeenSet()}; + } + + public global::Request ApplyChanges(global::Request value) + { + if (Id.HasBeenSet()) + { + value = value with {Id = Id}; + } + + if (SerialNumber.HasBeenSet()) + { + value = value with {SerialNumber = SerialNumber}; + } + + if (Type.HasBeenSet()) + { + value = value with {Type = Type}; + } + + ResetChanges(); + return value; + } + + public PatchRocket ResetChanges() + { + Id = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Id); + SerialNumber = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(SerialNumber); + Type = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Type); + return this; + } + + void IPropertyTracking.ResetChanges() + { + ResetChanges(); + } +} +#nullable restore diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Track_Changes.00.verified.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Track_Changes.00.verified.txt new file mode 100644 index 000000000..ebf90d64e --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Track_Changes.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Track_Changes.01.verified.cs.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Track_Changes.01.verified.cs.txt new file mode 100644 index 000000000..3fa348f63 --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Generate_Record_With_Underlying_Properties_And_Track_Changes.01.verified.cs.txt @@ -0,0 +1,59 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; + +public partial record PatchRocket +{ + public Rocket.Surgery.LaunchPad.Foundation.Assigned Id { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned SerialNumber { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned Type { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public record Changes + { + public bool Id { get; init; } + + public bool SerialNumber { get; init; } + + public bool Type { get; init; } + } + + public Changes GetChangedState() + { + return new Changes() + {Id = Id.HasBeenSet(), SerialNumber = SerialNumber.HasBeenSet(), Type = Type.HasBeenSet()}; + } + + public global::Request ApplyChanges(global::Request value) + { + if (Id.HasBeenSet()) + { + value = value with {Id = Id}; + } + + if (SerialNumber.HasBeenSet()) + { + value = value with {SerialNumber = SerialNumber}; + } + + if (Type.HasBeenSet()) + { + value = value with {Type = Type}; + } + + ResetChanges(); + return value; + } + + public PatchRocket ResetChanges() + { + Id = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Id); + SerialNumber = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(SerialNumber); + Type = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Type); + return this; + } + + void IPropertyTracking.ResetChanges() + { + ResetChanges(); + } +} +#nullable restore diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.00.verified.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.00.verified.txt new file mode 100644 index 000000000..9033f5217 --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.00.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0001, + Title: Type must be made partial, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (9,24)-(9,35), + Description: , + HelpLink: , + MessageFormat: Type {0} must be made partial., + Message: Type Sample.Core.Operations.Rockets.PublicClass must be made partial., + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator: [ + { + Id: LPAD0001, + Title: Type must be made partial, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (9,24)-(9,35), + Description: , + HelpLink: , + MessageFormat: Type {0} must be made partial., + Message: Type Sample.Core.Operations.Rockets.PublicClass must be made partial., + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.verified.cs.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.verified.cs.txt new file mode 100644 index 000000000..d0f233962 --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Partial_Parent_Type_Declaration.01.verified.cs.txt @@ -0,0 +1,56 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; + +namespace Sample.Core.Operations.Rockets +{ + public static class PublicClass + { + public partial record PatchRocket + { + public Rocket.Surgery.LaunchPad.Foundation.Assigned SerialNumber { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned Type { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public record Changes + { + public bool SerialNumber { get; init; } + + public bool Type { get; init; } + } + + public Changes GetChangedState() + { + return new Changes() + {SerialNumber = SerialNumber.HasBeenSet(), Type = Type.HasBeenSet()}; + } + + public global::Sample.Core.Operations.Rockets.Request ApplyChanges(global::Sample.Core.Operations.Rockets.Request value) + { + if (SerialNumber.HasBeenSet()) + { + value = value with {SerialNumber = SerialNumber}; + } + + if (Type.HasBeenSet()) + { + value = value with {Type = Type}; + } + + ResetChanges(); + return value; + } + + public PatchRocket ResetChanges() + { + SerialNumber = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(SerialNumber); + Type = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Type); + return this; + } + + void IPropertyTracking.ResetChanges() + { + ResetChanges(); + } + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Partial_Type_Declaration.verified.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Partial_Type_Declaration.verified.txt new file mode 100644 index 000000000..ed3b24e98 --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Partial_Type_Declaration.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0001, + Title: Type must be made partial, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (9,17)-(9,28), + Description: , + HelpLink: , + MessageFormat: Type {0} must be made partial., + Message: Type Sample.Core.Operations.Rockets.PatchRocket must be made partial., + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator: [ + { + Id: LPAD0001, + Title: Type must be made partial, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (9,17)-(9,28), + Description: , + HelpLink: , + MessageFormat: Type {0} must be made partial., + Message: Type Sample.Core.Operations.Rockets.PatchRocket must be made partial., + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Class.verified.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Class.verified.txt new file mode 100644 index 000000000..c594ef88e --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Class.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0005, + Title: The given declaration must match, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (9,19)-(9,25), + Description: , + HelpLink: , + MessageFormat: The declaration {0} must be a {1}., + Message: The declaration Sample.Core.Operations.Rockets.PatchRocket must be a class., + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator: [ + { + Id: LPAD0005, + Title: The given declaration must match, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (9,19)-(9,25), + Description: , + HelpLink: , + MessageFormat: The declaration {0} must be a {1}., + Message: The declaration Sample.Core.Operations.Rockets.PatchRocket must be a class., + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Record.verified.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Record.verified.txt new file mode 100644 index 000000000..63a2ac366 --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Require_Same_Type_As_Record.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0005, + Title: The given declaration must match, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (9,19)-(9,24), + Description: , + HelpLink: , + MessageFormat: The declaration {0} must be a {1}., + Message: The declaration Sample.Core.Operations.Rockets.PatchRocket must be a record., + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator: [ + { + Id: LPAD0005, + Title: The given declaration must match, + Severity: Error, + WarningLevel: 0, + Location: Test1.cs: (9,19)-(9,24), + Description: , + HelpLink: , + MessageFormat: The declaration {0} must be a {1}., + Message: The declaration Sample.Core.Operations.Rockets.PatchRocket must be a record., + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.00.verified.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.00.verified.txt new file mode 100644 index 000000000..ebf90d64e --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.01.verified.cs.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.01.verified.cs.txt new file mode 100644 index 000000000..438520a70 --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Class_Property.01.verified.cs.txt @@ -0,0 +1,53 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; + +namespace Sample.Core.Operations.Rockets +{ + public partial record PatchRocket + { + public Rocket.Surgery.LaunchPad.Foundation.Assigned SerialNumber { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned Type { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public record Changes + { + public bool SerialNumber { get; init; } + + public bool Type { get; init; } + } + + public Changes GetChangedState() + { + return new Changes() + {SerialNumber = SerialNumber.HasBeenSet(), Type = Type.HasBeenSet()}; + } + + public global::Sample.Core.Operations.Rockets.Request ApplyChanges(global::Sample.Core.Operations.Rockets.Request value) + { + if (SerialNumber.HasBeenSet()) + { + value = value with {SerialNumber = SerialNumber}; + } + + if (Type.HasBeenSet()) + { + value = value with {Type = Type}; + } + + ResetChanges(); + return value; + } + + public PatchRocket ResetChanges() + { + SerialNumber = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(SerialNumber); + Type = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Type); + return this; + } + + void IPropertyTracking.ResetChanges() + { + ResetChanges(); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.00.verified.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.00.verified.txt new file mode 100644 index 000000000..ebf90d64e --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.01.verified.cs.txt b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.01.verified.cs.txt new file mode 100644 index 000000000..461692843 --- /dev/null +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.Should_Support_Nullable_Struct_Property.01.verified.cs.txt @@ -0,0 +1,53 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.PropertyTrackingGenerator/Test1_PatchRocket.cs +#nullable enable +using System; + +namespace Sample.Core.Operations.Rockets +{ + public partial record PatchRocket + { + public Rocket.Surgery.LaunchPad.Foundation.Assigned SerialNumber { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public Rocket.Surgery.LaunchPad.Foundation.Assigned Type { get; set; } = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(default); + public record Changes + { + public bool SerialNumber { get; init; } + + public bool Type { get; init; } + } + + public Changes GetChangedState() + { + return new Changes() + {SerialNumber = SerialNumber.HasBeenSet(), Type = Type.HasBeenSet()}; + } + + public global::Sample.Core.Operations.Rockets.Request ApplyChanges(global::Sample.Core.Operations.Rockets.Request value) + { + if (SerialNumber.HasBeenSet()) + { + value = value with {SerialNumber = SerialNumber}; + } + + if (Type.HasBeenSet()) + { + value = value with {Type = Type}; + } + + ResetChanges(); + return value; + } + + public PatchRocket ResetChanges() + { + SerialNumber = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(SerialNumber); + Type = Rocket.Surgery.LaunchPad.Foundation.Assigned.Empty(Type); + return this; + } + + void IPropertyTracking.ResetChanges() + { + ResetChanges(); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.cs b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.cs index c9ff5aec0..5a58b31c2 100644 --- a/test/Analyzers.Tests/PropertyTrackingGeneratorTests.cs +++ b/test/Analyzers.Tests/PropertyTrackingGeneratorTests.cs @@ -8,16 +8,13 @@ namespace Analyzers.Tests; +[UsesVerify] public class PropertyTrackingGeneratorTests : GeneratorTest { [Fact] public async Task Should_Require_Partial_Type_Declaration() { var source = @" -using System; -using MediatR; -using Rocket.Surgery.LaunchPad.Foundation; - namespace Sample.Core.Operations.Rockets { public class Request : IRequest @@ -37,16 +34,14 @@ public class PatchRocket : IPropertyTracking, IRequest var diagnostic = output!.Diagnostics.Should().HaveCount(1).And.Subject.First(); diagnostic.Id.Should().Be("LPAD0001"); diagnostic.ToString().Should().Contain("Type Sample.Core.Operations.Rockets.PatchRocket must be made partial."); + + await Verify(result); } [Fact] public async Task Should_Require_Partial_Parent_Type_Declaration() { var source = @" -using System; -using MediatR; -using Rocket.Surgery.LaunchPad.Foundation; - namespace Sample.Core.Operations.Rockets { public record Request : IRequest @@ -69,17 +64,14 @@ public partial record PatchRocket : IPropertyTracking, IRequest { public Guid Id { get; init; } @@ -123,18 +115,14 @@ public partial record PatchRocket : IPropertyTracking, IRequest { public Guid Id { get; init; } @@ -173,16 +161,14 @@ public partial class PatchRocket : IPropertyTracking, IRequest @@ -202,16 +188,14 @@ public partial class PatchRocket : IPropertyTracking, IRequest @@ -231,6 +215,56 @@ public partial record PatchRocket : IPropertyTracking, IRequest + { + public Guid Id { get; init; } + public string? SerialNumber { get; set; } = null!; + public int Type { get; set; } + } + public partial record PatchRocket : IPropertyTracking, IRequest + { + public Guid Id { get; init; } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + + await Verify(result); + } + + [Fact] + public async Task Should_Support_Nullable_Struct_Property() + { + var source = @" +namespace Sample.Core.Operations.Rockets +{ + public record Request : IRequest + { + public Guid Id { get; init; } + public string SerialNumber { get; set; } = null!; + public int? Type { get; set; } + } + public partial record PatchRocket : IPropertyTracking, IRequest + { + public Guid Id { get; init; } + } +} +"; + var result = await GenerateAsync(source); + result.TryGetResult(out var output).Should().BeTrue(); + + await Verify(result); } public PropertyTrackingGeneratorTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, LogLevel.Trace) @@ -239,7 +273,10 @@ public PropertyTrackingGeneratorTests(ITestOutputHelper testOutputHelper) : base AddReferences(typeof(IPropertyTracking<>), typeof(IMediator), typeof(IBaseRequest)); AddSources( @" -using System; +global using System; +global using MediatR; +global using Rocket.Surgery.LaunchPad.Foundation; +global using Sample.Core.Operations.Rockets; namespace Sample.Core.Operations.Rockets { public class RocketModel @@ -258,11 +295,6 @@ public class RocketModel public async Task Should_Generate_Record_With_Underlying_Properties_And_Apply_Changes(string property, object value) { var source = @" -using System; -using MediatR; -using Rocket.Surgery.LaunchPad.Foundation; -using Sample.Core.Operations.Rockets; - public record Request : IRequest { public Guid Id { get; init; } @@ -295,6 +327,8 @@ public partial record PatchRocket : IPropertyTracking, IRequest z.GetValue(request)).ToArray()); + + await Verify(result).UseParameters(property, value); } [Theory] @@ -303,11 +337,6 @@ public partial record PatchRocket : IPropertyTracking, IRequest { public Guid Id { get; init; } @@ -341,5 +370,7 @@ public partial class PatchRocket : IPropertyTracking, IRequest z.GetValue(request)).ToArray()); + + await Verify(result).UseParameters(property, value); } } diff --git a/test/Sample.Graphql.Tests/Rockets/UpdateRocketTests.cs b/test/Sample.Graphql.Tests/Rockets/UpdateRocketTests.cs index f37487457..de19817b3 100644 --- a/test/Sample.Graphql.Tests/Rockets/UpdateRocketTests.cs +++ b/test/Sample.Graphql.Tests/Rockets/UpdateRocketTests.cs @@ -1,4 +1,5 @@ -using Humanizer; +using HotChocolate; +using Humanizer; using Microsoft.Extensions.DependencyInjection; using Rocket.Surgery.DependencyInjection; using Sample.Core.Domain; @@ -62,10 +63,10 @@ public async Task Should_Patch_A_Rocket_SerialNumber() ); var u = await client.PatchRocket.ExecuteAsync( - new EditRocketPatchRequestInput + new() { Id = rocket.Id.Value, - SerialNumber = new() { Value = "123456789012345" } + SerialNumber = "123456789012345" } ); u.EnsureNoErrors(); @@ -96,14 +97,16 @@ public async Task Should_Fail_To_Patch_A_Null_Rocket_SerialNumber() ); - Func>> u = () => client.PatchRocket.ExecuteAsync( - new EditRocketPatchRequestInput + var u = await client.PatchRocket.ExecuteAsync( + new() { Id = rocket.Id.Value, - SerialNumber = new() { Value = null } + SerialNumber = null } ); - await u.Should().ThrowAsync(); + + u.IsErrorResult().Should().BeTrue(); + u.Errors[0].Message.Should().Be("'Serial Number' must not be empty."); } [Fact] @@ -128,13 +131,12 @@ public async Task Should_Patch_A_Rocket_Type() ); var u = await client.PatchRocket.ExecuteAsync( - new EditRocketPatchRequestInput + new() { Id = rocket.Id.Value, - Type = new() { Value = RocketType.FalconHeavy } + Type = RocketType.FalconHeavy } ); - u.EnsureNoErrors(); u.Data!.PatchRocket.Type.Should().Be(RocketType.FalconHeavy); u.Data!.PatchRocket.SerialNumber.Should().Be("12345678901234"); diff --git a/test/Sample.Graphql.Tests/schema.graphql b/test/Sample.Graphql.Tests/schema.graphql index 796ae8676..48c436285 100644 --- a/test/Sample.Graphql.Tests/schema.graphql +++ b/test/Sample.Graphql.Tests/schema.graphql @@ -60,6 +60,7 @@ type Mutation { deleteRocket(request: DeleteRocketRequest!): Void! createLaunchRecord(request: CreateLaunchRecordRequest!): CreateLaunchRecordResponse! editLaunchRecord(request: EditLaunchRecordRequest!): LaunchRecordModel! + patchLaunchRecord(request: EditLaunchRecordPatchRequestInput!): LaunchRecordModel! deleteLaunchRecord(request: DeleteLaunchRecordRequest!): Void! } @@ -84,13 +85,13 @@ type LaunchRecord { input LaunchRecordFilterInput { and: [LaunchRecordFilterInput!] or: [LaunchRecordFilterInput!] - id: StronglyTypedIdOperationFilterInput + id: StronglyTypedIdFilterOfUuidTypeFilterInput partner: StringOperationFilterInput payload: StringOperationFilterInput payloadWeightKg: ComparableInt64OperationFilterInput actualLaunchDate: ComparableNullableOfDateTimeOffsetOperationFilterInput scheduledLaunchDate: ComparableDateTimeOffsetOperationFilterInput - rocketId: StronglyTypedIdOperationFilterInput + rocketId: StronglyTypedIdFilterOfUuidTypeFilterInput rocket: ReadyRocketFilterInput } @@ -106,7 +107,7 @@ input LaunchRecordSortInput { input ReadyRocketFilterInput { and: [ReadyRocketFilterInput!] or: [ReadyRocketFilterInput!] - id: StronglyTypedIdOperationFilterInput + id: StronglyTypedIdFilterOfUuidTypeFilterInput serialNumber: StringOperationFilterInput type: RocketTypeOperationFilterInput launchRecords: ListFilterInputTypeOfLaunchRecordFilterInput @@ -152,7 +153,7 @@ type ReadyRocketBase { type: RocketType! } -input StronglyTypedIdOperationFilterInput { +input StronglyTypedIdFilterOfUuidTypeFilterInput { eq: UUID neq: UUID in: [UUID] @@ -291,10 +292,9 @@ input DeleteRocketRequest { } input EditRocketPatchRequestInput { - "The rocket id" id: UUID! - serialNumber: AssignedOfStringInput! - type: AssignedOfRocketTypeInput! + serialNumber: String + type: RocketType! } "The edit operation to update a rocket" @@ -369,16 +369,18 @@ input EditLaunchRecordRequest { rocketId: UUID! } +input EditLaunchRecordPatchRequestInput { + id: UUID! + partner: String + payload: String + payloadWeightKg: Float + actualLaunchDate: Instant + scheduledLaunchDate: Instant + rocketId: UUID +} + "The request to delete a launch record" input DeleteLaunchRecordRequest { "The launch record to delete" id: UUID! -} - -input AssignedOfRocketTypeInput { - value: RocketType! -} - -input AssignedOfStringInput { - value: String! } \ No newline at end of file From 65f0705c65c8122cca4c0e13fc78d770b1076c9e Mon Sep 17 00:00:00 2001 From: David Driscoll Date: Wed, 13 Apr 2022 00:58:56 -0400 Subject: [PATCH 3/3] Updated renovate config --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index b84185d0f..522144225 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -9,7 +9,7 @@ "packageRules": [ { "description": "dotnet monorepo", - "enabled": false, + "enabled": true, "matchSourceUrlPrefixes": [ "https://github.com/dotnet/aspnetcore", "https://github.com/dotnet/efcore",