From 80711c7b6037dd8e58d6c0b6d2235ab33a201c0c Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 5 Mar 2024 13:16:43 +0100 Subject: [PATCH 1/5] Started work on the new source gen. --- .../Factories/SchemaSyntaxVisitorContext.cs | 9 +- .../Core/src/Types/Types/Scalars/JsonType.cs | 9 +- .../Validation/IDocumentValidatorContext.cs | 2 +- .../Filters/Visitor/IFilterVisitorContext.cs | 2 +- .../QueryablePagingProjectionOptimizer.cs | 5 +- .../Sorting/Visitor/ISortVisitorContext.cs | 4 +- .../Fusion/HotChocolate.Fusion.sln | 14 + .../HotChocolate.Fusion.Analyzers.csproj | 45 ++ .../Fusion/src/Analyzers/ProjectClass.cs | 6 + .../Properties/Resources.Designer.cs | 54 +++ .../src/Analyzers/Properties/Resources.resx | 409 ++++++++++++++++++ .../src/Analyzers/TypeModuleGenerator.cs | 42 ++ .../src/Analyzers/WellKnownTypeNames.cs | 6 + .../CommandLine/Commands/ComposeCommand.cs | 15 +- .../Extensions/SchemaExtensions.cs | 2 +- .../src/Core/Execution/Nodes/QueryPlan.cs | 2 +- .../Nodes/ReformatVariableRewriter.cs | 2 +- ...usionGraphConfigurationToSchemaRewriter.cs | 13 +- ...ResolverDefinition.FetchRewriterContext.cs | 32 +- .../RequestDocumentFormatter.cs | 2 +- .../FusionGatewayConfiguration.cs | 22 + ...HotChocolate.Fusion.Analyzers.Tests.csproj | 28 ++ .../test/Analyzers.Tests/IProjectMetadata.cs | 9 + .../Analyzers.Tests/IResourceAnnotation.cs | 5 + .../Fusion/test/Analyzers.Tests/T.cs | 7 + .../Contracts/INavigatorContext.cs | 2 +- .../Contracts/ISyntaxRewriter.cs | 2 +- .../Contracts/ISyntaxVisitorContext.cs | 5 - .../Contracts/ISyntaxVisitor~1.cs | 2 +- .../DelegateSyntaxRewriter.cs | 12 +- .../DelegateSyntaxVisitor.cs | 8 +- .../Language.Visitors/RewriteSyntaxNode.cs | 5 +- .../src/Language.Visitors/SyntaxRewriter.cs | 7 +- .../src/Language.Visitors/SyntaxRewriter~1.cs | 4 +- .../src/Language.Visitors/SyntaxVisitor.cs | 15 +- .../src/Language.Visitors/SyntaxVisitor~1.cs | 4 +- .../Language.Visitors/SyntaxWalker.Enter.cs | 84 ++-- .../Language.Visitors/SyntaxWalker.Leave.cs | 82 ++-- .../Language.Visitors/SyntaxWalker~1.Enter.cs | 2 +- .../Language.Visitors/SyntaxWalker~1.Leave.cs | 2 +- .../src/Language.Visitors/SyntaxWalker~1.cs | 4 +- .../Utilities/SyntaxRewriterExtensions.cs | 10 +- .../Utilities/SyntaxVisitorExtensions.cs | 8 +- .../src/Language.Visitors/VisitSyntaxNode.cs | 9 +- .../Visitors/SchemaCoordinateVisitorTests.cs | 32 +- .../Skimmed/src/Skimmed/Refactor.cs | 21 +- .../Utilities/EntityIdRewriter.cs | 9 +- .../Utilities/ExtractOperationContext.cs | 2 +- .../Utilities/FragmentRewriter.cs | 2 +- .../RemoveClientDirectivesRewriter.cs | 4 +- .../RemovedUnusedFragmentRewriter.cs | 2 +- .../Utilities/TypeNameQueryRewriter.cs | 2 +- 52 files changed, 809 insertions(+), 278 deletions(-) create mode 100644 src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.csproj create mode 100644 src/HotChocolate/Fusion/src/Analyzers/ProjectClass.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.Designer.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.resx create mode 100644 src/HotChocolate/Fusion/src/Analyzers/TypeModuleGenerator.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/WellKnownTypeNames.cs create mode 100644 src/HotChocolate/Fusion/test/Analyzers.Tests/FusionGatewayConfiguration.cs create mode 100644 src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj create mode 100644 src/HotChocolate/Fusion/test/Analyzers.Tests/IProjectMetadata.cs create mode 100644 src/HotChocolate/Fusion/test/Analyzers.Tests/IResourceAnnotation.cs create mode 100644 src/HotChocolate/Fusion/test/Analyzers.Tests/T.cs delete mode 100644 src/HotChocolate/Language/src/Language.Visitors/Contracts/ISyntaxVisitorContext.cs diff --git a/src/HotChocolate/Core/src/Types/Types/Factories/SchemaSyntaxVisitorContext.cs b/src/HotChocolate/Core/src/Types/Types/Factories/SchemaSyntaxVisitorContext.cs index 05149cc87aa..4aed6b11bdf 100644 --- a/src/HotChocolate/Core/src/Types/Types/Factories/SchemaSyntaxVisitorContext.cs +++ b/src/HotChocolate/Core/src/Types/Types/Factories/SchemaSyntaxVisitorContext.cs @@ -7,13 +7,8 @@ namespace HotChocolate.Types.Factories; -internal class SchemaSyntaxVisitorContext : ISyntaxVisitorContext +internal class SchemaSyntaxVisitorContext(IDescriptorContext directiveContext) { - public SchemaSyntaxVisitorContext(IDescriptorContext directiveContext) - { - DirectiveContext = directiveContext; - } - public List Types { get; } = []; public IReadOnlyCollection? Directives { get; set; } @@ -26,5 +21,5 @@ public SchemaSyntaxVisitorContext(IDescriptorContext directiveContext) public string? Description { get; set; } - public IDescriptorContext DirectiveContext { get; } + public IDescriptorContext DirectiveContext { get; } = directiveContext; } diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/JsonType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/JsonType.cs index 34192585f45..cccd91d743f 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/JsonType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/JsonType.cs @@ -268,14 +268,9 @@ protected override ISyntaxVisitorAction Enter( } } - private sealed class JsonFormatterContext : ISyntaxVisitorContext + private sealed class JsonFormatterContext(Utf8JsonWriter writer) { - public JsonFormatterContext(Utf8JsonWriter writer) - { - Writer = writer; - } - - public Utf8JsonWriter Writer { get; } + public Utf8JsonWriter Writer { get; } = writer; } } } diff --git a/src/HotChocolate/Core/src/Validation/IDocumentValidatorContext.cs b/src/HotChocolate/Core/src/Validation/IDocumentValidatorContext.cs index ec0062cc053..6cbfb07034a 100644 --- a/src/HotChocolate/Core/src/Validation/IDocumentValidatorContext.cs +++ b/src/HotChocolate/Core/src/Validation/IDocumentValidatorContext.cs @@ -10,7 +10,7 @@ namespace HotChocolate.Validation; /// This interface represents the document validation context that can /// be used by validation visitors to build up state. /// -public interface IDocumentValidatorContext : ISyntaxVisitorContext +public interface IDocumentValidatorContext { /// /// Gets the schema on which the validation is executed. diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterVisitorContext.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterVisitorContext.cs index 776efabf50a..61bc6fb4e7d 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterVisitorContext.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterVisitorContext.cs @@ -7,7 +7,7 @@ namespace HotChocolate.Data.Filters; /// /// A context object that is passed along the visitation cycle /// -public interface IFilterVisitorContext : ISyntaxVisitorContext +public interface IFilterVisitorContext { /// /// The already visited types diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs index 46487fcbc05..bce60064166 100644 --- a/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs @@ -155,8 +155,7 @@ private static void CollectSelectionOfNodes( SelectionSetOptimizerContext context, List selections) { - if (context.Selections.Values.FirstOrDefault( - x => x.Field.Name == "nodes") is { } nodeSelection) + if (context.Selections.Values.FirstOrDefault(x => x.Field.Name == "nodes") is { } nodeSelection) { foreach (var nodeField in nodeSelection.SelectionSet!.Selections) { @@ -167,7 +166,7 @@ private static void CollectSelectionOfNodes( } } - private static readonly ISyntaxRewriter _cloneSelectionSetRewriter = + private static readonly ISyntaxRewriter _cloneSelectionSetRewriter = SyntaxRewriter.Create( n => n.Kind is SyntaxKind.SelectionSet ? new SelectionSetNode(((SelectionSetNode)n).Selections) diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortVisitorContext.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortVisitorContext.cs index 1f759a10ebf..b457f108b7b 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortVisitorContext.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortVisitorContext.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; -using HotChocolate.Language.Visitors; using HotChocolate.Types; namespace HotChocolate.Data.Sorting; -public interface ISortVisitorContext : ISyntaxVisitorContext +public interface ISortVisitorContext { Stack Types { get; } diff --git a/src/HotChocolate/Fusion/HotChocolate.Fusion.sln b/src/HotChocolate/Fusion/HotChocolate.Fusion.sln index 8244356f6a5..74fa4a68220 100644 --- a/src/HotChocolate/Fusion/HotChocolate.Fusion.sln +++ b/src/HotChocolate/Fusion/HotChocolate.Fusion.sln @@ -25,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Fusion.Command EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Fusion.CommandLine.Tests", "test\CommandLine.Tests\HotChocolate.Fusion.CommandLine.Tests.csproj", "{DBD317C2-8485-4A75-8BB7-D70C02B40944}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Fusion.Analyzers", "src\Analyzers\HotChocolate.Fusion.Analyzers.csproj", "{D212EA75-626D-482E-91DF-3B2BBF185D7C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Fusion.Analyzers.Tests", "test\Analyzers.Tests\HotChocolate.Fusion.Analyzers.Tests.csproj", "{FEA9A31F-CABC-410A-9235-E6D440925F62}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -70,6 +74,14 @@ Global {DBD317C2-8485-4A75-8BB7-D70C02B40944}.Debug|Any CPU.Build.0 = Debug|Any CPU {DBD317C2-8485-4A75-8BB7-D70C02B40944}.Release|Any CPU.ActiveCfg = Release|Any CPU {DBD317C2-8485-4A75-8BB7-D70C02B40944}.Release|Any CPU.Build.0 = Release|Any CPU + {D212EA75-626D-482E-91DF-3B2BBF185D7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D212EA75-626D-482E-91DF-3B2BBF185D7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D212EA75-626D-482E-91DF-3B2BBF185D7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D212EA75-626D-482E-91DF-3B2BBF185D7C}.Release|Any CPU.Build.0 = Release|Any CPU + {FEA9A31F-CABC-410A-9235-E6D440925F62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FEA9A31F-CABC-410A-9235-E6D440925F62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FEA9A31F-CABC-410A-9235-E6D440925F62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FEA9A31F-CABC-410A-9235-E6D440925F62}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {0355AF0F-B91D-4852-8C9F-8E13CE5C88F3} = {748FCFC6-3EE7-4CFD-AFB3-B0F7B1ACD026} @@ -81,5 +93,7 @@ Global {0A07E4BB-0CFE-406C-B3F4-E26D0100F6F9} = {0EF9C546-286E-407F-A02E-731804507FDE} {B4FA97BA-2C36-48F6-ABC4-7088C655E936} = {748FCFC6-3EE7-4CFD-AFB3-B0F7B1ACD026} {DBD317C2-8485-4A75-8BB7-D70C02B40944} = {0EF9C546-286E-407F-A02E-731804507FDE} + {D212EA75-626D-482E-91DF-3B2BBF185D7C} = {748FCFC6-3EE7-4CFD-AFB3-B0F7B1ACD026} + {FEA9A31F-CABC-410A-9235-E6D440925F62} = {0EF9C546-286E-407F-A02E-731804507FDE} EndGlobalSection EndGlobal diff --git a/src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.csproj b/src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.csproj new file mode 100644 index 00000000000..9af5096ae08 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.csproj @@ -0,0 +1,45 @@ + + + + netstandard2.0 + netstandard2.0 + enable + enable + false + false + true + + + + HotChocolate.Fusion.Analyzers + HotChocolate.Fusion.Analyzers + HotChocolate.Fusion.Analyzers + This package provides source generators for HotChocolate Fusion. + + + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + True + True + Resources.resx + + + + diff --git a/src/HotChocolate/Fusion/src/Analyzers/ProjectClass.cs b/src/HotChocolate/Fusion/src/Analyzers/ProjectClass.cs new file mode 100644 index 00000000000..14fae287b0d --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/ProjectClass.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Types.Analyzers; + +public sealed class ProjectClass(string name) +{ + public string Name { get; } = name; +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.Designer.cs b/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.Designer.cs new file mode 100644 index 00000000000..d74bd580a15 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.Designer.cs @@ -0,0 +1,54 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace HotChocolate.Fusion.Analyzers.Properties { + using System; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { + get { + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("HotChocolate.Fusion.Analyzers.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static string CliCode { + get { + return ResourceManager.GetString("CliCode", resourceCulture); + } + } + } +} diff --git a/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.resx b/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.resx new file mode 100644 index 00000000000..b5c55dd8a49 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.resx @@ -0,0 +1,409 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + using System.Buffers; +using System.Text.Json; +using System.Text.Json.Serialization; +using HotChocolate.Fusion; +using HotChocolate.Fusion.Composition; +using HotChocolate.Fusion.Composition.Features; +using HotChocolate.Language; +using HotChocolate.Language.Visitors; +using HotChocolate.Skimmed.Serialization; + +namespace Projects; + +internal static class FusionGatewayConfiguration +{ + public static async Task ComposeAsync(CancellationToken cancellationToken = default) + { + var gatewayDirectory = Path.GetDirectoryName(FusionGatewayConfigurationFiles.GatewayProject)!; + var packageFileName = Path.Combine(gatewayDirectory, $"gateway{Extensions.FusionPackage}"); + var packageFile = new FileInfo(packageFileName); + var settingsFileName = Path.Combine(gatewayDirectory, "gateway-settings.json"); + var settingsFile = new FileInfo(settingsFileName); + var subgraphDirectories = FusionGatewayConfigurationFiles.SubgraphProjects.Select(t => Path.GetDirectoryName(t)!).ToArray(); + + // Ensure Gateway Project Directory Exists. + if (!Directory.Exists(gatewayDirectory)) + { + Directory.CreateDirectory(gatewayDirectory); + } + + await using var package = FusionGraphPackage.Open(packageFile.FullName); + var subgraphConfigs = (await package.GetSubgraphConfigurationsAsync(cancellationToken)).ToDictionary(t => t.Name); + await ResolveSubgraphPackagesAsync(subgraphDirectories, subgraphConfigs, cancellationToken); + + using var settingsJson = settingsFile.Exists + ? JsonDocument.Parse(await File.ReadAllTextAsync(settingsFile.FullName, cancellationToken)) + : await package.GetFusionGraphSettingsAsync(cancellationToken); + var settings = settingsJson.Deserialize<PackageSettings>() ?? new PackageSettings(); + + var features = settings.CreateFeatures(); + + var composer = new FusionGraphComposer( + settings.FusionTypePrefix, + settings.FusionTypeSelf, + () => new ConsoleLog()); + + var fusionGraph = await composer.TryComposeAsync(subgraphConfigs.Values, features, cancellationToken); + + if (fusionGraph is null) + { + Console.WriteLine("Fusion graph composition failed."); + return; + } + + var fusionGraphDoc = Utf8GraphQLParser.Parse(SchemaFormatter.FormatAsString(fusionGraph)); + var typeNames = FusionTypeNames.From(fusionGraphDoc); + var rewriter = new FusionGraphConfigurationToSchemaRewriter(); + var schemaDoc = (DocumentNode)rewriter.Rewrite(fusionGraphDoc, typeNames)!; + + using var updateSettingsJson = JsonSerializer.SerializeToDocument(settings, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + await package.SetFusionGraphAsync(fusionGraphDoc, cancellationToken); + await package.SetFusionGraphSettingsAsync(updateSettingsJson, cancellationToken); + await package.SetSchemaAsync(schemaDoc, cancellationToken); + + foreach (var config in subgraphConfigs.Values) + { + await package.SetSubgraphConfigurationAsync(config, cancellationToken); + } + + Console.WriteLine("Fusion graph composed."); + } + + private static async Task ResolveSubgraphPackagesAsync( + IReadOnlyList<string> subgraphDirectories, + IDictionary<string, SubgraphConfiguration> subgraphConfigs, + CancellationToken cancellationToken) + { + for (var i = 0; i < subgraphDirectories.Count; i++) + { + var path = subgraphDirectories[i]; + + if (!Directory.Exists(path)) + { + continue; + } + + var configFile = Path.Combine(path, Defaults.ConfigFile); + var schemaFile = Path.Combine(path, Defaults.SchemaFile); + var extensionFile = Path.Combine(path, Defaults.ExtensionFile); + + if (!File.Exists(configFile) || !File.Exists(schemaFile)) + { + continue; + } + + var conf = await PackageHelper.LoadSubgraphConfigAsync(configFile, cancellationToken); + var schema = await File.ReadAllTextAsync(schemaFile, cancellationToken); + var extensions = Array.Empty<string>(); + + if (File.Exists(extensionFile)) + { + extensions = [await File.ReadAllTextAsync(extensionFile, cancellationToken),]; + } + + subgraphConfigs[conf.Name] = + new SubgraphConfiguration( + conf.Name, + schema, + extensions, + conf.Clients, + conf.Extensions); + } + } +} + +file static class Defaults +{ + public const string SchemaFile = "schema.graphql"; + public const string ExtensionFile = "schema.extensions.graphql"; + public const string ConfigFile = "subgraph-config.json"; +} + +file static class Extensions +{ + public const string FusionPackage = ".fgp"; +} + +file class PackageSettings +{ + private Feature? _reEncodeIds; + private Feature? _nodeField; + private TagDirective? _tagDirective; + private Transport? _transport; + + [JsonPropertyName("fusionTypePrefix")] + [JsonPropertyOrder(10)] + public string? FusionTypePrefix { get; set; } + + [JsonPropertyName("fusionTypeSelf")] + [JsonPropertyOrder(11)] + public bool FusionTypeSelf { get; set; } + + public Transport Transport + { + get => _transport ??= new(); + set => _transport = value; + } + + [JsonPropertyName("nodeField")] + [JsonPropertyOrder(12)] + public Feature NodeField + { + get => _nodeField ??= new(); + set => _nodeField = value; + } + + [JsonPropertyName("reEncodeIds")] + [JsonPropertyOrder(13)] + public Feature ReEncodeIds + { + get => _reEncodeIds ??= new(); + set => _reEncodeIds = value; + } + + [JsonPropertyName("tagDirective")] + [JsonPropertyOrder(14)] + public TagDirective TagDirective + { + get => _tagDirective ??= new(); + set => _tagDirective = value; + } + + public FusionFeatureCollection CreateFeatures() + { + var features = new List<IFusionFeature> + { + new TransportFeature + { + DefaultClientName = Transport.DefaultClientName, + }, + }; + + if (NodeField.Enabled) + { + features.Add(FusionFeatures.NodeField); + } + + if (ReEncodeIds.Enabled) + { + features.Add(FusionFeatures.ReEncodeIds); + } + + if (TagDirective.Enabled) + { + features.Add( + FusionFeatures.TagDirective( + TagDirective.Exclude, + TagDirective.MakePublic)); + } + + return new FusionFeatureCollection(features); + } +} + +file class Feature +{ + [JsonPropertyName("enabled")] + [JsonPropertyOrder(10)] + public bool Enabled { get; set; } +} + +file sealed class TagDirective : Feature +{ + private string[]? _exclude; + + [JsonPropertyName("makePublic")] + [JsonPropertyOrder(100)] + public bool MakePublic { get; set; } + + [JsonPropertyName("exclude")] + [JsonPropertyOrder(101)] + public string[] Exclude + { + get => _exclude ?? Array.Empty<string>(); + set => _exclude = value; + } +} + +file sealed class Transport +{ + [JsonPropertyName("defaultClientName")] + [JsonPropertyOrder(10)] + public string? DefaultClientName { get; set; } = "Fusion"; +} + +file sealed class ConsoleLog : ICompositionLog +{ + public bool HasErrors { get; private set; } + + public void Write(LogEntry e) + { + if (e.Severity is LogSeverity.Error) + { + HasErrors = true; + } + + if (e.Code is null) + { + Console.WriteLine($"{e.Severity}: {e.Message}"); + } + else if (e.Coordinate is null) + { + Console.WriteLine($"{e.Severity}: {e.Code} {e.Message}"); + } + else + { + Console.WriteLine($"{e.Severity}: {e.Code} {e.Message} {e.Coordinate}"); + } + } +} + +file sealed class SubgraphConfigurationDto( + string name, + IReadOnlyList<IClientConfiguration>? clients = null, + JsonElement? extensions = null) +{ + public string Name { get; } = name; + + public IReadOnlyList<IClientConfiguration> Clients { get; } = clients ?? Array.Empty<IClientConfiguration>(); + + public JsonElement? Extensions { get; } = extensions; +} + +file static class PackageHelper +{ + public static async Task<SubgraphConfigurationDto> LoadSubgraphConfigAsync( + string filename, + CancellationToken ct) + { + await using var stream = File.OpenRead(filename); + return await ParseSubgraphConfigAsync(stream, ct); + } + + private static async Task<SubgraphConfigurationDto> ParseSubgraphConfigAsync( + Stream stream, + CancellationToken ct) + { + using var document = await JsonDocument.ParseAsync(stream, cancellationToken: ct); + var configs = new List<IClientConfiguration>(); + var subgraph = default(string?); + var extensions = default(JsonElement?); + + foreach (var property in document.RootElement.EnumerateObject()) + { + switch (property.Name) + { + case "subgraph": + subgraph = property.Value.GetString(); + break; + + case "http": + configs.Add(ReadHttpClientConfiguration(property.Value)); + break; + + case "websocket": + configs.Add(ReadWebSocketClientConfiguration(property.Value)); + break; + + case "extensions": + extensions = property.Value.SafeClone(); + break; + + default: + throw new NotSupportedException( + $"Configuration property `{property.Value}` is not supported."); + } + } + + if (string.IsNullOrEmpty(subgraph)) + { + throw new InvalidOperationException("No subgraph name was specified."); + } + + return new SubgraphConfigurationDto(subgraph, configs, extensions); + } + + private static HttpClientConfiguration ReadHttpClientConfiguration( + JsonElement element) + { + var baseAddress = new Uri(element.GetProperty("baseAddress").GetString()!); + var clientName = default(string?); + + if (element.TryGetProperty("clientName", out var clientNameElement)) + { + clientName = clientNameElement.GetString(); + } + + return new HttpClientConfiguration(baseAddress, clientName); + } + + private static WebSocketClientConfiguration ReadWebSocketClientConfiguration( + JsonElement element) + { + var baseAddress = new Uri(element.GetProperty("baseAddress").GetString()!); + var clientName = default(string?); + + if (element.TryGetProperty("clientName", out var clientNameElement)) + { + clientName = clientNameElement.GetString(); + } + + return new WebSocketClientConfiguration(baseAddress, clientName); + } + + private static JsonElement SafeClone(this JsonElement element) + { + var writer = new ArrayBufferWriter<byte>(); + using var jsonWriter = new Utf8JsonWriter(writer); + + element.WriteTo(jsonWriter); + jsonWriter.Flush(); + var reader = new Utf8JsonReader(writer.WrittenSpan, true, default); + + return JsonElement.ParseValue(ref reader); + } +} + +file sealed class FusionGraphConfigurationToSchemaRewriter + : SyntaxRewriter<FusionTypeNames> +{ + public DocumentNode Rewrite(DocumentNode fusionGraph) + { + var typeNames = FusionTypeNames.From(fusionGraph); + var schemaDoc = (DocumentNode?)Rewrite(fusionGraph, typeNames); + + if (schemaDoc is null) + { + throw new InvalidOperationException(); + } + + return schemaDoc; + } + + protected override DirectiveNode? RewriteDirective(DirectiveNode node, FusionTypeNames context) + => context.IsFusionDirective(node.Name.Value) ? null : base.RewriteDirective(node, context); +} + + \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/TypeModuleGenerator.cs b/src/HotChocolate/Fusion/src/Analyzers/TypeModuleGenerator.cs new file mode 100644 index 00000000000..2493b91dac0 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/TypeModuleGenerator.cs @@ -0,0 +1,42 @@ +using System.Collections.Immutable; +using HotChocolate.Fusion.Analyzers.Properties; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace HotChocolate.Types.Analyzers; + +[Generator] +public class TypeModuleGenerator : IIncrementalGenerator +{ + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var modulesAndTypes = + context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (s, _) => IsRelevant(s), + transform: TryGetProjectClass) + .Where(static t => t is not null); + + var valueProvider = context.CompilationProvider.Combine(modulesAndTypes.Collect()); + + context.RegisterSourceOutput( + valueProvider, + static (context, source) => Execute(context, source.Left, source.Right!)); + + } + + private static bool IsRelevant(SyntaxNode node) + => node is ClassDeclarationSyntax { BaseList.Types.Count: > 0, }; + + private ProjectClass? TryGetProjectClass(GeneratorSyntaxContext context, CancellationToken cancellationToken) + => new ProjectClass("abc"); + + private static void Execute( + SourceProductionContext context, + Compilation compilation, + ImmutableArray syntaxInfos) + { + context.AddSource("FusionGatewayConfiguration.g.cs", Resources.CliCode); + } +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/WellKnownTypeNames.cs b/src/HotChocolate/Fusion/src/Analyzers/WellKnownTypeNames.cs new file mode 100644 index 00000000000..559cecee435 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/WellKnownTypeNames.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Types.Analyzers; + +public static class WellKnownTypeNames +{ + public const string ProjectMetadata = "Aspire.Hosting.IProjectMetadata"; +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/CommandLine/Commands/ComposeCommand.cs b/src/HotChocolate/Fusion/src/CommandLine/Commands/ComposeCommand.cs index c309064064c..207eb46ab2d 100644 --- a/src/HotChocolate/Fusion/src/CommandLine/Commands/ComposeCommand.cs +++ b/src/HotChocolate/Fusion/src/CommandLine/Commands/ComposeCommand.cs @@ -280,15 +280,8 @@ private static async Task ResolveSubgraphPackagesAsync( } } - private sealed class ConsoleLog : ICompositionLog + private sealed class ConsoleLog(IConsole console) : ICompositionLog { - private readonly IConsole _console; - - public ConsoleLog(IConsole console) - { - _console = console; - } - public bool HasErrors { get; private set; } public void Write(LogEntry e) @@ -300,15 +293,15 @@ public void Write(LogEntry e) if (e.Code is null) { - _console.WriteLine($"{e.Severity}: {e.Message}"); + console.WriteLine($"{e.Severity}: {e.Message}"); } else if (e.Coordinate is null) { - _console.WriteLine($"{e.Severity}: {e.Code} {e.Message}"); + console.WriteLine($"{e.Severity}: {e.Code} {e.Message}"); } else { - _console.WriteLine($"{e.Severity}: {e.Code} {e.Message} {e.Coordinate}"); + console.WriteLine($"{e.Severity}: {e.Code} {e.Message} {e.Coordinate}"); } } } diff --git a/src/HotChocolate/Fusion/src/Composition/Extensions/SchemaExtensions.cs b/src/HotChocolate/Fusion/src/Composition/Extensions/SchemaExtensions.cs index 003d60d95c6..870ce74169e 100644 --- a/src/HotChocolate/Fusion/src/Composition/Extensions/SchemaExtensions.cs +++ b/src/HotChocolate/Fusion/src/Composition/Extensions/SchemaExtensions.cs @@ -81,7 +81,7 @@ protected override ISyntaxVisitorAction Enter( } } - private sealed class FieldVariableNameContext : ISyntaxVisitorContext + private sealed class FieldVariableNameContext { public StringBuilder Name { get; } = new(); } diff --git a/src/HotChocolate/Fusion/src/Core/Execution/Nodes/QueryPlan.cs b/src/HotChocolate/Fusion/src/Core/Execution/Nodes/QueryPlan.cs index e95e9cf4124..52ae77426d9 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/Nodes/QueryPlan.cs +++ b/src/HotChocolate/Fusion/src/Core/Execution/Nodes/QueryPlan.cs @@ -337,7 +337,7 @@ protected override ISyntaxVisitorAction Enter( public static ExportPathVisitor Instance { get; } = new(); } - private sealed class ExportPathVisitorContext : ISyntaxVisitorContext + private sealed class ExportPathVisitorContext { public Queue Path { get; } = new(); } diff --git a/src/HotChocolate/Fusion/src/Core/Execution/Nodes/ReformatVariableRewriter.cs b/src/HotChocolate/Fusion/src/Core/Execution/Nodes/ReformatVariableRewriter.cs index adcdcd4d8af..66d91725859 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/Nodes/ReformatVariableRewriter.cs +++ b/src/HotChocolate/Fusion/src/Core/Execution/Nodes/ReformatVariableRewriter.cs @@ -5,7 +5,7 @@ namespace HotChocolate.Fusion.Execution.Nodes; -internal sealed class ReformatVariableRewriter : SyntaxRewriter, ISyntaxVisitorContext +internal sealed class ReformatVariableRewriter : SyntaxRewriter { private static readonly ReformatVariableRewriter _instance = new(); diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/FusionGraphConfigurationToSchemaRewriter.cs b/src/HotChocolate/Fusion/src/Core/Metadata/FusionGraphConfigurationToSchemaRewriter.cs index dcb427e8b8b..ed078df3415 100644 --- a/src/HotChocolate/Fusion/src/Core/Metadata/FusionGraphConfigurationToSchemaRewriter.cs +++ b/src/HotChocolate/Fusion/src/Core/Metadata/FusionGraphConfigurationToSchemaRewriter.cs @@ -22,9 +22,7 @@ public DocumentNode Rewrite(DocumentNode fusionGraph) return schemaDoc; } - protected override DirectiveNode? RewriteDirective( - DirectiveNode node, - Context context) + protected override DirectiveNode? RewriteDirective(DirectiveNode node, Context context) { if (context.TypeNames.IsFusionDirective(node.Name.Value)) { @@ -34,13 +32,8 @@ public DocumentNode Rewrite(DocumentNode fusionGraph) return base.RewriteDirective(node, context); } - internal sealed class Context : ISyntaxVisitorContext + internal sealed class Context(FusionTypeNames typeNames) { - public Context(FusionTypeNames typeNames) - { - TypeNames = typeNames; - } - - public FusionTypeNames TypeNames { get; } + public FusionTypeNames TypeNames { get; } = typeNames; } } diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ResolverDefinition.FetchRewriterContext.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ResolverDefinition.FetchRewriterContext.cs index 7b221eeaaf2..4fa26741f36 100644 --- a/src/HotChocolate/Fusion/src/Core/Metadata/ResolverDefinition.FetchRewriterContext.cs +++ b/src/HotChocolate/Fusion/src/Core/Metadata/ResolverDefinition.FetchRewriterContext.cs @@ -1,42 +1,32 @@ using HotChocolate.Language; -using HotChocolate.Language.Visitors; namespace HotChocolate.Fusion.Metadata; internal sealed partial class ResolverDefinition { - private sealed class FetchRewriterContext : ISyntaxVisitorContext + private sealed class FetchRewriterContext( + FragmentSpreadNode? placeholder, + IReadOnlyDictionary variables, + SelectionSetNode? selectionSet, + string? responseName, + IReadOnlyList? unspecifiedArguments) { - public FetchRewriterContext( - FragmentSpreadNode? placeholder, - IReadOnlyDictionary variables, - SelectionSetNode? selectionSet, - string? responseName, - IReadOnlyList? unspecifiedArguments) - { - Placeholder = placeholder; - Variables = variables; - SelectionSet = selectionSet; - ResponseName = responseName; - UnspecifiedArguments = unspecifiedArguments; - } - - public string? ResponseName { get; } + public string? ResponseName { get; } = responseName; public Stack Path { get; } = new(); - public FragmentSpreadNode? Placeholder { get; } + public FragmentSpreadNode? Placeholder { get; } = placeholder; public bool PlaceholderFound { get; set; } - public IReadOnlyDictionary Variables { get; } + public IReadOnlyDictionary Variables { get; } = variables; /// /// An optional list of arguments that weren't explicitly specified in the original query. /// - public IReadOnlyList? UnspecifiedArguments { get; } + public IReadOnlyList? UnspecifiedArguments { get; } = unspecifiedArguments; - public SelectionSetNode? SelectionSet { get; } + public SelectionSetNode? SelectionSet { get; } = selectionSet; public IReadOnlyList SelectionPath { get; set; } = Array.Empty(); } diff --git a/src/HotChocolate/Fusion/src/Core/Planning/RequestFormatters/RequestDocumentFormatter.cs b/src/HotChocolate/Fusion/src/Core/Planning/RequestFormatters/RequestDocumentFormatter.cs index 86a5c38d37b..7e0f91df47e 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/RequestFormatters/RequestDocumentFormatter.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/RequestFormatters/RequestDocumentFormatter.cs @@ -576,7 +576,7 @@ public static IEnumerable Collect(IValueNode node) } } - private class VariableVisitorContext : ISyntaxVisitorContext + private sealed class VariableVisitorContext { public HashSet VariableNodes { get; } = new(SyntaxComparer.BySyntax); } diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/FusionGatewayConfiguration.cs b/src/HotChocolate/Fusion/test/Analyzers.Tests/FusionGatewayConfiguration.cs new file mode 100644 index 00000000000..3430213f9ec --- /dev/null +++ b/src/HotChocolate/Fusion/test/Analyzers.Tests/FusionGatewayConfiguration.cs @@ -0,0 +1,22 @@ +using System.Buffers; +using System.Text.Json; +using System.Text.Json.Serialization; +using HotChocolate.Fusion; +using HotChocolate.Fusion.Composition; +using HotChocolate.Fusion.Composition.Features; +using HotChocolate.Language; +using HotChocolate.Language.Visitors; +using HotChocolate.Skimmed.Serialization; + +namespace Projects; + +public static class FusionGatewayConfigurationFiles +{ + public static readonly string[] SubgraphProjects = + [ + """/Users/michael/local/webshop-workshop/src/Basket.API/eShop.Basket.API.csproj""", + ]; + + public const string GatewayProject = + """/Users/michael/local/webshop-workshop/src/Gateway/eShop.Gateway.csproj"""; +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj b/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj new file mode 100644 index 00000000000..549c96c3dd9 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/IProjectMetadata.cs b/src/HotChocolate/Fusion/test/Analyzers.Tests/IProjectMetadata.cs new file mode 100644 index 00000000000..8af381ba071 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Analyzers.Tests/IProjectMetadata.cs @@ -0,0 +1,9 @@ +namespace Aspire.Hosting; + +public interface IProjectMetadata : IResourceAnnotation +{ + /// + /// Gets the fully-qualified path to the project. + /// + public string ProjectPath { get; } +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/IResourceAnnotation.cs b/src/HotChocolate/Fusion/test/Analyzers.Tests/IResourceAnnotation.cs new file mode 100644 index 00000000000..62db54a0cd8 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Analyzers.Tests/IResourceAnnotation.cs @@ -0,0 +1,5 @@ +namespace Aspire.Hosting; + +public interface IResourceAnnotation +{ +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/T.cs b/src/HotChocolate/Fusion/test/Analyzers.Tests/T.cs new file mode 100644 index 00000000000..753b4020d93 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Analyzers.Tests/T.cs @@ -0,0 +1,7 @@ +namespace Projects; + +[global::System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, ProjectPath = {ProjectPath}")] +public class eShop_Basket_API : global::Aspire.Hosting.IProjectMetadata +{ + public string ProjectPath => """/Users/michael/local/webshop-workshop/src/Basket.API/eShop.Basket.API.csproj"""; +} \ No newline at end of file diff --git a/src/HotChocolate/Language/src/Language.Visitors/Contracts/INavigatorContext.cs b/src/HotChocolate/Language/src/Language.Visitors/Contracts/INavigatorContext.cs index 1010d22b21e..689e6d5bf7b 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/Contracts/INavigatorContext.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/Contracts/INavigatorContext.cs @@ -3,7 +3,7 @@ namespace HotChocolate.Language.Visitors; /// /// A visitor context that contains a syntax navigator. /// -public interface INavigatorContext : ISyntaxVisitorContext +public interface INavigatorContext { /// /// Gets the associated from the current context. diff --git a/src/HotChocolate/Language/src/Language.Visitors/Contracts/ISyntaxRewriter.cs b/src/HotChocolate/Language/src/Language.Visitors/Contracts/ISyntaxRewriter.cs index 8e364aa0208..3feee422033 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/Contracts/ISyntaxRewriter.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/Contracts/ISyntaxRewriter.cs @@ -7,7 +7,7 @@ namespace HotChocolate.Language.Visitors; /// /// The context type. /// -public interface ISyntaxRewriter where TContext : ISyntaxVisitorContext +public interface ISyntaxRewriter { /// /// Rewrite the syntax node. diff --git a/src/HotChocolate/Language/src/Language.Visitors/Contracts/ISyntaxVisitorContext.cs b/src/HotChocolate/Language/src/Language.Visitors/Contracts/ISyntaxVisitorContext.cs deleted file mode 100644 index 22e142b8120..00000000000 --- a/src/HotChocolate/Language/src/Language.Visitors/Contracts/ISyntaxVisitorContext.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace HotChocolate.Language.Visitors; - -public interface ISyntaxVisitorContext -{ -} diff --git a/src/HotChocolate/Language/src/Language.Visitors/Contracts/ISyntaxVisitor~1.cs b/src/HotChocolate/Language/src/Language.Visitors/Contracts/ISyntaxVisitor~1.cs index a770cdc258b..26d6ed2a624 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/Contracts/ISyntaxVisitor~1.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/Contracts/ISyntaxVisitor~1.cs @@ -1,6 +1,6 @@ namespace HotChocolate.Language.Visitors; -public interface ISyntaxVisitor where TContext : ISyntaxVisitorContext +public interface ISyntaxVisitor { ISyntaxVisitorAction Visit(ISyntaxNode node, TContext context); } diff --git a/src/HotChocolate/Language/src/Language.Visitors/DelegateSyntaxRewriter.cs b/src/HotChocolate/Language/src/Language.Visitors/DelegateSyntaxRewriter.cs index 02884cf720b..e4d70c62967 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/DelegateSyntaxRewriter.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/DelegateSyntaxRewriter.cs @@ -1,10 +1,6 @@ -using System; - namespace HotChocolate.Language.Visitors; -internal sealed class DelegateSyntaxRewriter - : SyntaxRewriter - where TContext : ISyntaxVisitorContext +internal sealed class DelegateSyntaxRewriter : SyntaxRewriter { private readonly RewriteSyntaxNode _rewrite; private readonly Func _enter; @@ -15,9 +11,9 @@ public DelegateSyntaxRewriter( Func? enter = null, Action? leave = null) { - _rewrite = rewrite ?? new RewriteSyntaxNode(static (node, _) => node); - _enter = enter ?? new Func(static (_, ctx) => ctx); - _leave = leave ?? new Action(static (_, _) => { }); + _rewrite = rewrite ?? (static (node, _) => node); + _enter = enter ?? (static (_, ctx) => ctx); + _leave = leave ?? (static (_, _) => { }); } protected override TContext OnEnter( diff --git a/src/HotChocolate/Language/src/Language.Visitors/DelegateSyntaxVisitor.cs b/src/HotChocolate/Language/src/Language.Visitors/DelegateSyntaxVisitor.cs index 31ce56d419c..c9ce3e732af 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/DelegateSyntaxVisitor.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/DelegateSyntaxVisitor.cs @@ -1,8 +1,6 @@ namespace HotChocolate.Language.Visitors; -internal sealed class DelegateSyntaxVisitor - : SyntaxVisitor - where TContext : ISyntaxVisitorContext +internal sealed class DelegateSyntaxVisitor : SyntaxVisitor { private readonly VisitSyntaxNode _enter; private readonly VisitSyntaxNode _leave; @@ -14,8 +12,8 @@ public DelegateSyntaxVisitor( SyntaxVisitorOptions options = default) : base(defaultResult ?? Skip, options) { - _enter = enter ?? new VisitSyntaxNode((_, _) => DefaultAction); - _leave = leave ?? new VisitSyntaxNode((_, _) => DefaultAction); + _enter = enter ?? ((_, _) => DefaultAction); + _leave = leave ?? ((_, _) => DefaultAction); } protected override ISyntaxVisitorAction Enter(ISyntaxNode node, TContext context) diff --git a/src/HotChocolate/Language/src/Language.Visitors/RewriteSyntaxNode.cs b/src/HotChocolate/Language/src/Language.Visitors/RewriteSyntaxNode.cs index 662a817408d..856e4f84794 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/RewriteSyntaxNode.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/RewriteSyntaxNode.cs @@ -1,6 +1,3 @@ namespace HotChocolate.Language.Visitors; -public delegate ISyntaxNode? RewriteSyntaxNode( - ISyntaxNode node, - TContext context) - where TContext : ISyntaxVisitorContext; +public delegate ISyntaxNode? RewriteSyntaxNode(ISyntaxNode node, TContext context); diff --git a/src/HotChocolate/Language/src/Language.Visitors/SyntaxRewriter.cs b/src/HotChocolate/Language/src/Language.Visitors/SyntaxRewriter.cs index 860b706db5b..8ac057924c4 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/SyntaxRewriter.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/SyntaxRewriter.cs @@ -1,5 +1,3 @@ -using System; - namespace HotChocolate.Language.Visitors; /// @@ -7,16 +5,15 @@ namespace HotChocolate.Language.Visitors; /// public static class SyntaxRewriter { - public static ISyntaxRewriter Create( + public static ISyntaxRewriter Create( Func rewrite) - => new DelegateSyntaxRewriter( + => new DelegateSyntaxRewriter( rewrite: (node, _) => rewrite(node)); public static ISyntaxRewriter Create( RewriteSyntaxNode? rewrite = null, Func? enter = null, Action? leave = null) - where TContext : ISyntaxVisitorContext => new DelegateSyntaxRewriter(rewrite, enter, leave); public static ISyntaxRewriter CreateWithNavigator( diff --git a/src/HotChocolate/Language/src/Language.Visitors/SyntaxRewriter~1.cs b/src/HotChocolate/Language/src/Language.Visitors/SyntaxRewriter~1.cs index f621e2f64df..e8d59a8697d 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/SyntaxRewriter~1.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/SyntaxRewriter~1.cs @@ -10,9 +10,7 @@ namespace HotChocolate.Language.Visitors; /// /// The context type. /// -public class SyntaxRewriter - : ISyntaxRewriter - where TContext : ISyntaxVisitorContext +public class SyntaxRewriter : ISyntaxRewriter { public virtual ISyntaxNode? Rewrite(ISyntaxNode node, TContext context) { diff --git a/src/HotChocolate/Language/src/Language.Visitors/SyntaxVisitor.cs b/src/HotChocolate/Language/src/Language.Visitors/SyntaxVisitor.cs index d81721f1e1a..55e6f320e85 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/SyntaxVisitor.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/SyntaxVisitor.cs @@ -1,8 +1,6 @@ -using System; - namespace HotChocolate.Language.Visitors; -public class SyntaxVisitor : SyntaxVisitor +public class SyntaxVisitor : SyntaxVisitor { public SyntaxVisitor(SyntaxVisitorOptions options = default) : base(options) @@ -16,17 +14,17 @@ public SyntaxVisitor( { } - public static ISyntaxVisitor Create( + public static ISyntaxVisitor Create( Func? enter = null, Func? leave = null, ISyntaxVisitorAction? defaultAction = null, SyntaxVisitorOptions options = default) - => new DelegateSyntaxVisitor( + => new DelegateSyntaxVisitor( enter is not null - ? new VisitSyntaxNode((n, _) => enter(n)) + ? new VisitSyntaxNode((n, _) => enter(n)) : null, leave is not null - ? new VisitSyntaxNode((n, _) => leave(n)) + ? new VisitSyntaxNode((n, _) => leave(n)) : null, defaultAction, options); @@ -36,7 +34,6 @@ public static ISyntaxVisitor Create( VisitSyntaxNode? leave = null, ISyntaxVisitorAction? defaultAction = null, SyntaxVisitorOptions options = default) - where TContext : ISyntaxVisitorContext { defaultAction ??= Skip; @@ -73,7 +70,7 @@ public static ISyntaxVisitor CreateWithNavigator( context.Navigator.Pop(); return leave(node, context); } - : (node, context) => + : (_, context) => { context.Navigator.Pop(); return defaultAction; diff --git a/src/HotChocolate/Language/src/Language.Visitors/SyntaxVisitor~1.cs b/src/HotChocolate/Language/src/Language.Visitors/SyntaxVisitor~1.cs index e41439e7279..a9bb3b4bc2e 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/SyntaxVisitor~1.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/SyntaxVisitor~1.cs @@ -1,8 +1,6 @@ namespace HotChocolate.Language.Visitors; -public partial class SyntaxVisitor - : ISyntaxVisitor - where TContext : ISyntaxVisitorContext +public partial class SyntaxVisitor : ISyntaxVisitor { private readonly SyntaxVisitorOptions _options; diff --git a/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker.Enter.cs b/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker.Enter.cs index 1ac868807a6..babb654619e 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker.Enter.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker.Enter.cs @@ -1,12 +1,8 @@ -using System; - namespace HotChocolate.Language.Visitors; public partial class SyntaxWalker { - protected override ISyntaxVisitorAction Enter( - ISyntaxNode node, - ISyntaxVisitorContext context) + protected override ISyntaxVisitorAction Enter(ISyntaxNode node, object? context) { switch (node.Kind) { @@ -100,196 +96,196 @@ protected override ISyntaxVisitorAction Enter( protected virtual ISyntaxVisitorAction Enter( NameNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( DocumentNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( OperationDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( VariableDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( VariableNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( SelectionSetNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( FieldNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( ArgumentNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( FragmentSpreadNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( InlineFragmentNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( FragmentDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( DirectiveNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( NamedTypeNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( ListTypeNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( NonNullTypeNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( ListValueNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( ObjectValueNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( ObjectFieldNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( IValueNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( SchemaDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( OperationTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( ScalarTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( ObjectTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( FieldDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( InputValueDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( InterfaceTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( UnionTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( EnumTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( EnumValueDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( InputObjectTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( DirectiveDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( SchemaExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( ScalarTypeExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( ObjectTypeExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( InterfaceTypeExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( UnionTypeExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( EnumTypeExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( InputObjectTypeExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Enter( SchemaCoordinateNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; } diff --git a/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker.Leave.cs b/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker.Leave.cs index f25342a4a4e..9cf727f0938 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker.Leave.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker.Leave.cs @@ -1,12 +1,10 @@ -using System; - namespace HotChocolate.Language.Visitors; public partial class SyntaxWalker { protected override ISyntaxVisitorAction Leave( ISyntaxNode node, - ISyntaxVisitorContext context) + object? context) { switch (node.Kind) { @@ -99,196 +97,196 @@ protected override ISyntaxVisitorAction Leave( } protected virtual ISyntaxVisitorAction Leave( NameNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( DocumentNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( OperationDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( VariableDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( VariableNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( SelectionSetNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( FieldNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( ArgumentNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( FragmentSpreadNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( InlineFragmentNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( FragmentDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( DirectiveNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( NamedTypeNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( ListTypeNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( NonNullTypeNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( ListValueNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( ObjectValueNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( ObjectFieldNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( IValueNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( SchemaDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( OperationTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( ScalarTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( ObjectTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( FieldDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( InputValueDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( InterfaceTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( UnionTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( EnumTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( EnumValueDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( InputObjectTypeDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( DirectiveDefinitionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( SchemaExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( ScalarTypeExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( ObjectTypeExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( InterfaceTypeExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( UnionTypeExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( EnumTypeExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( InputObjectTypeExtensionNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; protected virtual ISyntaxVisitorAction Leave( SchemaCoordinateNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; } diff --git a/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker~1.Enter.cs b/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker~1.Enter.cs index 826fb56c728..b386d9a23b5 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker~1.Enter.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker~1.Enter.cs @@ -290,6 +290,6 @@ protected virtual ISyntaxVisitorAction Enter( protected virtual ISyntaxVisitorAction Enter( SchemaCoordinateNode node, - ISyntaxVisitorContext context) => + object? context) => DefaultAction; } diff --git a/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker~1.Leave.cs b/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker~1.Leave.cs index 112c5bd9e68..3565cb02bc5 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker~1.Leave.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker~1.Leave.cs @@ -289,6 +289,6 @@ protected virtual ISyntaxVisitorAction Leave( protected virtual ISyntaxVisitorAction Leave( SchemaCoordinateNode node, - ISyntaxVisitorContext context) => + TContext context) => DefaultAction; } diff --git a/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker~1.cs b/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker~1.cs index 46da15e02a2..b49845bedae 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker~1.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/SyntaxWalker~1.cs @@ -1,8 +1,6 @@ namespace HotChocolate.Language.Visitors; -public partial class SyntaxWalker - : SyntaxVisitor - where TContext : ISyntaxVisitorContext +public partial class SyntaxWalker : SyntaxVisitor { protected SyntaxWalker(SyntaxVisitorOptions options = default) : base(Continue, options) diff --git a/src/HotChocolate/Language/src/Language.Visitors/Utilities/SyntaxRewriterExtensions.cs b/src/HotChocolate/Language/src/Language.Visitors/Utilities/SyntaxRewriterExtensions.cs index 6dadb1cbaf0..490264bdc84 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/Utilities/SyntaxRewriterExtensions.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/Utilities/SyntaxRewriterExtensions.cs @@ -2,20 +2,16 @@ namespace HotChocolate.Language.Visitors; public static class SyntaxRewriterExtensions { - private static readonly EmptySyntaxVisitorContext _empty = new(); + private static readonly object? _empty = new(); public static ISyntaxNode? Rewrite( - this ISyntaxRewriter rewriter, + this ISyntaxRewriter rewriter, ISyntaxNode node) => rewriter.Rewrite(node, _empty); public static T? Rewrite( - this ISyntaxRewriter rewriter, + this ISyntaxRewriter rewriter, T node) where T : ISyntaxNode => (T?)rewriter.Rewrite(node, _empty); - - private sealed class EmptySyntaxVisitorContext : ISyntaxVisitorContext - { - } } diff --git a/src/HotChocolate/Language/src/Language.Visitors/Utilities/SyntaxVisitorExtensions.cs b/src/HotChocolate/Language/src/Language.Visitors/Utilities/SyntaxVisitorExtensions.cs index 9cb45949dd0..b4068ba9844 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/Utilities/SyntaxVisitorExtensions.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/Utilities/SyntaxVisitorExtensions.cs @@ -2,14 +2,10 @@ namespace HotChocolate.Language.Visitors; public static class SyntaxVisitorExtensions { - private static readonly EmptySyntaxVisitorContext _empty = new(); + private static readonly object _empty = new(); public static ISyntaxVisitorAction Visit( - this ISyntaxVisitor visitor, + this ISyntaxVisitor visitor, ISyntaxNode node) => visitor.Visit(node, _empty); - - private sealed class EmptySyntaxVisitorContext : ISyntaxVisitorContext - { - } } diff --git a/src/HotChocolate/Language/src/Language.Visitors/VisitSyntaxNode.cs b/src/HotChocolate/Language/src/Language.Visitors/VisitSyntaxNode.cs index ff363e81867..b9e312e9ab2 100644 --- a/src/HotChocolate/Language/src/Language.Visitors/VisitSyntaxNode.cs +++ b/src/HotChocolate/Language/src/Language.Visitors/VisitSyntaxNode.cs @@ -1,10 +1,3 @@ namespace HotChocolate.Language.Visitors; -public delegate ISyntaxVisitorAction VisitSyntaxNode( - ISyntaxNode node, - ISyntaxVisitorContext context); - -public delegate ISyntaxVisitorAction VisitSyntaxNode( - ISyntaxNode node, - TContext context) - where TContext : ISyntaxVisitorContext; +public delegate ISyntaxVisitorAction VisitSyntaxNode(ISyntaxNode node, TContext context); diff --git a/src/HotChocolate/Language/test/Language.Tests/Visitors/SchemaCoordinateVisitorTests.cs b/src/HotChocolate/Language/test/Language.Tests/Visitors/SchemaCoordinateVisitorTests.cs index 183d6c03ba5..260c4db2371 100644 --- a/src/HotChocolate/Language/test/Language.Tests/Visitors/SchemaCoordinateVisitorTests.cs +++ b/src/HotChocolate/Language/test/Language.Tests/Visitors/SchemaCoordinateVisitorTests.cs @@ -75,30 +75,19 @@ public static void VisitAllNodes_With_Generic_Walker() s => Assert.Equal("ghi", s)); } - public class CustomSyntaxWalker : SyntaxWalker + public class CustomSyntaxWalker(List list) + : SyntaxWalker(new() { VisitNames = true, }) { - private readonly List _list; - - public CustomSyntaxWalker(List list) - : base(new() { VisitNames = true, }) + protected override ISyntaxVisitorAction Enter(NameNode node, object context) { - _list = list; - } - - protected override ISyntaxVisitorAction Enter(NameNode node, ISyntaxVisitorContext context) - { - _list.Add(node.Value); + list.Add(node.Value); return DefaultAction; } } - public class CustomGenericSyntaxWalker : SyntaxWalker + public class CustomGenericSyntaxWalker() + : SyntaxWalker(new SyntaxVisitorOptions { VisitNames = true, }) { - public CustomGenericSyntaxWalker() - : base(new() { VisitNames = true, }) - { - } - protected override ISyntaxVisitorAction Enter(NameNode node, CustomContext context) { context.List.Add(node.Value); @@ -106,13 +95,8 @@ protected override ISyntaxVisitorAction Enter(NameNode node, CustomContext conte } } - public class CustomContext : ISyntaxVisitorContext + public class CustomContext(List list) { - public CustomContext(List list) - { - List = list; - } - - public List List { get; } + public List List { get; } = list; } } diff --git a/src/HotChocolate/Skimmed/src/Skimmed/Refactor.cs b/src/HotChocolate/Skimmed/src/Skimmed/Refactor.cs index 5d7b2416683..d87b153b361 100644 --- a/src/HotChocolate/Skimmed/src/Skimmed/Refactor.cs +++ b/src/HotChocolate/Skimmed/src/Skimmed/Refactor.cs @@ -402,19 +402,13 @@ private sealed class ValueRewriter : SyntaxRewriter } } - private class RewriterContext : ISyntaxVisitorContext + private class RewriterContext(string value) { - public RewriterContext(string value) - { - Value = value; - } - - public string Value { get; } + public string Value { get; } = value; } } - private sealed class RemoveInputFieldRewriter - : SchemaVisitor<(InputObjectType Type, InputField Field)> + private sealed class RemoveInputFieldRewriter : SchemaVisitor<(InputObjectType Type, InputField Field)> { public override void VisitInputField(InputField field, (InputObjectType Type, InputField Field) context) { @@ -457,14 +451,9 @@ private sealed class ValueRewriter : SyntaxRewriter } } - private class RewriterContext : ISyntaxVisitorContext + private class RewriterContext(string name) { - public RewriterContext(string name) - { - Name = name; - } - - public string Name { get; } + public string Name { get; } = name; } } } \ No newline at end of file diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/EntityIdRewriter.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/EntityIdRewriter.cs index 0c86f6bb2f6..f19129e9370 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/EntityIdRewriter.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/EntityIdRewriter.cs @@ -115,14 +115,9 @@ public static DocumentNode Rewrite(DocumentNode document, ISchema schema) return rewriter.RewriteDocument(document, new Context(schema))!; } - public class Context : ISyntaxVisitorContext + public class Context(ISchema schema) { - public Context(ISchema schema) - { - Schema = schema; - } - - public ISchema Schema { get; } + public ISchema Schema { get; } = schema; public Stack Types { get; } = new(); diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/ExtractOperationContext.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/ExtractOperationContext.cs index 87f6a490bfe..e8b66b8f26e 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/ExtractOperationContext.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/ExtractOperationContext.cs @@ -6,7 +6,7 @@ namespace StrawberryShake.CodeGeneration.Utilities; -internal sealed class ExtractOperationContext : ISyntaxVisitorContext +internal sealed class ExtractOperationContext { private readonly DocumentNode _document; private int _index = -1; diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/FragmentRewriter.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/FragmentRewriter.cs index 788450604c9..99bfb2a3221 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/FragmentRewriter.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/FragmentRewriter.cs @@ -56,7 +56,7 @@ public static DocumentNode Rewrite(DocumentNode document) return rewriter.RewriteDocument(document, context)!; } - internal sealed class Context : ISyntaxVisitorContext + internal sealed class Context { public HashSet Deferred { get; } = []; } diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/RemoveClientDirectivesRewriter.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/RemoveClientDirectivesRewriter.cs index 4be6ce22398..44ca144d254 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/RemoveClientDirectivesRewriter.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/RemoveClientDirectivesRewriter.cs @@ -5,11 +5,11 @@ namespace StrawberryShake.CodeGeneration.Utilities; -internal sealed class RemoveClientDirectivesRewriter : SyntaxRewriter +internal sealed class RemoveClientDirectivesRewriter : SyntaxRewriter { private const string _returns = "returns"; - protected override FieldNode RewriteField(FieldNode node, ISyntaxVisitorContext context) + protected override FieldNode RewriteField(FieldNode node, object? context) { var current = node; diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/RemovedUnusedFragmentRewriter.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/RemovedUnusedFragmentRewriter.cs index 6cbcd14ba06..809fa340702 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/RemovedUnusedFragmentRewriter.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/RemovedUnusedFragmentRewriter.cs @@ -46,7 +46,7 @@ public static DocumentNode Rewrite(DocumentNode document) return rewriter.RewriteDocument(document, context); } - internal sealed class Context : ISyntaxVisitorContext + internal sealed class Context { public HashSet Used { get; } = []; } diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/TypeNameQueryRewriter.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/TypeNameQueryRewriter.cs index 8fb0fe27837..8222755ce49 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/TypeNameQueryRewriter.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/Utilities/TypeNameQueryRewriter.cs @@ -87,7 +87,7 @@ internal sealed class TypeNameQueryRewriter : SyntaxRewriter Nodes { get; } = new(); } From f1f850e3ede9d640be702509753e53d8f084ccf1 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 6 Mar 2024 12:52:43 +0100 Subject: [PATCH 2/5] More source gen work --- global.json | 3 +- .../Inspectors/DataLoaderDefaultsInfo.cs | 28 +- .../Inspectors/RequestMiddlewareInspector.cs | 23 +- .../src/Analyzers/ConfigurationGenerator.cs | 261 ++++++++++++++++++ .../src/Analyzers/Helpers/CodeWriter.cs | 156 +++++++++++ .../Analyzers/Helpers/CodeWriterExtensions.cs | 46 +++ .../Analyzers/Helpers/StringBuilderPool.cs | 20 ++ .../src/Analyzers/Helpers/SymbolExtensions.cs | 9 + .../Fusion/src/Analyzers/ProjectClass.cs | 36 ++- .../Analyzers/Properties/launchSettings.json | 9 + .../src/Analyzers/TypeModuleGenerator.cs | 42 --- .../src/Analyzers/WellKnownTypeNames.cs | 2 +- .../Pipeline/OperationExecutionMiddleware.cs | 2 - .../FusionGatewayConfiguration.cs | 22 -- ...HotChocolate.Fusion.Analyzers.Tests.csproj | 1 + .../test/Analyzers.Tests/IProjectMetadata.cs | 9 - .../Analyzers.Tests/IResourceAnnotation.cs | 5 - .../Fusion/test/Analyzers.Tests/Program.cs | 110 ++++++++ .../Fusion/test/Analyzers.Tests/T.cs | 26 +- 19 files changed, 687 insertions(+), 123 deletions(-) create mode 100644 src/HotChocolate/Fusion/src/Analyzers/ConfigurationGenerator.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriter.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriterExtensions.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/Helpers/StringBuilderPool.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/Helpers/SymbolExtensions.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/Properties/launchSettings.json delete mode 100644 src/HotChocolate/Fusion/src/Analyzers/TypeModuleGenerator.cs delete mode 100644 src/HotChocolate/Fusion/test/Analyzers.Tests/FusionGatewayConfiguration.cs delete mode 100644 src/HotChocolate/Fusion/test/Analyzers.Tests/IProjectMetadata.cs delete mode 100644 src/HotChocolate/Fusion/test/Analyzers.Tests/IResourceAnnotation.cs create mode 100644 src/HotChocolate/Fusion/test/Analyzers.Tests/Program.cs diff --git a/global.json b/global.json index e71d6cc5772..5ce84955149 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,5 @@ { "sdk": { - "version": "8.0.100", - "rollForward": "major" + "version": "8.0.100" } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderDefaultsInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderDefaultsInfo.cs index 19323617545..64e2b8a1fa8 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderDefaultsInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderDefaultsInfo.cs @@ -1,26 +1,20 @@ namespace HotChocolate.Types.Analyzers.Inspectors; -public sealed class DataLoaderDefaultsInfo : ISyntaxInfo, IEquatable +public sealed class DataLoaderDefaultsInfo( + bool? scoped, + bool? isPublic, + bool? isInterfacePublic, + bool registerServices) + : ISyntaxInfo + , IEquatable { - public DataLoaderDefaultsInfo( - bool? scoped, - bool? isPublic, - bool? isInterfacePublic, - bool registerServices) - { - Scoped = scoped; - IsPublic = isPublic; - IsInterfacePublic = isInterfacePublic; - RegisterServices = registerServices; - } - - public bool? Scoped { get; } + public bool? Scoped { get; } = scoped; - public bool? IsPublic { get; } + public bool? IsPublic { get; } = isPublic; - public bool? IsInterfacePublic { get; } + public bool? IsInterfacePublic { get; } = isInterfacePublic; - public bool RegisterServices { get; } + public bool RegisterServices { get; } = registerServices; public bool Equals(DataLoaderDefaultsInfo? other) { diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/RequestMiddlewareInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/RequestMiddlewareInspector.cs index 8a0dcd6a26c..bba979d86f0 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/RequestMiddlewareInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/RequestMiddlewareInspector.cs @@ -89,7 +89,7 @@ ctor is not var parameterTypeName = parameter.Type.ToFullyQualified(); if (parameterTypeName.Equals("global::HotChocolate.Schema") || - parameterTypeName.Equals("global::HotChocolate.!Schema")) + parameterTypeName.Equals("global::HotChocolate.ISchema")) { kind = RequestMiddlewareParameterKind.Schema; } @@ -108,26 +108,7 @@ ctor is not invokeParameters.Add(new RequestMiddlewareParameterInfo(kind, parameterTypeName)); } - - - /* - public static IRequestExecutorBuilder UseRequest( - this IRequestExecutorBuilder builder) - where TMiddleware : class - - [InterceptsLocation(@"C:\testapp\Program.cs", line: 4, column: 5)] - public static RouteHandlerBuilder InterceptMapGet( // 👈 The interceptor must - this IEndpointRouteBuilder endpoints, // have the same signature - string pattern, // as the method being - Delegate handler) // intercepted - { - Console.WriteLine($"Intercepted '{pattern}'" ); - - return endpoints.MapGet(pattern, handler); - } - */ - - + syntaxInfo = new RequestMiddlewareInfo( middlewareType.Name, middlewareType.ToFullyQualified(), diff --git a/src/HotChocolate/Fusion/src/Analyzers/ConfigurationGenerator.cs b/src/HotChocolate/Fusion/src/Analyzers/ConfigurationGenerator.cs new file mode 100644 index 00000000000..dc6a9342037 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/ConfigurationGenerator.cs @@ -0,0 +1,261 @@ +using System.Collections.Immutable; +using System.Runtime.InteropServices; +using System.Text; +using HotChocolate.Fusion.Analyzers.Properties; +using HotChocolate.Types.Analyzers.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace HotChocolate.Types.Analyzers; + +[Generator] +public class ConfigurationGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var modulesAndTypes = + context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (s, _) => IsRelevant(s), + transform: TryGetProjectClass) + .Where(static t => t is not null); + + var valueProvider = context.CompilationProvider.Combine(modulesAndTypes.Collect()); + + context.RegisterSourceOutput( + valueProvider, + static (context, source) => Execute(context, source.Left, source.Right!)); + } + + private static bool IsRelevant(SyntaxNode node) + { + if (node is ClassDeclarationSyntax { BaseList.Types.Count: > 0, TypeParameterList: null, }) + { + return true; + } + + if (node is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax memberAccess, + }) + { + if (memberAccess.Name is GenericNameSyntax genericName && + genericName.TypeArgumentList.Arguments.Count == 1) + { + if (genericName.Identifier.ValueText.Equals("AddFusionGateway", StringComparison.Ordinal)) + { + return true; + } + + if (genericName.Identifier.ValueText.Equals("AddProject", StringComparison.Ordinal)) + { + var current = node; + + while (current.Parent is InvocationExpressionSyntax or MemberAccessExpressionSyntax) + { + current = current.Parent; + } + + if (current.Parent is EqualsValueClauseSyntax) + { + return true; + } + } + } + + if (memberAccess.Name is not GenericNameSyntax && + memberAccess.Name.Identifier.ValueText.Equals("WithSubgraph", StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private static ISyntaxInfo? TryGetProjectClass(GeneratorSyntaxContext context, CancellationToken cancellationToken) + { + if (context.Node is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name: GenericNameSyntax + { + Identifier.ValueText: { } name, + TypeArgumentList: { Arguments.Count: 1, } args, + }, + }, + } invocation) + { + if (name.Equals("AddProject") && + context.SemanticModel.GetTypeInfo(args.Arguments[0]).Type is INamedTypeSymbol subgraphType && + subgraphType.AllInterfaces.Any(t => t.ToFullyQualified().Equals(WellKnownTypeNames.ProjectMetadata))) + { + SyntaxNode current = invocation; + + while (current.Parent is InvocationExpressionSyntax or MemberAccessExpressionSyntax) + { + current = current.Parent; + } + + if (current.Parent is EqualsValueClauseSyntax && + current.Parent.Parent is VariableDeclaratorSyntax variable) + { + return new ProjectClass( + subgraphType.Name, + subgraphType.ToFullyQualified(), + variable.Identifier.ValueText); + } + } + } + + if (context.Node is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name.Identifier.ValueText: "WithSubgraph", + }, + ArgumentList.Arguments.Count: 1, + } subgraphInvocation) + { + SyntaxNode current = subgraphInvocation; + + while (current is InvocationExpressionSyntax or MemberAccessExpressionSyntax) + { + if (current is InvocationExpressionSyntax invocationSyntax) + { + current = invocationSyntax.Expression; + } + else if (current is MemberAccessExpressionSyntax memberAccessSyntax) + { + current = memberAccessSyntax.Expression; + } + + if (current is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name: GenericNameSyntax + { + Identifier.ValueText: "AddFusionGateway", + TypeArgumentList.Arguments: { Count: 1, } fusionArgs, + }, + }, + } parentInvocation && + context.SemanticModel.GetTypeInfo(fusionArgs[0]).Type is INamedTypeSymbol gatewayType) + { + var argument = subgraphInvocation.ArgumentList.Arguments[0]; + return new GatewayClass( + gatewayType.Name, + gatewayType.ToFullyQualified(), + GetVariableName(argument)); + } + } + } + + + return null; + } + + private static string GetVariableName(ArgumentSyntax argument) + { + SyntaxNode current = argument.Expression; + + while (current is InvocationExpressionSyntax or MemberAccessExpressionSyntax or IdentifierNameSyntax) + { + if (current is InvocationExpressionSyntax invocation) + { + current = invocation.Expression; + } + + if (current is MemberAccessExpressionSyntax memberAccess) + { + current = memberAccess.Expression; + } + + if (current is IdentifierNameSyntax identifier) + { + return identifier.Identifier.ValueText; + } + } + + throw new InvalidOperationException(); + } + + private static void Execute( + SourceProductionContext context, + Compilation compilation, + ImmutableArray syntaxInfos) + { + if (syntaxInfos.Length == 0) + { + return; + } + + var projects = new Dictionary(); + foreach (var project in syntaxInfos.OfType()) + { + projects[project.VariableName] = project; + } + + var gateways = new List(); + foreach (var gatewayGroup in syntaxInfos.OfType().GroupBy(t => t.Name)) + { + var gateway = new GatewayInfo(gatewayGroup.Key, gatewayGroup.First().TypeName); + + foreach (var projectLink in gatewayGroup) + { + if (projects.TryGetValue(projectLink.VariableName, out var project)) + { + gateway.Projects.Add(new ProjectInfo(project.Name, project.TypeName)); + gateways.Add(gateway); + } + } + + } + + if (gateways.Count == 0) + { + return; + } + + var code = StringBuilderPool.Get(); + using var writer = new CodeWriter(code); + writer.WriteFileHeader(); + writer.WriteLine(); + writer.Write(Resources.CliCode); + writer.WriteLine(); + writer.WriteLine(); + writer.WriteIndentedLine("file static class FusionGatewayConfigurationFiles"); + writer.WriteIndentedLine("{"); + + using (writer.IncreaseIndent()) + { + writer.WriteIndentedLine("public static readonly string[] SubgraphProjects ="); + writer.WriteIndentedLine("["); + + using (writer.IncreaseIndent()) + { + foreach (var project in gateways[0].Projects) + { + writer.WriteIndentedLine("new {0}().ProjectPath,", project.TypeName); + } + } + + writer.WriteIndentedLine("];"); + writer.WriteLine(); + writer.WriteIndentedLine("public static string GatewayProject"); + + using (writer.IncreaseIndent()) + { + writer.WriteIndentedLine("=> new {0}().ProjectPath;", gateways[0].TypeName); + } + } + + + writer.WriteIndentedLine("}"); + + context.AddSource("FusionGatewayConfiguration.g.cs", code.ToString()); + StringBuilderPool.Return(code); + } +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriter.cs b/src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriter.cs new file mode 100644 index 00000000000..3bef6695746 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriter.cs @@ -0,0 +1,156 @@ +using System.Text; + +namespace HotChocolate.Types.Analyzers.Helpers; + +public class CodeWriter : TextWriter +{ + private readonly TextWriter _writer; + private readonly bool _disposeWriter; + private bool _disposed; + private int _indent; + + public CodeWriter(TextWriter writer) + { + _writer = writer; + _disposeWriter = false; + } + + public CodeWriter(StringBuilder text) + { + _writer = new StringWriter(text); + _disposeWriter = true; + } + + public override Encoding Encoding { get; } = Encoding.UTF8; + + public static string Indent { get; } = new(' ', 4); + + public override void Write(char value) => + _writer.Write(value); + + public void WriteStringValue(string value) + { + Write('"'); + Write(value); + Write('"'); + } + + public void WriteIndent() + { + if (_indent > 0) + { + var spaces = _indent * 4; + for (var i = 0; i < spaces; i++) + { + Write(' '); + } + } + } + + public string GetIndentString() + { + if (_indent > 0) + { + return new string(' ', _indent * 4); + } + return string.Empty; + } + + public void WriteIndentedLine(string format, params object?[] args) + { + WriteIndent(); + + if (args.Length == 0) + { + Write(format); + } + else + { + Write(format, args); + } + + WriteLine(); + } + + public void WriteIndented(string format, params object?[] args) + { + WriteIndent(); + + if (args.Length == 0) + { + Write(format); + } + else + { + Write(format, args); + } + } + + public void WriteSpace() => Write(' '); + + public IDisposable IncreaseIndent() + { + _indent++; + return new Block(DecreaseIndent); + } + + public void DecreaseIndent() + { + if (_indent > 0) + { + _indent--; + } + } + + public IDisposable WriteBraces() + { + WriteLeftBrace(); + WriteLine(); + +#pragma warning disable CA2000 + var indent = IncreaseIndent(); +#pragma warning restore CA2000 + + return new Block(() => + { + WriteLine(); + indent.Dispose(); + WriteIndent(); + WriteRightBrace(); + }); + } + + public void WriteLeftBrace() => Write('{'); + + public void WriteRightBrace() => Write('}'); + + public override void Flush() + { + base.Flush(); + _writer.Flush(); + } + + protected override void Dispose(bool disposing) + { + if (!_disposed && _disposeWriter) + { + if (disposing) + { + _writer.Dispose(); + } + _disposed = true; + } + } + + private sealed class Block : IDisposable + { + private readonly Action _decrease; + + public Block(Action close) + { + _decrease = close; + } + + public void Dispose() => _decrease(); + } +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriterExtensions.cs b/src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriterExtensions.cs new file mode 100644 index 00000000000..dfa0815ce57 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriterExtensions.cs @@ -0,0 +1,46 @@ +namespace HotChocolate.Types.Analyzers.Helpers; + +public static class CodeWriterExtensions +{ + public static void WriteGeneratedAttribute(this CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + +#if DEBUG + writer.WriteIndentedLine( + "[global::System.CodeDom.Compiler.GeneratedCode(" + + "\"HotChocolate\", \"11.0.0\")]"); +#else + var version = typeof(CodeWriter).Assembly.GetName().Version!.ToString(); + writer.WriteIndentedLine( + "[global::System.CodeDom.Compiler.GeneratedCode(" + + $"\"HotChocolate\", \"{version}\")]"); +#endif + } + + public static void WriteFileHeader(this CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteIndentedLine("// "); + writer.WriteLine(); + writer.WriteIndentedLine("#nullable enable"); + writer.WriteLine(); + writer.WriteIndentedLine("using System;"); + writer.WriteIndentedLine("using System.Runtime.CompilerServices;"); + writer.WriteIndentedLine("using HotChocolate.Execution.Configuration;"); + } + + public static CodeWriter WriteComment(this CodeWriter writer, string comment) + { + writer.Write("// "); + writer.WriteLine(comment); + return writer; + } +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/Helpers/StringBuilderPool.cs b/src/HotChocolate/Fusion/src/Analyzers/Helpers/StringBuilderPool.cs new file mode 100644 index 00000000000..2f8fe5b7e15 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/Helpers/StringBuilderPool.cs @@ -0,0 +1,20 @@ +using System.Text; + +namespace HotChocolate.Types.Analyzers.Helpers; + +public static class StringBuilderPool +{ + private static StringBuilder? _stringBuilder; + + public static StringBuilder Get() + { + var stringBuilder = Interlocked.Exchange(ref _stringBuilder, null); + return stringBuilder ?? new StringBuilder(); + } + + public static void Return(StringBuilder stringBuilder) + { + stringBuilder.Clear(); + Interlocked.CompareExchange(ref _stringBuilder, stringBuilder, null); + } +} diff --git a/src/HotChocolate/Fusion/src/Analyzers/Helpers/SymbolExtensions.cs b/src/HotChocolate/Fusion/src/Analyzers/Helpers/SymbolExtensions.cs new file mode 100644 index 00000000000..0473d15b7cc --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/Helpers/SymbolExtensions.cs @@ -0,0 +1,9 @@ +using Microsoft.CodeAnalysis; + +namespace HotChocolate.Types.Analyzers.Helpers; + +public static class SymbolExtensions +{ + public static string ToFullyQualified(this ITypeSymbol typeSymbol) + => typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); +} diff --git a/src/HotChocolate/Fusion/src/Analyzers/ProjectClass.cs b/src/HotChocolate/Fusion/src/Analyzers/ProjectClass.cs index 14fae287b0d..924267c3021 100644 --- a/src/HotChocolate/Fusion/src/Analyzers/ProjectClass.cs +++ b/src/HotChocolate/Fusion/src/Analyzers/ProjectClass.cs @@ -1,6 +1,40 @@ namespace HotChocolate.Types.Analyzers; -public sealed class ProjectClass(string name) +public sealed class ProjectClass(string name, string typeName, string variableName) : ISyntaxInfo { public string Name { get; } = name; + + public string TypeName { get; } = typeName; + + public string VariableName { get; } = variableName; +} + +public sealed class GatewayClass(string name, string typeName, string variableName) : ISyntaxInfo +{ + public string Name { get; } = name; + + public string TypeName { get; } = typeName; + + public string VariableName { get; } = variableName; +} + +public class GatewayInfo(string name, string typeName) +{ + public string Name { get; } = name; + + public string TypeName { get; } = typeName; + + public List Projects { get; } = new(); +} + +public class ProjectInfo(string name, string typeName) +{ + public string Name { get; } = name; + + public string TypeName { get; } = typeName; +} + +public interface ISyntaxInfo +{ + string Name { get; } } \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/Properties/launchSettings.json b/src/HotChocolate/Fusion/src/Analyzers/Properties/launchSettings.json new file mode 100644 index 00000000000..8af0cdb9a16 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Generators": { + "commandName": "DebugRoslynComponent", + "targetProject": "../../test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj" + } + } +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/TypeModuleGenerator.cs b/src/HotChocolate/Fusion/src/Analyzers/TypeModuleGenerator.cs deleted file mode 100644 index 2493b91dac0..00000000000 --- a/src/HotChocolate/Fusion/src/Analyzers/TypeModuleGenerator.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Immutable; -using HotChocolate.Fusion.Analyzers.Properties; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace HotChocolate.Types.Analyzers; - -[Generator] -public class TypeModuleGenerator : IIncrementalGenerator -{ - - public void Initialize(IncrementalGeneratorInitializationContext context) - { - var modulesAndTypes = - context.SyntaxProvider - .CreateSyntaxProvider( - predicate: static (s, _) => IsRelevant(s), - transform: TryGetProjectClass) - .Where(static t => t is not null); - - var valueProvider = context.CompilationProvider.Combine(modulesAndTypes.Collect()); - - context.RegisterSourceOutput( - valueProvider, - static (context, source) => Execute(context, source.Left, source.Right!)); - - } - - private static bool IsRelevant(SyntaxNode node) - => node is ClassDeclarationSyntax { BaseList.Types.Count: > 0, }; - - private ProjectClass? TryGetProjectClass(GeneratorSyntaxContext context, CancellationToken cancellationToken) - => new ProjectClass("abc"); - - private static void Execute( - SourceProductionContext context, - Compilation compilation, - ImmutableArray syntaxInfos) - { - context.AddSource("FusionGatewayConfiguration.g.cs", Resources.CliCode); - } -} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/WellKnownTypeNames.cs b/src/HotChocolate/Fusion/src/Analyzers/WellKnownTypeNames.cs index 559cecee435..69bbb2e7267 100644 --- a/src/HotChocolate/Fusion/src/Analyzers/WellKnownTypeNames.cs +++ b/src/HotChocolate/Fusion/src/Analyzers/WellKnownTypeNames.cs @@ -2,5 +2,5 @@ namespace HotChocolate.Types.Analyzers; public static class WellKnownTypeNames { - public const string ProjectMetadata = "Aspire.Hosting.IProjectMetadata"; + public const string ProjectMetadata = "global::Aspire.Hosting.IProjectMetadata"; } \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Core/Execution/Pipeline/OperationExecutionMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Execution/Pipeline/OperationExecutionMiddleware.cs index 8d704a9d5df..eb51be262b8 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/Pipeline/OperationExecutionMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Execution/Pipeline/OperationExecutionMiddleware.cs @@ -1,7 +1,5 @@ using HotChocolate.Execution; -using HotChocolate.Execution.Caching; using HotChocolate.Execution.DependencyInjection; -using HotChocolate.Execution.Instrumentation; using HotChocolate.Execution.Processing; using HotChocolate.Fetching; using HotChocolate.Fusion.Clients; diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/FusionGatewayConfiguration.cs b/src/HotChocolate/Fusion/test/Analyzers.Tests/FusionGatewayConfiguration.cs deleted file mode 100644 index 3430213f9ec..00000000000 --- a/src/HotChocolate/Fusion/test/Analyzers.Tests/FusionGatewayConfiguration.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Buffers; -using System.Text.Json; -using System.Text.Json.Serialization; -using HotChocolate.Fusion; -using HotChocolate.Fusion.Composition; -using HotChocolate.Fusion.Composition.Features; -using HotChocolate.Language; -using HotChocolate.Language.Visitors; -using HotChocolate.Skimmed.Serialization; - -namespace Projects; - -public static class FusionGatewayConfigurationFiles -{ - public static readonly string[] SubgraphProjects = - [ - """/Users/michael/local/webshop-workshop/src/Basket.API/eShop.Basket.API.csproj""", - ]; - - public const string GatewayProject = - """/Users/michael/local/webshop-workshop/src/Gateway/eShop.Gateway.csproj"""; -} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj b/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj index 549c96c3dd9..0b4499b2a54 100644 --- a/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj +++ b/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/IProjectMetadata.cs b/src/HotChocolate/Fusion/test/Analyzers.Tests/IProjectMetadata.cs deleted file mode 100644 index 8af381ba071..00000000000 --- a/src/HotChocolate/Fusion/test/Analyzers.Tests/IProjectMetadata.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Aspire.Hosting; - -public interface IProjectMetadata : IResourceAnnotation -{ - /// - /// Gets the fully-qualified path to the project. - /// - public string ProjectPath { get; } -} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/IResourceAnnotation.cs b/src/HotChocolate/Fusion/test/Analyzers.Tests/IResourceAnnotation.cs deleted file mode 100644 index 62db54a0cd8..00000000000 --- a/src/HotChocolate/Fusion/test/Analyzers.Tests/IResourceAnnotation.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Aspire.Hosting; - -public interface IResourceAnnotation -{ -} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/Program.cs b/src/HotChocolate/Fusion/test/Analyzers.Tests/Program.cs new file mode 100644 index 00000000000..0b8daa14a9b --- /dev/null +++ b/src/HotChocolate/Fusion/test/Analyzers.Tests/Program.cs @@ -0,0 +1,110 @@ +namespace HotChocolate.Fusion.Analyzers.Tests; + +public class Program +{ + public static void Foo(string[] args) + { + var builder = DistributedApplication.CreateBuilder(args); + + // resources + var redis = builder.AddRedisContainer("redis"); + + var postgres = builder + .AddPostgresContainer("postgres") + .WithPgAdmin() + .WithAnnotation( + new ContainerImageAnnotation + { + Image = "ankane/pgvector", + Tag = "latest", + }); + + var rabbitMq = builder + .AddRabbitMQContainer("event-bus") + .WithEndpoint(containerPort: 58812, name: "management"); + + var catalogDb = postgres.AddDatabase("CatalogDB"); + var identityDb = postgres.AddDatabase("IdentityDB"); + var orderingDb = postgres.AddDatabase("OrderingDB"); + + // APIs + var identityApi = builder + .AddProject("identity-api") + .WithReference(identityDb) + .WithLaunchProfile("https"); + + var identityHttpsEndpoint = identityApi.GetEndpoint("https"); + + var catalogApi = builder + .AddProject("catalog-api") + .WithReference(catalogDb) + .WithEnvironment("Identity__Url", identityHttpsEndpoint); + + var basketApi = builder + .AddProject("basket-api") + .WithReference(redis) + .WithEnvironment("Identity__Url", identityHttpsEndpoint) + .WithReference(catalogApi.GetEndpoint("http")); + + var orderingApi = builder + .AddProject("ordering-api") + .WithReference(rabbitMq) + .WithReference(orderingDb) + .WithReference(catalogApi.GetEndpoint("http")) + .WithEnvironment("Identity__Url", identityHttpsEndpoint); + + // Fusion + builder + .AddFusionGateway("gateway") + .WithSubgraph(basketApi) + .WithSubgraph(identityApi.GetEndpoint("http")) + .WithSubgraph(catalogApi) + .WithSubgraph(orderingApi) + .WithEnvironment("Identity__Url", identityHttpsEndpoint); + + builder.Build().Run(); + } +} + +file static class FusionExtensions +{ + public static IResourceBuilder AddFusionGateway( + this IDistributedApplicationBuilder builder, + string name) + where TProject : IProjectMetadata, new() + => new FusionGatewayResourceBuilder(builder.AddProject(name)); + + public static IResourceBuilder WithSubgraph( + this IResourceBuilder builder, + IResourceBuilder subgraphProject) + => builder.WithReference(subgraphProject.GetEndpoint("http")); + + public static IResourceBuilder WithSubgraph( + this IResourceBuilder builder, + EndpointReference subgraphEndpoint) + => builder.WithReference(subgraphEndpoint); +} + +file class FusionGatewayResource(ProjectResource projectResource) + : Resource(projectResource.Name) + , IResourceWithEnvironment + , IResourceWithServiceDiscovery +{ + public ProjectResource ProjectResource { get; } = projectResource; +} + +file class FusionGatewayResourceBuilder( + IResourceBuilder projectResourceBuilder) + : IResourceBuilder +{ + public IResourceBuilder WithAnnotation(TAnnotation annotation) + where TAnnotation : IResourceAnnotation + { + projectResourceBuilder.WithAnnotation(annotation); + return this; + } + + public IDistributedApplicationBuilder ApplicationBuilder => projectResourceBuilder.ApplicationBuilder; + + public FusionGatewayResource Resource { get; } = new(projectResourceBuilder.Resource); +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/T.cs b/src/HotChocolate/Fusion/test/Analyzers.Tests/T.cs index 753b4020d93..210a4e76fc5 100644 --- a/src/HotChocolate/Fusion/test/Analyzers.Tests/T.cs +++ b/src/HotChocolate/Fusion/test/Analyzers.Tests/T.cs @@ -4,4 +4,28 @@ namespace Projects; public class eShop_Basket_API : global::Aspire.Hosting.IProjectMetadata { public string ProjectPath => """/Users/michael/local/webshop-workshop/src/Basket.API/eShop.Basket.API.csproj"""; -} \ No newline at end of file +} + +[global::System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, ProjectPath = {ProjectPath}")] +public class eShop_Identity_API : global::Aspire.Hosting.IProjectMetadata +{ + public string ProjectPath => """/Users/michael/local/webshop-workshop/src/Identity.API/eShop.Identity.API.csproj"""; +} + +[global::System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, ProjectPath = {ProjectPath}")] +public class eShop_Catalog_API : global::Aspire.Hosting.IProjectMetadata +{ + public string ProjectPath => """/Users/michael/local/webshop-workshop/src/Catalog.API/eShop.Catalog.API.csproj"""; +} + +[global::System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, ProjectPath = {ProjectPath}")] +public class eShop_Ordering_API : global::Aspire.Hosting.IProjectMetadata +{ + public string ProjectPath => """/Users/michael/local/webshop-workshop/src/Ordering.API/eShop.Ordering.API.csproj"""; +} + +[global::System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, ProjectPath = {ProjectPath}")] +public class eShop_Gateway : global::Aspire.Hosting.IProjectMetadata +{ + public string ProjectPath => """/Users/michael/local/webshop-workshop/src/Gateway/eShop.Gateway.csproj"""; +} From 2342202ba0f0a065fd7731b6d925eb7dd7523949 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 6 Mar 2024 20:05:46 +0100 Subject: [PATCH 3/5] edits --- .../Core/src/Abstractions/SchemaFactory.cs | 11 + .../src/Analyzers/ConfigurationGenerator.cs | 79 +- .../HotChocolate.Fusion.Analyzers.csproj | 3 +- .../HotChocolate.Fusion.Analyzers.targets | 5 + .../src/Analyzers/Models/GatewayClass.cs | 10 + .../src/Analyzers/Models/GatewayInfo.cs | 10 + .../src/Analyzers/Models/ISyntaxInfo.cs | 6 + .../src/Analyzers/Models/SubgraphClass.cs | 10 + .../src/Analyzers/Models/SubgraphInfo.cs | 8 + .../Fusion/src/Analyzers/ProjectClass.cs | 40 - .../src/Analyzers/Properties/Resources.resx | 748 +++++++++++------- ...HotChocolate.Fusion.Analyzers.Tests.csproj | 59 +- .../Fusion/test/Analyzers.Tests/Program.cs | 12 +- .../Analyzers.Tests/{T.cs => Projects.cs} | 6 + .../Fusion/test/Analyzers.Tests/Test.cs | 618 +++++++++++++++ 15 files changed, 1263 insertions(+), 362 deletions(-) create mode 100644 src/HotChocolate/Core/src/Abstractions/SchemaFactory.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.targets create mode 100644 src/HotChocolate/Fusion/src/Analyzers/Models/GatewayClass.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/Models/GatewayInfo.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/Models/ISyntaxInfo.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/Models/SubgraphClass.cs create mode 100644 src/HotChocolate/Fusion/src/Analyzers/Models/SubgraphInfo.cs delete mode 100644 src/HotChocolate/Fusion/src/Analyzers/ProjectClass.cs rename src/HotChocolate/Fusion/test/Analyzers.Tests/{T.cs => Projects.cs} (83%) create mode 100644 src/HotChocolate/Fusion/test/Analyzers.Tests/Test.cs diff --git a/src/HotChocolate/Core/src/Abstractions/SchemaFactory.cs b/src/HotChocolate/Core/src/Abstractions/SchemaFactory.cs new file mode 100644 index 00000000000..ab08836968c --- /dev/null +++ b/src/HotChocolate/Core/src/Abstractions/SchemaFactory.cs @@ -0,0 +1,11 @@ +using HotChocolate.Language; + +namespace HotChocolate; + +/// +/// Implement this interface to enable design-time services to create the GraphQL type system. +/// +public interface IDesignTimeSchemaDocumentFactory +{ + DocumentNode CreateSchemaDocument(string[] args); +} diff --git a/src/HotChocolate/Fusion/src/Analyzers/ConfigurationGenerator.cs b/src/HotChocolate/Fusion/src/Analyzers/ConfigurationGenerator.cs index dc6a9342037..d36cd30abf6 100644 --- a/src/HotChocolate/Fusion/src/Analyzers/ConfigurationGenerator.cs +++ b/src/HotChocolate/Fusion/src/Analyzers/ConfigurationGenerator.cs @@ -1,6 +1,4 @@ using System.Collections.Immutable; -using System.Runtime.InteropServices; -using System.Text; using HotChocolate.Fusion.Analyzers.Properties; using HotChocolate.Types.Analyzers.Helpers; using Microsoft.CodeAnalysis; @@ -101,7 +99,7 @@ private static bool IsRelevant(SyntaxNode node) if (current.Parent is EqualsValueClauseSyntax && current.Parent.Parent is VariableDeclaratorSyntax variable) { - return new ProjectClass( + return new SubgraphClass( subgraphType.Name, subgraphType.ToFullyQualified(), variable.Identifier.ValueText); @@ -192,8 +190,8 @@ private static void Execute( return; } - var projects = new Dictionary(); - foreach (var project in syntaxInfos.OfType()) + var projects = new Dictionary(); + foreach (var project in syntaxInfos.OfType()) { projects[project.VariableName] = project; } @@ -207,11 +205,11 @@ private static void Execute( { if (projects.TryGetValue(projectLink.VariableName, out var project)) { - gateway.Projects.Add(new ProjectInfo(project.Name, project.TypeName)); + gateway.Subgraphs.Add(new SubgraphInfo(project.Name, project.TypeName)); gateways.Add(gateway); } } - + } if (gateways.Count == 0) @@ -226,36 +224,73 @@ private static void Execute( writer.Write(Resources.CliCode); writer.WriteLine(); writer.WriteLine(); - writer.WriteIndentedLine("file static class FusionGatewayConfigurationFiles"); + writer.WriteIndentedLine("namespace HotChocolate.Fusion.Composition.Tooling"); writer.WriteIndentedLine("{"); using (writer.IncreaseIndent()) { - writer.WriteIndentedLine("public static readonly string[] SubgraphProjects ="); - writer.WriteIndentedLine("["); + writer.WriteIndentedLine("file class GatewayList : List"); + writer.WriteIndentedLine("{"); using (writer.IncreaseIndent()) { - foreach (var project in gateways[0].Projects) + writer.WriteIndentedLine("public GatewayList()"); + + using (writer.IncreaseIndent()) { - writer.WriteIndentedLine("new {0}().ProjectPath,", project.TypeName); - } - } + writer.WriteIndentedLine(": base("); + writer.WriteIndentedLine("["); - writer.WriteIndentedLine("];"); - writer.WriteLine(); - writer.WriteIndentedLine("public static string GatewayProject"); + using (writer.IncreaseIndent()) + { + foreach (var gateway in gateways) + { + writer.WriteIndentedLine("GatewayInfo.Create<{0}>(", gateway.TypeName); - using (writer.IncreaseIndent()) - { - writer.WriteIndentedLine("=> new {0}().ProjectPath;", gateways[0].TypeName); + using (writer.IncreaseIndent()) + { + writer.WriteIndentedLine("\"{}\",", gateway.Name); + + foreach (var project in gateway.Subgraphs) + { + + } + writer.WriteIndentedLine(""); + writer.WriteIndentedLine(""); + writer.WriteIndentedLine(""); + writer.WriteIndentedLine(""); + } + } + } + + writer.WriteIndentedLine("]) { }"); + } } + + writer.WriteIndentedLine("}"); } - - + writer.WriteIndentedLine("}"); context.AddSource("FusionGatewayConfiguration.g.cs", code.ToString()); StringBuilderPool.Return(code); } +} + +namespace HotChocolate.Fusion.Composition.Tooling +{ + file class GatewayList : List + { + public GatewayList() + : base( + [ + GatewayInfo.Create( + "gateway", + SubgraphInfo.Create("purchase-api", "purchaseApi"), + SubgraphInfo.Create("ordering-api", "orderingApi"), + SubgraphInfo.Create("catalog-api", "catalogApi"), + SubgraphInfo.Create("identity-api", "identityApi"), + SubgraphInfo.Create("basket-api", "basketApi")) + ]) { } + } } \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.csproj b/src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.csproj index 9af5096ae08..e79fd6da3d6 100644 --- a/src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.csproj +++ b/src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.csproj @@ -25,6 +25,7 @@ + @@ -41,5 +42,5 @@ Resources.resx - + diff --git a/src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.targets b/src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.targets new file mode 100644 index 00000000000..340becf594f --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.targets @@ -0,0 +1,5 @@ + + + + + diff --git a/src/HotChocolate/Fusion/src/Analyzers/Models/GatewayClass.cs b/src/HotChocolate/Fusion/src/Analyzers/Models/GatewayClass.cs new file mode 100644 index 00000000000..67e7e4c8960 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/Models/GatewayClass.cs @@ -0,0 +1,10 @@ +namespace HotChocolate.Types.Analyzers; + +public sealed class GatewayClass(string name, string typeName, string variableName) : ISyntaxInfo +{ + public string Name { get; } = name; + + public string TypeName { get; } = typeName; + + public string VariableName { get; } = variableName; +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/Models/GatewayInfo.cs b/src/HotChocolate/Fusion/src/Analyzers/Models/GatewayInfo.cs new file mode 100644 index 00000000000..8b316fc5faf --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/Models/GatewayInfo.cs @@ -0,0 +1,10 @@ +namespace HotChocolate.Types.Analyzers; + +public class GatewayInfo(string name, string typeName) +{ + public string Name { get; } = name; + + public string TypeName { get; } = typeName; + + public List Subgraphs { get; } = new(); +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/Models/ISyntaxInfo.cs b/src/HotChocolate/Fusion/src/Analyzers/Models/ISyntaxInfo.cs new file mode 100644 index 00000000000..097a6e47f90 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/Models/ISyntaxInfo.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Types.Analyzers; + +public interface ISyntaxInfo +{ + string Name { get; } +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/Models/SubgraphClass.cs b/src/HotChocolate/Fusion/src/Analyzers/Models/SubgraphClass.cs new file mode 100644 index 00000000000..54bf0182bc6 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/Models/SubgraphClass.cs @@ -0,0 +1,10 @@ +namespace HotChocolate.Types.Analyzers; + +public sealed class SubgraphClass(string name, string typeName, string variableName) : ISyntaxInfo +{ + public string Name { get; } = name; + + public string TypeName { get; } = typeName; + + public string VariableName { get; } = variableName; +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/Models/SubgraphInfo.cs b/src/HotChocolate/Fusion/src/Analyzers/Models/SubgraphInfo.cs new file mode 100644 index 00000000000..f937a6d0f0c --- /dev/null +++ b/src/HotChocolate/Fusion/src/Analyzers/Models/SubgraphInfo.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Types.Analyzers; + +public class SubgraphInfo(string name, string typeName) +{ + public string Name { get; } = name; + + public string TypeName { get; } = typeName; +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/ProjectClass.cs b/src/HotChocolate/Fusion/src/Analyzers/ProjectClass.cs deleted file mode 100644 index 924267c3021..00000000000 --- a/src/HotChocolate/Fusion/src/Analyzers/ProjectClass.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace HotChocolate.Types.Analyzers; - -public sealed class ProjectClass(string name, string typeName, string variableName) : ISyntaxInfo -{ - public string Name { get; } = name; - - public string TypeName { get; } = typeName; - - public string VariableName { get; } = variableName; -} - -public sealed class GatewayClass(string name, string typeName, string variableName) : ISyntaxInfo -{ - public string Name { get; } = name; - - public string TypeName { get; } = typeName; - - public string VariableName { get; } = variableName; -} - -public class GatewayInfo(string name, string typeName) -{ - public string Name { get; } = name; - - public string TypeName { get; } = typeName; - - public List Projects { get; } = new(); -} - -public class ProjectInfo(string name, string typeName) -{ - public string Name { get; } = name; - - public string TypeName { get; } = typeName; -} - -public interface ISyntaxInfo -{ - string Name { get; } -} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.resx b/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.resx index b5c55dd8a49..9540f7ab4bb 100644 --- a/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.resx +++ b/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.resx @@ -19,391 +19,601 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - using System.Buffers; + using System; +using System.Runtime.CompilerServices; +using System.Buffers; +using System.Diagnostics; +using System.Net.Mime; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using HotChocolate.Execution.Configuration; using HotChocolate.Fusion; using HotChocolate.Fusion.Composition; using HotChocolate.Fusion.Composition.Features; +using HotChocolate.Fusion.Composition.Tooling; using HotChocolate.Language; using HotChocolate.Language.Visitors; using HotChocolate.Skimmed.Serialization; +using Microsoft.Extensions.DependencyInjection; -namespace Projects; - -internal static class FusionGatewayConfiguration +namespace HotChocolate.Fusion.Composition.Tooling { - public static async Task ComposeAsync(CancellationToken cancellationToken = default) + + internal static class FusionGatewayConfgHelper { - var gatewayDirectory = Path.GetDirectoryName(FusionGatewayConfigurationFiles.GatewayProject)!; - var packageFileName = Path.Combine(gatewayDirectory, $"gateway{Extensions.FusionPackage}"); - var packageFile = new FileInfo(packageFileName); - var settingsFileName = Path.Combine(gatewayDirectory, "gateway-settings.json"); - var settingsFile = new FileInfo(settingsFileName); - var subgraphDirectories = FusionGatewayConfigurationFiles.SubgraphProjects.Select(t => Path.GetDirectoryName(t)!).ToArray(); - - // Ensure Gateway Project Directory Exists. - if (!Directory.Exists(gatewayDirectory)) + public static async Task ConfigureAsync(CancellationToken cancellationToken = default) { - Directory.CreateDirectory(gatewayDirectory); + ExportSubgraphSchemaDocs(); + await EnsureSubgraphHasConfigAsync(cancellationToken); + await ComposeAsync(cancellationToken); } - await using var package = FusionGraphPackage.Open(packageFile.FullName); - var subgraphConfigs = (await package.GetSubgraphConfigurationsAsync(cancellationToken)).ToDictionary(t => t.Name); - await ResolveSubgraphPackagesAsync(subgraphDirectories, subgraphConfigs, cancellationToken); - - using var settingsJson = settingsFile.Exists - ? JsonDocument.Parse(await File.ReadAllTextAsync(settingsFile.FullName, cancellationToken)) - : await package.GetFusionGraphSettingsAsync(cancellationToken); - var settings = settingsJson.Deserialize<PackageSettings>() ?? new PackageSettings(); - - var features = settings.CreateFeatures(); - - var composer = new FusionGraphComposer( - settings.FusionTypePrefix, - settings.FusionTypeSelf, - () => new ConsoleLog()); + private static void ExportSubgraphSchemaDocs() + { + var processed = new HashSet<string>(); - var fusionGraph = await composer.TryComposeAsync(subgraphConfigs.Values, features, cancellationToken); + foreach (var gateway in new GatewayList()) + { + foreach (var subgraph in gateway.Subgraphs) + { + if (!processed.Add(subgraph.Path)) + { + continue; + } + + Console.WriteLine("Expoorting schema document for subgraph {0} ...", subgraph.Name); + + var workingDirectory = System.IO.Path.GetDirectoryName(subgraph.Path)!; + + var processStartInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "dotnet run --no-build --no-restore -- schema export --output schema.graphql", + WorkingDirectory = workingDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + using (var process = Process.Start(processStartInfo)!) + { + string output = process.StandardOutput.ReadToEnd(); + string errors = process.StandardError.ReadToEnd(); + + process.WaitForExit(); + + if (!string.IsNullOrEmpty(output)) + { + Console.WriteLine(output); + } + + if (!string.IsNullOrEmpty(errors)) + { + Console.WriteLine(errors); + } + + if (process.ExitCode != 0) + { + Console.WriteLine( + "{0}(1,1): error HF1002: ; Failed to export schema document for subgraph {1} ...", + subgraph.Path, + subgraph.Name); + Environment.Exit(-255); + } + } + } + } + } - if (fusionGraph is null) + private static async Task EnsureSubgraphHasConfigAsync(CancellationToken cancellationToken = default) { - Console.WriteLine("Fusion graph composition failed."); - return; + foreach (var gateway in new GatewayList()) + { + foreach (var project in gateway.Subgraphs) + { + var projectRoot = System.IO.Path.GetDirectoryName(project.Path)!; + var configFile = System.IO.Path.Combine(projectRoot, Defaults.ConfigFile); + + if (File.Exists(configFile)) + { + continue; + } + + var config = new SubgraphConfigurationDto(project.Name); + var configJson = PackageHelper.FormatSubgraphConfig(config); + await File.WriteAllTextAsync(configFile, configJson, cancellationToken); + } + } } - var fusionGraphDoc = Utf8GraphQLParser.Parse(SchemaFormatter.FormatAsString(fusionGraph)); - var typeNames = FusionTypeNames.From(fusionGraphDoc); - var rewriter = new FusionGraphConfigurationToSchemaRewriter(); - var schemaDoc = (DocumentNode)rewriter.Rewrite(fusionGraphDoc, typeNames)!; - - using var updateSettingsJson = JsonSerializer.SerializeToDocument(settings, new JsonSerializerOptions(JsonSerializerDefaults.Web)); - - await package.SetFusionGraphAsync(fusionGraphDoc, cancellationToken); - await package.SetFusionGraphSettingsAsync(updateSettingsJson, cancellationToken); - await package.SetSchemaAsync(schemaDoc, cancellationToken); - - foreach (var config in subgraphConfigs.Values) + private static async Task ComposeAsync(CancellationToken cancellationToken = default) { - await package.SetSubgraphConfigurationAsync(config, cancellationToken); + foreach (var gateway in new GatewayList()) + { + await ComposeGatewayAsync(gateway.Path, gateway.Subgraphs.Select(t => t.Path), cancellationToken); + } } - Console.WriteLine("Fusion graph composed."); - } - - private static async Task ResolveSubgraphPackagesAsync( - IReadOnlyList<string> subgraphDirectories, - IDictionary<string, SubgraphConfiguration> subgraphConfigs, - CancellationToken cancellationToken) - { - for (var i = 0; i < subgraphDirectories.Count; i++) + private static async Task ComposeGatewayAsync( + string gatewayProject, + IEnumerable<string> subgraphProjects, + CancellationToken cancellationToken = default) { - var path = subgraphDirectories[i]; - - if (!Directory.Exists(path)) + var gatewayDirectory = System.IO.Path.GetDirectoryName(gatewayProject)!; + var packageFileName = System.IO.Path.Combine(gatewayDirectory, $"gateway{Extensions.FusionPackage}"); + var packageFile = new FileInfo(packageFileName); + var settingsFileName = System.IO.Path.Combine(gatewayDirectory, "gateway-settings.json"); + var settingsFile = new FileInfo(settingsFileName); + var subgraphDirectories = subgraphProjects.Select(t => System.IO.Path.GetDirectoryName(t)!).ToArray(); + + // Ensure Gateway Project Directory Exists. + if (!Directory.Exists(gatewayDirectory)) { - continue; + Directory.CreateDirectory(gatewayDirectory); } - var configFile = Path.Combine(path, Defaults.ConfigFile); - var schemaFile = Path.Combine(path, Defaults.SchemaFile); - var extensionFile = Path.Combine(path, Defaults.ExtensionFile); + await using var package = FusionGraphPackage.Open(packageFile.FullName); + var subgraphConfigs = + (await package.GetSubgraphConfigurationsAsync(cancellationToken)).ToDictionary(t => t.Name); + await ResolveSubgraphPackagesAsync(subgraphDirectories, subgraphConfigs, cancellationToken); - if (!File.Exists(configFile) || !File.Exists(schemaFile)) - { - continue; - } + using var settingsJson = settingsFile.Exists + ? JsonDocument.Parse(await File.ReadAllTextAsync(settingsFile.FullName, cancellationToken)) + : await package.GetFusionGraphSettingsAsync(cancellationToken); + var settings = settingsJson.Deserialize<PackageSettings>() ?? new PackageSettings(); + + var features = settings.CreateFeatures(); + + var composer = new FusionGraphComposer( + settings.FusionTypePrefix, + settings.FusionTypeSelf, + () => new ConsoleLog()); - var conf = await PackageHelper.LoadSubgraphConfigAsync(configFile, cancellationToken); - var schema = await File.ReadAllTextAsync(schemaFile, cancellationToken); - var extensions = Array.Empty<string>(); + var fusionGraph = await composer.TryComposeAsync(subgraphConfigs.Values, features, cancellationToken); - if (File.Exists(extensionFile)) + if (fusionGraph is null) { - extensions = [await File.ReadAllTextAsync(extensionFile, cancellationToken),]; + Console.WriteLine("Fusion graph composition failed."); + return; } - subgraphConfigs[conf.Name] = - new SubgraphConfiguration( - conf.Name, - schema, - extensions, - conf.Clients, - conf.Extensions); - } - } -} + var fusionGraphDoc = Utf8GraphQLParser.Parse(SchemaFormatter.FormatAsString(fusionGraph)); + var typeNames = FusionTypeNames.From(fusionGraphDoc); + var rewriter = new FusionGraphConfigurationToSchemaRewriter(); + var schemaDoc = (DocumentNode)rewriter.Rewrite(fusionGraphDoc, typeNames)!; -file static class Defaults -{ - public const string SchemaFile = "schema.graphql"; - public const string ExtensionFile = "schema.extensions.graphql"; - public const string ConfigFile = "subgraph-config.json"; -} - -file static class Extensions -{ - public const string FusionPackage = ".fgp"; -} + using var updateSettingsJson = JsonSerializer.SerializeToDocument( + settings, + new JsonSerializerOptions(JsonSerializerDefaults.Web)); -file class PackageSettings -{ - private Feature? _reEncodeIds; - private Feature? _nodeField; - private TagDirective? _tagDirective; - private Transport? _transport; + await package.SetFusionGraphAsync(fusionGraphDoc, cancellationToken); + await package.SetFusionGraphSettingsAsync(updateSettingsJson, cancellationToken); + await package.SetSchemaAsync(schemaDoc, cancellationToken); - [JsonPropertyName("fusionTypePrefix")] - [JsonPropertyOrder(10)] - public string? FusionTypePrefix { get; set; } + foreach (var config in subgraphConfigs.Values) + { + await package.SetSubgraphConfigurationAsync(config, cancellationToken); + } - [JsonPropertyName("fusionTypeSelf")] - [JsonPropertyOrder(11)] - public bool FusionTypeSelf { get; set; } + Console.WriteLine("Fusion graph composed."); + } - public Transport Transport - { - get => _transport ??= new(); - set => _transport = value; + private static async Task ResolveSubgraphPackagesAsync( + IReadOnlyList<string> subgraphDirectories, + IDictionary<string, SubgraphConfiguration> subgraphConfigs, + CancellationToken cancellationToken) + { + for (var i = 0; i < subgraphDirectories.Count; i++) + { + var path = subgraphDirectories[i]; + + if (!Directory.Exists(path)) + { + continue; + } + + var configFile = System.IO.Path.Combine(path, Defaults.ConfigFile); + var schemaFile = System.IO.Path.Combine(path, Defaults.SchemaFile); + var extensionFile = System.IO.Path.Combine(path, Defaults.ExtensionFile); + + if (!File.Exists(configFile) || !File.Exists(schemaFile)) + { + continue; + } + + var conf = await PackageHelper.LoadSubgraphConfigAsync(configFile, cancellationToken); + var schema = await File.ReadAllTextAsync(schemaFile, cancellationToken); + var extensions = Array.Empty<string>(); + + if (File.Exists(extensionFile)) + { + extensions = [await File.ReadAllTextAsync(extensionFile, cancellationToken),]; + } + + subgraphConfigs[conf.Name] = + new SubgraphConfiguration( + conf.Name, + schema, + extensions, + conf.Clients, + conf.Extensions); + } + } } - [JsonPropertyName("nodeField")] - [JsonPropertyOrder(12)] - public Feature NodeField + file static class Defaults { - get => _nodeField ??= new(); - set => _nodeField = value; + public const string SchemaFile = "schema.graphql"; + public const string ExtensionFile = "schema.extensions.graphql"; + public const string ConfigFile = "subgraph-config.json"; } - [JsonPropertyName("reEncodeIds")] - [JsonPropertyOrder(13)] - public Feature ReEncodeIds + file static class Extensions { - get => _reEncodeIds ??= new(); - set => _reEncodeIds = value; + public const string FusionPackage = ".fgp"; } - [JsonPropertyName("tagDirective")] - [JsonPropertyOrder(14)] - public TagDirective TagDirective + file class PackageSettings { - get => _tagDirective ??= new(); - set => _tagDirective = value; - } + private Feature? _reEncodeIds; + private Feature? _nodeField; + private TagDirective? _tagDirective; + private Transport? _transport; - public FusionFeatureCollection CreateFeatures() - { - var features = new List<IFusionFeature> + [JsonPropertyName("fusionTypePrefix")] + [JsonPropertyOrder(10)] + public string? FusionTypePrefix { get; set; } + + [JsonPropertyName("fusionTypeSelf")] + [JsonPropertyOrder(11)] + public bool FusionTypeSelf { get; set; } + + public Transport Transport { - new TransportFeature - { - DefaultClientName = Transport.DefaultClientName, - }, - }; + get => _transport ??= new(); + set => _transport = value; + } - if (NodeField.Enabled) + [JsonPropertyName("nodeField")] + [JsonPropertyOrder(12)] + public Feature NodeField { - features.Add(FusionFeatures.NodeField); + get => _nodeField ??= new(); + set => _nodeField = value; } - if (ReEncodeIds.Enabled) + [JsonPropertyName("reEncodeIds")] + [JsonPropertyOrder(13)] + public Feature ReEncodeIds { - features.Add(FusionFeatures.ReEncodeIds); + get => _reEncodeIds ??= new(); + set => _reEncodeIds = value; } - if (TagDirective.Enabled) + [JsonPropertyName("tagDirective")] + [JsonPropertyOrder(14)] + public TagDirective TagDirective { - features.Add( - FusionFeatures.TagDirective( - TagDirective.Exclude, - TagDirective.MakePublic)); + get => _tagDirective ??= new(); + set => _tagDirective = value; } - return new FusionFeatureCollection(features); - } -} + public FusionFeatureCollection CreateFeatures() + { + var features = new List<IFusionFeature> + { + new TransportFeature + { + DefaultClientName = Transport.DefaultClientName, + }, + }; -file class Feature -{ - [JsonPropertyName("enabled")] - [JsonPropertyOrder(10)] - public bool Enabled { get; set; } -} + if (NodeField.Enabled) + { + features.Add(FusionFeatures.NodeField); + } -file sealed class TagDirective : Feature -{ - private string[]? _exclude; + if (ReEncodeIds.Enabled) + { + features.Add(FusionFeatures.ReEncodeIds); + } - [JsonPropertyName("makePublic")] - [JsonPropertyOrder(100)] - public bool MakePublic { get; set; } + if (TagDirective.Enabled) + { + features.Add( + FusionFeatures.TagDirective( + TagDirective.Exclude, + TagDirective.MakePublic)); + } + + return new FusionFeatureCollection(features); + } + } - [JsonPropertyName("exclude")] - [JsonPropertyOrder(101)] - public string[] Exclude + file class Feature { - get => _exclude ?? Array.Empty<string>(); - set => _exclude = value; + [JsonPropertyName("enabled")] + [JsonPropertyOrder(10)] + public bool Enabled { get; set; } } -} -file sealed class Transport -{ - [JsonPropertyName("defaultClientName")] - [JsonPropertyOrder(10)] - public string? DefaultClientName { get; set; } = "Fusion"; -} + file sealed class TagDirective : Feature + { + private string[]? _exclude; -file sealed class ConsoleLog : ICompositionLog -{ - public bool HasErrors { get; private set; } + [JsonPropertyName("makePublic")] + [JsonPropertyOrder(100)] + public bool MakePublic { get; set; } - public void Write(LogEntry e) - { - if (e.Severity is LogSeverity.Error) + [JsonPropertyName("exclude")] + [JsonPropertyOrder(101)] + public string[] Exclude { - HasErrors = true; + get => _exclude ?? Array.Empty<string>(); + set => _exclude = value; } + } - if (e.Code is null) - { - Console.WriteLine($"{e.Severity}: {e.Message}"); - } - else if (e.Coordinate is null) - { - Console.WriteLine($"{e.Severity}: {e.Code} {e.Message}"); - } - else - { - Console.WriteLine($"{e.Severity}: {e.Code} {e.Message} {e.Coordinate}"); - } + file sealed class Transport + { + [JsonPropertyName("defaultClientName")] + [JsonPropertyOrder(10)] + public string? DefaultClientName { get; set; } = "Fusion"; } -} -file sealed class SubgraphConfigurationDto( - string name, - IReadOnlyList<IClientConfiguration>? clients = null, - JsonElement? extensions = null) -{ - public string Name { get; } = name; + file sealed class ConsoleLog : ICompositionLog + { + public bool HasErrors { get; private set; } - public IReadOnlyList<IClientConfiguration> Clients { get; } = clients ?? Array.Empty<IClientConfiguration>(); + public void Write(LogEntry e) + { + if (e.Severity is LogSeverity.Error) + { + HasErrors = true; + } - public JsonElement? Extensions { get; } = extensions; -} + if (e.Code is null) + { + Console.WriteLine($"{e.Severity}: {e.Message}"); + } + else if (e.Coordinate is null) + { + Console.WriteLine($"{e.Severity}: {e.Code} {e.Message}"); + } + else + { + Console.WriteLine($"{e.Severity}: {e.Code} {e.Message} {e.Coordinate}"); + } + } + } -file static class PackageHelper -{ - public static async Task<SubgraphConfigurationDto> LoadSubgraphConfigAsync( - string filename, - CancellationToken ct) + file sealed class SubgraphConfigurationDto( + string name, + IReadOnlyList<IClientConfiguration>? clients = null, + JsonElement? extensions = null) { - await using var stream = File.OpenRead(filename); - return await ParseSubgraphConfigAsync(stream, ct); + public string Name { get; } = name; + + public IReadOnlyList<IClientConfiguration> Clients { get; } = clients ?? Array.Empty<IClientConfiguration>(); + + public JsonElement? Extensions { get; } = extensions; } - private static async Task<SubgraphConfigurationDto> ParseSubgraphConfigAsync( - Stream stream, - CancellationToken ct) + file static class PackageHelper { - using var document = await JsonDocument.ParseAsync(stream, cancellationToken: ct); - var configs = new List<IClientConfiguration>(); - var subgraph = default(string?); - var extensions = default(JsonElement?); + public static async Task<SubgraphConfigurationDto> LoadSubgraphConfigAsync( + string filename, + CancellationToken ct) + { + await using var stream = File.OpenRead(filename); + return await ParseSubgraphConfigAsync(stream, ct); + } - foreach (var property in document.RootElement.EnumerateObject()) + private static async Task<SubgraphConfigurationDto> ParseSubgraphConfigAsync( + Stream stream, + CancellationToken ct) { - switch (property.Name) + using var document = await JsonDocument.ParseAsync(stream, cancellationToken: ct); + var configs = new List<IClientConfiguration>(); + var subgraph = default(string?); + var extensions = default(JsonElement?); + + foreach (var property in document.RootElement.EnumerateObject()) { - case "subgraph": - subgraph = property.Value.GetString(); - break; + switch (property.Name) + { + case "subgraph": + subgraph = property.Value.GetString(); + break; + + case "http": + configs.Add(ReadHttpClientConfiguration(property.Value)); + break; + + case "websocket": + configs.Add(ReadWebSocketClientConfiguration(property.Value)); + break; + + case "extensions": + extensions = property.Value.SafeClone(); + break; + + default: + throw new NotSupportedException( + $"Configuration property `{property.Value}` is not supported."); + } + } + + if (string.IsNullOrEmpty(subgraph)) + { + throw new InvalidOperationException("No subgraph name was specified."); + } + + return new SubgraphConfigurationDto(subgraph, configs, extensions); + } + + private static HttpClientConfiguration ReadHttpClientConfiguration( + JsonElement element) + { + var baseAddress = new Uri(element.GetProperty("baseAddress").GetString()!); + var clientName = default(string?); - case "http": - configs.Add(ReadHttpClientConfiguration(property.Value)); - break; + if (element.TryGetProperty("clientName", out var clientNameElement)) + { + clientName = clientNameElement.GetString(); + } - case "websocket": - configs.Add(ReadWebSocketClientConfiguration(property.Value)); - break; + return new HttpClientConfiguration(baseAddress, clientName); + } - case "extensions": - extensions = property.Value.SafeClone(); - break; + private static WebSocketClientConfiguration ReadWebSocketClientConfiguration( + JsonElement element) + { + var baseAddress = new Uri(element.GetProperty("baseAddress").GetString()!); + var clientName = default(string?); - default: - throw new NotSupportedException( - $"Configuration property `{property.Value}` is not supported."); + if (element.TryGetProperty("clientName", out var clientNameElement)) + { + clientName = clientNameElement.GetString(); } + + return new WebSocketClientConfiguration(baseAddress, clientName); } - if (string.IsNullOrEmpty(subgraph)) + private static JsonElement SafeClone(this JsonElement element) { - throw new InvalidOperationException("No subgraph name was specified."); + var writer = new ArrayBufferWriter<byte>(); + using var jsonWriter = new Utf8JsonWriter(writer); + + element.WriteTo(jsonWriter); + jsonWriter.Flush(); + var reader = new Utf8JsonReader(writer.WrittenSpan, true, default); + + return JsonElement.ParseValue(ref reader); } - return new SubgraphConfigurationDto(subgraph, configs, extensions); + public static string FormatSubgraphConfig( + SubgraphConfigurationDto subgraphConfig) + { + var buffer = new ArrayBufferWriter<byte>(); + using var writer = new Utf8JsonWriter(buffer); + + writer.WriteStartObject(); + writer.WriteString("subgraph", subgraphConfig.Name); + + foreach (var client in subgraphConfig.Clients) + { + switch (client) + { + case HttpClientConfiguration config: + writer.WriteStartObject("http"); + writer.WriteString("baseAddress", config.BaseAddress.ToString()); + + if (config.ClientName is not null) + { + writer.WriteString("clientName", config.ClientName); + } + + writer.WriteEndObject(); + break; + + case WebSocketClientConfiguration config: + writer.WriteStartObject("websocket"); + writer.WriteString("baseAddress", config.BaseAddress.ToString()); + + if (config.ClientName is not null) + { + writer.WriteString("clientName", config.ClientName); + } + + writer.WriteEndObject(); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(client)); + } + } + + if (subgraphConfig.Extensions is not null) + { + writer.WritePropertyName("extensions"); + subgraphConfig.Extensions.Value.WriteTo(writer); + } + + writer.WriteEndObject(); + writer.Flush(); + + return Encoding.UTF8.GetString(buffer.WrittenSpan); + } } - private static HttpClientConfiguration ReadHttpClientConfiguration( - JsonElement element) + file sealed class FusionGraphConfigurationToSchemaRewriter + : SyntaxRewriter<FusionTypeNames> { - var baseAddress = new Uri(element.GetProperty("baseAddress").GetString()!); - var clientName = default(string?); - - if (element.TryGetProperty("clientName", out var clientNameElement)) + public DocumentNode Rewrite(DocumentNode fusionGraph) { - clientName = clientNameElement.GetString(); + var typeNames = FusionTypeNames.From(fusionGraph); + var schemaDoc = (DocumentNode?)Rewrite(fusionGraph, typeNames); + + if (schemaDoc is null) + { + throw new InvalidOperationException(); + } + + return schemaDoc; } - return new HttpClientConfiguration(baseAddress, clientName); + protected override DirectiveNode? RewriteDirective(DirectiveNode node, FusionTypeNames context) + => context.IsFusionDirective(node.Name.Value) + ? null + : base.RewriteDirective(node, context); } - private static WebSocketClientConfiguration ReadWebSocketClientConfiguration( - JsonElement element) + file sealed class SubgraphInfo(string name, string path, string variableName) { - var baseAddress = new Uri(element.GetProperty("baseAddress").GetString()!); - var clientName = default(string?); + public string Name { get; } = name; - if (element.TryGetProperty("clientName", out var clientNameElement)) - { - clientName = clientNameElement.GetString(); - } + public string Path { get; } = path; + + public string VariableName { get; } = variableName; - return new WebSocketClientConfiguration(baseAddress, clientName); + public static SubgraphInfo Create<TProject>(string name, string variableName) + where TProject : IProjectMetadata, new() + => new(name, new TProject().ProjectPath, variableName); } - private static JsonElement SafeClone(this JsonElement element) + file sealed class GatewayInfo(string name, string path, IReadOnlyList<SubgraphInfo> subgraphs) { - var writer = new ArrayBufferWriter<byte>(); - using var jsonWriter = new Utf8JsonWriter(writer); + public string Name { get; } = name; - element.WriteTo(jsonWriter); - jsonWriter.Flush(); - var reader = new Utf8JsonReader(writer.WrittenSpan, true, default); + public string Path { get; } = path; - return JsonElement.ParseValue(ref reader); + public IReadOnlyList<SubgraphInfo> Subgraphs { get; } = subgraphs; + + public static GatewayInfo Create<TProject>(string name, params SubgraphInfo[] projects) + where TProject : IProjectMetadata, new() + => new(name, new TProject().ProjectPath, projects); } } -file sealed class FusionGraphConfigurationToSchemaRewriter - : SyntaxRewriter<FusionTypeNames> +namespace Aspire.Hosting { - public DocumentNode Rewrite(DocumentNode fusionGraph) + public static class Extensions { - var typeNames = FusionTypeNames.From(fusionGraph); - var schemaDoc = (DocumentNode?)Rewrite(fusionGraph, typeNames); - - if (schemaDoc is null) + public static DistributedApplication Compose(this DistributedApplication application) { - throw new InvalidOperationException(); - } + var options = application.Services.GetRequiredService<DistributedApplicationOptions>(); - return schemaDoc; - } + if (options.Args is ["compose"]) + { + FusionGatewayConfgHelper.ConfigureAsync().Wait(); + Environment.Exit(0); + } - protected override DirectiveNode? RewriteDirective(DirectiveNode node, FusionTypeNames context) - => context.IsFusionDirective(node.Name.Value) ? null : base.RewriteDirective(node, context); + return application; + } + } } \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj b/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj index 0b4499b2a54..6c6b47f9fa8 100644 --- a/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj +++ b/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj @@ -1,29 +1,30 @@ - - - - net8.0 - enable - enable - - false - true - - - - - - - - - - - - - - - - - - - - + + + + net8.0 + enable + enable + + false + true + true + + + + + + + + + + + + + + + + + + + + diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/Program.cs b/src/HotChocolate/Fusion/test/Analyzers.Tests/Program.cs index 0b8daa14a9b..f93248ffc84 100644 --- a/src/HotChocolate/Fusion/test/Analyzers.Tests/Program.cs +++ b/src/HotChocolate/Fusion/test/Analyzers.Tests/Program.cs @@ -52,17 +52,27 @@ public static void Foo(string[] args) .WithReference(orderingDb) .WithReference(catalogApi.GetEndpoint("http")) .WithEnvironment("Identity__Url", identityHttpsEndpoint); + + var purchaseApi = builder + .AddProject("ordering-api") + .WithReference(rabbitMq) + .WithReference(orderingDb) + .WithReference(catalogApi.GetEndpoint("http")) + .WithEnvironment("Identity__Url", identityHttpsEndpoint); // Fusion + /* builder .AddFusionGateway("gateway") .WithSubgraph(basketApi) .WithSubgraph(identityApi.GetEndpoint("http")) .WithSubgraph(catalogApi) .WithSubgraph(orderingApi) + .WithSubgraph(purchaseApi) .WithEnvironment("Identity__Url", identityHttpsEndpoint); + */ - builder.Build().Run(); + builder.Build().Compose().Run(); } } diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/T.cs b/src/HotChocolate/Fusion/test/Analyzers.Tests/Projects.cs similarity index 83% rename from src/HotChocolate/Fusion/test/Analyzers.Tests/T.cs rename to src/HotChocolate/Fusion/test/Analyzers.Tests/Projects.cs index 210a4e76fc5..7129c351a1a 100644 --- a/src/HotChocolate/Fusion/test/Analyzers.Tests/T.cs +++ b/src/HotChocolate/Fusion/test/Analyzers.Tests/Projects.cs @@ -24,6 +24,12 @@ public class eShop_Ordering_API : global::Aspire.Hosting.IProjectMetadata public string ProjectPath => """/Users/michael/local/webshop-workshop/src/Ordering.API/eShop.Ordering.API.csproj"""; } +[global::System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, ProjectPath = {ProjectPath}")] +public class eShop_Purchase_API : global::Aspire.Hosting.IProjectMetadata +{ + public string ProjectPath => """/Users/michael/local/webshop-workshop/src/Purchase.API/eShop.Purchase.API.csproj"""; +} + [global::System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, ProjectPath = {ProjectPath}")] public class eShop_Gateway : global::Aspire.Hosting.IProjectMetadata { diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/Test.cs b/src/HotChocolate/Fusion/test/Analyzers.Tests/Test.cs new file mode 100644 index 00000000000..cf5bc32552c --- /dev/null +++ b/src/HotChocolate/Fusion/test/Analyzers.Tests/Test.cs @@ -0,0 +1,618 @@ +// + +#nullable enable + +using System; +using System.Runtime.CompilerServices; +using System.Buffers; +using System.Diagnostics; +using System.Net.Mime; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using HotChocolate.Execution.Configuration; +using HotChocolate.Fusion; +using HotChocolate.Fusion.Composition; +using HotChocolate.Fusion.Composition.Features; +using HotChocolate.Fusion.Composition.Tooling; +using HotChocolate.Language; +using HotChocolate.Language.Visitors; +using HotChocolate.Skimmed.Serialization; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Fusion.Composition.Tooling +{ + + internal static class FusionGatewayConfgHelper + { + public static async Task ConfigureAsync(CancellationToken cancellationToken = default) + { + ExportSubgraphSchemaDocs(); + await EnsureSubgraphHasConfigAsync(cancellationToken); + await ComposeAsync(cancellationToken); + } + + private static void ExportSubgraphSchemaDocs() + { + var processed = new HashSet(); + + foreach (var gateway in new GatewayList()) + { + foreach (var subgraph in gateway.Subgraphs) + { + if (!processed.Add(subgraph.Path)) + { + continue; + } + + Console.WriteLine("Expoorting schema document for subgraph {0} ...", subgraph.Name); + + var workingDirectory = System.IO.Path.GetDirectoryName(subgraph.Path)!; + + var processStartInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "dotnet run --no-build --no-restore -- schema export --output schema.graphql", + WorkingDirectory = workingDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + using (var process = Process.Start(processStartInfo)!) + { + string output = process.StandardOutput.ReadToEnd(); + string errors = process.StandardError.ReadToEnd(); + + process.WaitForExit(); + + if (!string.IsNullOrEmpty(output)) + { + Console.WriteLine(output); + } + + if (!string.IsNullOrEmpty(errors)) + { + Console.WriteLine(errors); + } + + if (process.ExitCode != 0) + { + Console.WriteLine( + "{0}(1,1): error HF1002: ; Failed to export schema document for subgraph {1} ...", + subgraph.Path, + subgraph.Name); + Environment.Exit(-255); + } + } + } + } + } + + private static async Task EnsureSubgraphHasConfigAsync(CancellationToken cancellationToken = default) + { + foreach (var gateway in new GatewayList()) + { + foreach (var project in gateway.Subgraphs) + { + var projectRoot = System.IO.Path.GetDirectoryName(project.Path)!; + var configFile = System.IO.Path.Combine(projectRoot, Defaults.ConfigFile); + + if (File.Exists(configFile)) + { + continue; + } + + var config = new SubgraphConfigurationDto(project.Name); + var configJson = PackageHelper.FormatSubgraphConfig(config); + await File.WriteAllTextAsync(configFile, configJson, cancellationToken); + } + } + } + + private static async Task ComposeAsync(CancellationToken cancellationToken = default) + { + foreach (var gateway in new GatewayList()) + { + await ComposeGatewayAsync(gateway.Path, gateway.Subgraphs.Select(t => t.Path), cancellationToken); + } + } + + private static async Task ComposeGatewayAsync( + string gatewayProject, + IEnumerable subgraphProjects, + CancellationToken cancellationToken = default) + { + var gatewayDirectory = System.IO.Path.GetDirectoryName(gatewayProject)!; + var packageFileName = System.IO.Path.Combine(gatewayDirectory, $"gateway{Extensions.FusionPackage}"); + var packageFile = new FileInfo(packageFileName); + var settingsFileName = System.IO.Path.Combine(gatewayDirectory, "gateway-settings.json"); + var settingsFile = new FileInfo(settingsFileName); + var subgraphDirectories = subgraphProjects.Select(t => System.IO.Path.GetDirectoryName(t)!).ToArray(); + + // Ensure Gateway Project Directory Exists. + if (!Directory.Exists(gatewayDirectory)) + { + Directory.CreateDirectory(gatewayDirectory); + } + + await using var package = FusionGraphPackage.Open(packageFile.FullName); + var subgraphConfigs = + (await package.GetSubgraphConfigurationsAsync(cancellationToken)).ToDictionary(t => t.Name); + await ResolveSubgraphPackagesAsync(subgraphDirectories, subgraphConfigs, cancellationToken); + + using var settingsJson = settingsFile.Exists + ? JsonDocument.Parse(await File.ReadAllTextAsync(settingsFile.FullName, cancellationToken)) + : await package.GetFusionGraphSettingsAsync(cancellationToken); + var settings = settingsJson.Deserialize() ?? new PackageSettings(); + + var features = settings.CreateFeatures(); + + var composer = new FusionGraphComposer( + settings.FusionTypePrefix, + settings.FusionTypeSelf, + () => new ConsoleLog()); + + var fusionGraph = await composer.TryComposeAsync(subgraphConfigs.Values, features, cancellationToken); + + if (fusionGraph is null) + { + Console.WriteLine("Fusion graph composition failed."); + return; + } + + var fusionGraphDoc = Utf8GraphQLParser.Parse(SchemaFormatter.FormatAsString(fusionGraph)); + var typeNames = FusionTypeNames.From(fusionGraphDoc); + var rewriter = new FusionGraphConfigurationToSchemaRewriter(); + var schemaDoc = (DocumentNode)rewriter.Rewrite(fusionGraphDoc, typeNames)!; + + using var updateSettingsJson = JsonSerializer.SerializeToDocument( + settings, + new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + await package.SetFusionGraphAsync(fusionGraphDoc, cancellationToken); + await package.SetFusionGraphSettingsAsync(updateSettingsJson, cancellationToken); + await package.SetSchemaAsync(schemaDoc, cancellationToken); + + foreach (var config in subgraphConfigs.Values) + { + await package.SetSubgraphConfigurationAsync(config, cancellationToken); + } + + Console.WriteLine("Fusion graph composed."); + } + + private static async Task ResolveSubgraphPackagesAsync( + IReadOnlyList subgraphDirectories, + IDictionary subgraphConfigs, + CancellationToken cancellationToken) + { + for (var i = 0; i < subgraphDirectories.Count; i++) + { + var path = subgraphDirectories[i]; + + if (!Directory.Exists(path)) + { + continue; + } + + var configFile = System.IO.Path.Combine(path, Defaults.ConfigFile); + var schemaFile = System.IO.Path.Combine(path, Defaults.SchemaFile); + var extensionFile = System.IO.Path.Combine(path, Defaults.ExtensionFile); + + if (!File.Exists(configFile) || !File.Exists(schemaFile)) + { + continue; + } + + var conf = await PackageHelper.LoadSubgraphConfigAsync(configFile, cancellationToken); + var schema = await File.ReadAllTextAsync(schemaFile, cancellationToken); + var extensions = Array.Empty(); + + if (File.Exists(extensionFile)) + { + extensions = [await File.ReadAllTextAsync(extensionFile, cancellationToken),]; + } + + subgraphConfigs[conf.Name] = + new SubgraphConfiguration( + conf.Name, + schema, + extensions, + conf.Clients, + conf.Extensions); + } + } + } + + file static class Defaults + { + public const string SchemaFile = "schema.graphql"; + public const string ExtensionFile = "schema.extensions.graphql"; + public const string ConfigFile = "subgraph-config.json"; + } + + file static class Extensions + { + public const string FusionPackage = ".fgp"; + } + + file class PackageSettings + { + private Feature? _reEncodeIds; + private Feature? _nodeField; + private TagDirective? _tagDirective; + private Transport? _transport; + + [JsonPropertyName("fusionTypePrefix")] + [JsonPropertyOrder(10)] + public string? FusionTypePrefix { get; set; } + + [JsonPropertyName("fusionTypeSelf")] + [JsonPropertyOrder(11)] + public bool FusionTypeSelf { get; set; } + + public Transport Transport + { + get => _transport ??= new(); + set => _transport = value; + } + + [JsonPropertyName("nodeField")] + [JsonPropertyOrder(12)] + public Feature NodeField + { + get => _nodeField ??= new(); + set => _nodeField = value; + } + + [JsonPropertyName("reEncodeIds")] + [JsonPropertyOrder(13)] + public Feature ReEncodeIds + { + get => _reEncodeIds ??= new(); + set => _reEncodeIds = value; + } + + [JsonPropertyName("tagDirective")] + [JsonPropertyOrder(14)] + public TagDirective TagDirective + { + get => _tagDirective ??= new(); + set => _tagDirective = value; + } + + public FusionFeatureCollection CreateFeatures() + { + var features = new List + { + new TransportFeature + { + DefaultClientName = Transport.DefaultClientName, + }, + }; + + if (NodeField.Enabled) + { + features.Add(FusionFeatures.NodeField); + } + + if (ReEncodeIds.Enabled) + { + features.Add(FusionFeatures.ReEncodeIds); + } + + if (TagDirective.Enabled) + { + features.Add( + FusionFeatures.TagDirective( + TagDirective.Exclude, + TagDirective.MakePublic)); + } + + return new FusionFeatureCollection(features); + } + } + + file class Feature + { + [JsonPropertyName("enabled")] + [JsonPropertyOrder(10)] + public bool Enabled { get; set; } + } + + file sealed class TagDirective : Feature + { + private string[]? _exclude; + + [JsonPropertyName("makePublic")] + [JsonPropertyOrder(100)] + public bool MakePublic { get; set; } + + [JsonPropertyName("exclude")] + [JsonPropertyOrder(101)] + public string[] Exclude + { + get => _exclude ?? Array.Empty(); + set => _exclude = value; + } + } + + file sealed class Transport + { + [JsonPropertyName("defaultClientName")] + [JsonPropertyOrder(10)] + public string? DefaultClientName { get; set; } = "Fusion"; + } + + file sealed class ConsoleLog : ICompositionLog + { + public bool HasErrors { get; private set; } + + public void Write(LogEntry e) + { + if (e.Severity is LogSeverity.Error) + { + HasErrors = true; + } + + if (e.Code is null) + { + Console.WriteLine($"{e.Severity}: {e.Message}"); + } + else if (e.Coordinate is null) + { + Console.WriteLine($"{e.Severity}: {e.Code} {e.Message}"); + } + else + { + Console.WriteLine($"{e.Severity}: {e.Code} {e.Message} {e.Coordinate}"); + } + } + } + + file sealed class SubgraphConfigurationDto( + string name, + IReadOnlyList? clients = null, + JsonElement? extensions = null) + { + public string Name { get; } = name; + + public IReadOnlyList Clients { get; } = clients ?? Array.Empty(); + + public JsonElement? Extensions { get; } = extensions; + } + + file static class PackageHelper + { + public static async Task LoadSubgraphConfigAsync( + string filename, + CancellationToken ct) + { + await using var stream = File.OpenRead(filename); + return await ParseSubgraphConfigAsync(stream, ct); + } + + private static async Task ParseSubgraphConfigAsync( + Stream stream, + CancellationToken ct) + { + using var document = await JsonDocument.ParseAsync(stream, cancellationToken: ct); + var configs = new List(); + var subgraph = default(string?); + var extensions = default(JsonElement?); + + foreach (var property in document.RootElement.EnumerateObject()) + { + switch (property.Name) + { + case "subgraph": + subgraph = property.Value.GetString(); + break; + + case "http": + configs.Add(ReadHttpClientConfiguration(property.Value)); + break; + + case "websocket": + configs.Add(ReadWebSocketClientConfiguration(property.Value)); + break; + + case "extensions": + extensions = property.Value.SafeClone(); + break; + + default: + throw new NotSupportedException( + $"Configuration property `{property.Value}` is not supported."); + } + } + + if (string.IsNullOrEmpty(subgraph)) + { + throw new InvalidOperationException("No subgraph name was specified."); + } + + return new SubgraphConfigurationDto(subgraph, configs, extensions); + } + + private static HttpClientConfiguration ReadHttpClientConfiguration( + JsonElement element) + { + var baseAddress = new Uri(element.GetProperty("baseAddress").GetString()!); + var clientName = default(string?); + + if (element.TryGetProperty("clientName", out var clientNameElement)) + { + clientName = clientNameElement.GetString(); + } + + return new HttpClientConfiguration(baseAddress, clientName); + } + + private static WebSocketClientConfiguration ReadWebSocketClientConfiguration( + JsonElement element) + { + var baseAddress = new Uri(element.GetProperty("baseAddress").GetString()!); + var clientName = default(string?); + + if (element.TryGetProperty("clientName", out var clientNameElement)) + { + clientName = clientNameElement.GetString(); + } + + return new WebSocketClientConfiguration(baseAddress, clientName); + } + + private static JsonElement SafeClone(this JsonElement element) + { + var writer = new ArrayBufferWriter(); + using var jsonWriter = new Utf8JsonWriter(writer); + + element.WriteTo(jsonWriter); + jsonWriter.Flush(); + var reader = new Utf8JsonReader(writer.WrittenSpan, true, default); + + return JsonElement.ParseValue(ref reader); + } + + public static string FormatSubgraphConfig( + SubgraphConfigurationDto subgraphConfig) + { + var buffer = new ArrayBufferWriter(); + using var writer = new Utf8JsonWriter(buffer); + + writer.WriteStartObject(); + writer.WriteString("subgraph", subgraphConfig.Name); + + foreach (var client in subgraphConfig.Clients) + { + switch (client) + { + case HttpClientConfiguration config: + writer.WriteStartObject("http"); + writer.WriteString("baseAddress", config.BaseAddress.ToString()); + + if (config.ClientName is not null) + { + writer.WriteString("clientName", config.ClientName); + } + + writer.WriteEndObject(); + break; + + case WebSocketClientConfiguration config: + writer.WriteStartObject("websocket"); + writer.WriteString("baseAddress", config.BaseAddress.ToString()); + + if (config.ClientName is not null) + { + writer.WriteString("clientName", config.ClientName); + } + + writer.WriteEndObject(); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(client)); + } + } + + if (subgraphConfig.Extensions is not null) + { + writer.WritePropertyName("extensions"); + subgraphConfig.Extensions.Value.WriteTo(writer); + } + + writer.WriteEndObject(); + writer.Flush(); + + return Encoding.UTF8.GetString(buffer.WrittenSpan); + } + } + + file sealed class FusionGraphConfigurationToSchemaRewriter + : SyntaxRewriter + { + public DocumentNode Rewrite(DocumentNode fusionGraph) + { + var typeNames = FusionTypeNames.From(fusionGraph); + var schemaDoc = (DocumentNode?)Rewrite(fusionGraph, typeNames); + + if (schemaDoc is null) + { + throw new InvalidOperationException(); + } + + return schemaDoc; + } + + protected override DirectiveNode? RewriteDirective(DirectiveNode node, FusionTypeNames context) + => context.IsFusionDirective(node.Name.Value) + ? null + : base.RewriteDirective(node, context); + } + + file sealed class SubgraphInfo(string name, string path, string variableName) + { + public string Name { get; } = name; + + public string Path { get; } = path; + + public string VariableName { get; } = variableName; + + public static SubgraphInfo Create(string name, string variableName) + where TProject : IProjectMetadata, new() + => new(name, new TProject().ProjectPath, variableName); + } + + file sealed class GatewayInfo(string name, string path, IReadOnlyList subgraphs) + { + public string Name { get; } = name; + + public string Path { get; } = path; + + public IReadOnlyList Subgraphs { get; } = subgraphs; + + public static GatewayInfo Create(string name, params SubgraphInfo[] projects) + where TProject : IProjectMetadata, new() + => new(name, new TProject().ProjectPath, projects); + } +} + +namespace Aspire.Hosting +{ + public static class Extensions + { + public static DistributedApplication Compose(this DistributedApplication application) + { + var options = application.Services.GetRequiredService(); + + if (options.Args is ["compose"]) + { + FusionGatewayConfgHelper.ConfigureAsync().Wait(); + Environment.Exit(0); + } + + return application; + } + } +} + +namespace HotChocolate.Fusion.Composition.Tooling +{ +file class GatewayList : List +{ + public GatewayList() + : base( + [ + GatewayInfo.Create( + "gateway", + SubgraphInfo.Create("purchase-api", "purchaseApi"), + SubgraphInfo.Create("ordering-api", "orderingApi"), + SubgraphInfo.Create("catalog-api", "catalogApi"), + SubgraphInfo.Create("identity-api", "identityApi"), + SubgraphInfo.Create("basket-api", "basketApi")) + ]) { } +} +} \ No newline at end of file From c263ac0978460cc109ad51b19c2ff307fd3f69e5 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 6 Mar 2024 16:10:45 -0800 Subject: [PATCH 4/5] edits --- .../Fusion/HotChocolate.Fusion.sln | 24 +++--- .../ConfigurationGenerator.cs | 83 +++++++++++-------- .../Helpers/CodeWriter.cs | 0 .../Helpers/CodeWriterExtensions.cs | 3 - .../Helpers/StringBuilderPool.cs | 0 .../Helpers/SymbolExtensions.cs | 0 .../HotChocolate.Fusion.Analyzers.targets | 0 ...olate.Fusion.Composition.Analyzers.csproj} | 0 .../Models/GatewayClass.cs | 0 .../Models/GatewayInfo.cs | 0 .../Models/ISyntaxInfo.cs | 0 .../Models/SubgraphClass.cs | 0 .../Models/SubgraphInfo.cs | 0 .../Properties/Resources.Designer.cs | 6 ++ .../Properties/Resources.resx | 52 +++++++++++- .../Properties/launchSettings.json | 0 .../WellKnownTypeNames.cs | 0 ...Fusion.Composition.Analyzers.Tests.csproj} | 9 -- .../Program.cs | 48 +---------- .../Projects.cs | 0 .../Test.txt} | 0 .../Fusion/test/Directory.Build.props | 1 + 22 files changed, 120 insertions(+), 106 deletions(-) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/ConfigurationGenerator.cs (82%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/Helpers/CodeWriter.cs (100%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/Helpers/CodeWriterExtensions.cs (85%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/Helpers/StringBuilderPool.cs (100%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/Helpers/SymbolExtensions.cs (100%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/HotChocolate.Fusion.Analyzers.targets (100%) rename src/HotChocolate/Fusion/src/{Analyzers/HotChocolate.Fusion.Analyzers.csproj => Composition.Analyzers/HotChocolate.Fusion.Composition.Analyzers.csproj} (100%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/Models/GatewayClass.cs (100%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/Models/GatewayInfo.cs (100%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/Models/ISyntaxInfo.cs (100%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/Models/SubgraphClass.cs (100%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/Models/SubgraphInfo.cs (100%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/Properties/Resources.Designer.cs (92%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/Properties/Resources.resx (90%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/Properties/launchSettings.json (100%) rename src/HotChocolate/Fusion/src/{Analyzers => Composition.Analyzers}/WellKnownTypeNames.cs (100%) rename src/HotChocolate/Fusion/test/{Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj => Composition.Analyzers.Tests/HotChocolate.Fusion.Composition.Analyzers.Tests.csproj} (60%) rename src/HotChocolate/Fusion/test/{Analyzers.Tests => Composition.Analyzers.Tests}/Program.cs (60%) rename src/HotChocolate/Fusion/test/{Analyzers.Tests => Composition.Analyzers.Tests}/Projects.cs (100%) rename src/HotChocolate/Fusion/test/{Analyzers.Tests/Test.cs => Composition.Analyzers.Tests/Test.txt} (100%) diff --git a/src/HotChocolate/Fusion/HotChocolate.Fusion.sln b/src/HotChocolate/Fusion/HotChocolate.Fusion.sln index 74fa4a68220..9c42ceb037b 100644 --- a/src/HotChocolate/Fusion/HotChocolate.Fusion.sln +++ b/src/HotChocolate/Fusion/HotChocolate.Fusion.sln @@ -25,9 +25,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Fusion.Command EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Fusion.CommandLine.Tests", "test\CommandLine.Tests\HotChocolate.Fusion.CommandLine.Tests.csproj", "{DBD317C2-8485-4A75-8BB7-D70C02B40944}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Fusion.Analyzers", "src\Analyzers\HotChocolate.Fusion.Analyzers.csproj", "{D212EA75-626D-482E-91DF-3B2BBF185D7C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Fusion.Composition.Analyzers", "src\Composition.Analyzers\HotChocolate.Fusion.Composition.Analyzers.csproj", "{A939BC1B-93A0-40DC-B336-27FF2BC2F704}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Fusion.Analyzers.Tests", "test\Analyzers.Tests\HotChocolate.Fusion.Analyzers.Tests.csproj", "{FEA9A31F-CABC-410A-9235-E6D440925F62}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Fusion.Composition.Analyzers.Tests", "test\Composition.Analyzers.Tests\HotChocolate.Fusion.Composition.Analyzers.Tests.csproj", "{FD460672-8769-4EB8-87E0-A6A9D5C946C7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -74,14 +74,14 @@ Global {DBD317C2-8485-4A75-8BB7-D70C02B40944}.Debug|Any CPU.Build.0 = Debug|Any CPU {DBD317C2-8485-4A75-8BB7-D70C02B40944}.Release|Any CPU.ActiveCfg = Release|Any CPU {DBD317C2-8485-4A75-8BB7-D70C02B40944}.Release|Any CPU.Build.0 = Release|Any CPU - {D212EA75-626D-482E-91DF-3B2BBF185D7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D212EA75-626D-482E-91DF-3B2BBF185D7C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D212EA75-626D-482E-91DF-3B2BBF185D7C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D212EA75-626D-482E-91DF-3B2BBF185D7C}.Release|Any CPU.Build.0 = Release|Any CPU - {FEA9A31F-CABC-410A-9235-E6D440925F62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FEA9A31F-CABC-410A-9235-E6D440925F62}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FEA9A31F-CABC-410A-9235-E6D440925F62}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FEA9A31F-CABC-410A-9235-E6D440925F62}.Release|Any CPU.Build.0 = Release|Any CPU + {A939BC1B-93A0-40DC-B336-27FF2BC2F704}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A939BC1B-93A0-40DC-B336-27FF2BC2F704}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A939BC1B-93A0-40DC-B336-27FF2BC2F704}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A939BC1B-93A0-40DC-B336-27FF2BC2F704}.Release|Any CPU.Build.0 = Release|Any CPU + {FD460672-8769-4EB8-87E0-A6A9D5C946C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD460672-8769-4EB8-87E0-A6A9D5C946C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD460672-8769-4EB8-87E0-A6A9D5C946C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD460672-8769-4EB8-87E0-A6A9D5C946C7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {0355AF0F-B91D-4852-8C9F-8E13CE5C88F3} = {748FCFC6-3EE7-4CFD-AFB3-B0F7B1ACD026} @@ -93,7 +93,7 @@ Global {0A07E4BB-0CFE-406C-B3F4-E26D0100F6F9} = {0EF9C546-286E-407F-A02E-731804507FDE} {B4FA97BA-2C36-48F6-ABC4-7088C655E936} = {748FCFC6-3EE7-4CFD-AFB3-B0F7B1ACD026} {DBD317C2-8485-4A75-8BB7-D70C02B40944} = {0EF9C546-286E-407F-A02E-731804507FDE} - {D212EA75-626D-482E-91DF-3B2BBF185D7C} = {748FCFC6-3EE7-4CFD-AFB3-B0F7B1ACD026} - {FEA9A31F-CABC-410A-9235-E6D440925F62} = {0EF9C546-286E-407F-A02E-731804507FDE} + {A939BC1B-93A0-40DC-B336-27FF2BC2F704} = {748FCFC6-3EE7-4CFD-AFB3-B0F7B1ACD026} + {FD460672-8769-4EB8-87E0-A6A9D5C946C7} = {0EF9C546-286E-407F-A02E-731804507FDE} EndGlobalSection EndGlobal diff --git a/src/HotChocolate/Fusion/src/Analyzers/ConfigurationGenerator.cs b/src/HotChocolate/Fusion/src/Composition.Analyzers/ConfigurationGenerator.cs similarity index 82% rename from src/HotChocolate/Fusion/src/Analyzers/ConfigurationGenerator.cs rename to src/HotChocolate/Fusion/src/Composition.Analyzers/ConfigurationGenerator.cs index d36cd30abf6..90322479cd4 100644 --- a/src/HotChocolate/Fusion/src/Analyzers/ConfigurationGenerator.cs +++ b/src/HotChocolate/Fusion/src/Composition.Analyzers/ConfigurationGenerator.cs @@ -11,6 +11,8 @@ public class ConfigurationGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { + context.RegisterPostInitializationOutput(AddInitializationContent); + var modulesAndTypes = context.SyntaxProvider .CreateSyntaxProvider( @@ -22,7 +24,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput( valueProvider, - static (context, source) => Execute(context, source.Left, source.Right!)); + static (context, source) => Execute(context, source.Right!)); } private static bool IsRelevant(SyntaxNode node) @@ -37,8 +39,7 @@ private static bool IsRelevant(SyntaxNode node) Expression: MemberAccessExpressionSyntax memberAccess, }) { - if (memberAccess.Name is GenericNameSyntax genericName && - genericName.TypeArgumentList.Arguments.Count == 1) + if (memberAccess.Name is GenericNameSyntax { TypeArgumentList.Arguments.Count: 1, } genericName) { if (genericName.Identifier.ValueText.Equals("AddFusionGateway", StringComparison.Ordinal)) { @@ -139,7 +140,7 @@ private static bool IsRelevant(SyntaxNode node) TypeArgumentList.Arguments: { Count: 1, } fusionArgs, }, }, - } parentInvocation && + } && context.SemanticModel.GetTypeInfo(fusionArgs[0]).Type is INamedTypeSymbol gatewayType) { var argument = subgraphInvocation.ArgumentList.Arguments[0]; @@ -182,7 +183,6 @@ private static string GetVariableName(ArgumentSyntax argument) private static void Execute( SourceProductionContext context, - Compilation compilation, ImmutableArray syntaxInfos) { if (syntaxInfos.Length == 0) @@ -191,14 +191,22 @@ private static void Execute( } var projects = new Dictionary(); + foreach (var project in syntaxInfos.OfType()) { projects[project.VariableName] = project; } + var processed = new HashSet(); var gateways = new List(); + foreach (var gatewayGroup in syntaxInfos.OfType().GroupBy(t => t.Name)) { + if (!processed.Add(gatewayGroup.Key)) + { + continue; + } + var gateway = new GatewayInfo(gatewayGroup.Key, gatewayGroup.First().TypeName); foreach (var projectLink in gatewayGroup) @@ -206,10 +214,10 @@ private static void Execute( if (projects.TryGetValue(projectLink.VariableName, out var project)) { gateway.Subgraphs.Add(new SubgraphInfo(project.Name, project.TypeName)); - gateways.Add(gateway); } } - + + gateways.Add(gateway); } if (gateways.Count == 0) @@ -219,8 +227,8 @@ private static void Execute( var code = StringBuilderPool.Get(); using var writer = new CodeWriter(code); + writer.WriteFileHeader(); - writer.WriteLine(); writer.Write(Resources.CliCode); writer.WriteLine(); writer.WriteLine(); @@ -249,48 +257,57 @@ private static void Execute( using (writer.IncreaseIndent()) { - writer.WriteIndentedLine("\"{}\",", gateway.Name); + writer.WriteIndentedLine("\"{0}\",", gateway.Name); + + var first = true; foreach (var project in gateway.Subgraphs) { - + if (first) + { + first = false; + } + else + { + writer.Write(","); + writer.WriteLine(); + } + + writer.WriteIndented( + "SubgraphInfo.Create<{0}>(\"{1}\", \"{2}\")", + project.TypeName, + project.Name, + project.Name); } - writer.WriteIndentedLine(""); - writer.WriteIndentedLine(""); - writer.WriteIndentedLine(""); - writer.WriteIndentedLine(""); + + writer.Write("),"); + writer.WriteLine(); } } } - + writer.WriteIndentedLine("]) { }"); } } - + writer.WriteIndentedLine("}"); } - + writer.WriteIndentedLine("}"); - context.AddSource("FusionGatewayConfiguration.g.cs", code.ToString()); + context.AddSource("FusionConfiguration.g.cs", code.ToString()); StringBuilderPool.Return(code); } -} -namespace HotChocolate.Fusion.Composition.Tooling -{ - file class GatewayList : List + private static void AddInitializationContent(IncrementalGeneratorPostInitializationContext context) { - public GatewayList() - : base( - [ - GatewayInfo.Create( - "gateway", - SubgraphInfo.Create("purchase-api", "purchaseApi"), - SubgraphInfo.Create("ordering-api", "orderingApi"), - SubgraphInfo.Create("catalog-api", "catalogApi"), - SubgraphInfo.Create("identity-api", "identityApi"), - SubgraphInfo.Create("basket-api", "basketApi")) - ]) { } + var code = StringBuilderPool.Get(); + using var writer = new CodeWriter(code); + + writer.WriteFileHeader(); + writer.Write(Resources.Extensions); + + context.AddSource("FusionExtensions.g.cs", code.ToString()); + StringBuilderPool.Return(code); } } \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriter.cs b/src/HotChocolate/Fusion/src/Composition.Analyzers/Helpers/CodeWriter.cs similarity index 100% rename from src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriter.cs rename to src/HotChocolate/Fusion/src/Composition.Analyzers/Helpers/CodeWriter.cs diff --git a/src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriterExtensions.cs b/src/HotChocolate/Fusion/src/Composition.Analyzers/Helpers/CodeWriterExtensions.cs similarity index 85% rename from src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriterExtensions.cs rename to src/HotChocolate/Fusion/src/Composition.Analyzers/Helpers/CodeWriterExtensions.cs index dfa0815ce57..dd20afd63ad 100644 --- a/src/HotChocolate/Fusion/src/Analyzers/Helpers/CodeWriterExtensions.cs +++ b/src/HotChocolate/Fusion/src/Composition.Analyzers/Helpers/CodeWriterExtensions.cs @@ -32,9 +32,6 @@ public static void WriteFileHeader(this CodeWriter writer) writer.WriteLine(); writer.WriteIndentedLine("#nullable enable"); writer.WriteLine(); - writer.WriteIndentedLine("using System;"); - writer.WriteIndentedLine("using System.Runtime.CompilerServices;"); - writer.WriteIndentedLine("using HotChocolate.Execution.Configuration;"); } public static CodeWriter WriteComment(this CodeWriter writer, string comment) diff --git a/src/HotChocolate/Fusion/src/Analyzers/Helpers/StringBuilderPool.cs b/src/HotChocolate/Fusion/src/Composition.Analyzers/Helpers/StringBuilderPool.cs similarity index 100% rename from src/HotChocolate/Fusion/src/Analyzers/Helpers/StringBuilderPool.cs rename to src/HotChocolate/Fusion/src/Composition.Analyzers/Helpers/StringBuilderPool.cs diff --git a/src/HotChocolate/Fusion/src/Analyzers/Helpers/SymbolExtensions.cs b/src/HotChocolate/Fusion/src/Composition.Analyzers/Helpers/SymbolExtensions.cs similarity index 100% rename from src/HotChocolate/Fusion/src/Analyzers/Helpers/SymbolExtensions.cs rename to src/HotChocolate/Fusion/src/Composition.Analyzers/Helpers/SymbolExtensions.cs diff --git a/src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.targets b/src/HotChocolate/Fusion/src/Composition.Analyzers/HotChocolate.Fusion.Analyzers.targets similarity index 100% rename from src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.targets rename to src/HotChocolate/Fusion/src/Composition.Analyzers/HotChocolate.Fusion.Analyzers.targets diff --git a/src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.csproj b/src/HotChocolate/Fusion/src/Composition.Analyzers/HotChocolate.Fusion.Composition.Analyzers.csproj similarity index 100% rename from src/HotChocolate/Fusion/src/Analyzers/HotChocolate.Fusion.Analyzers.csproj rename to src/HotChocolate/Fusion/src/Composition.Analyzers/HotChocolate.Fusion.Composition.Analyzers.csproj diff --git a/src/HotChocolate/Fusion/src/Analyzers/Models/GatewayClass.cs b/src/HotChocolate/Fusion/src/Composition.Analyzers/Models/GatewayClass.cs similarity index 100% rename from src/HotChocolate/Fusion/src/Analyzers/Models/GatewayClass.cs rename to src/HotChocolate/Fusion/src/Composition.Analyzers/Models/GatewayClass.cs diff --git a/src/HotChocolate/Fusion/src/Analyzers/Models/GatewayInfo.cs b/src/HotChocolate/Fusion/src/Composition.Analyzers/Models/GatewayInfo.cs similarity index 100% rename from src/HotChocolate/Fusion/src/Analyzers/Models/GatewayInfo.cs rename to src/HotChocolate/Fusion/src/Composition.Analyzers/Models/GatewayInfo.cs diff --git a/src/HotChocolate/Fusion/src/Analyzers/Models/ISyntaxInfo.cs b/src/HotChocolate/Fusion/src/Composition.Analyzers/Models/ISyntaxInfo.cs similarity index 100% rename from src/HotChocolate/Fusion/src/Analyzers/Models/ISyntaxInfo.cs rename to src/HotChocolate/Fusion/src/Composition.Analyzers/Models/ISyntaxInfo.cs diff --git a/src/HotChocolate/Fusion/src/Analyzers/Models/SubgraphClass.cs b/src/HotChocolate/Fusion/src/Composition.Analyzers/Models/SubgraphClass.cs similarity index 100% rename from src/HotChocolate/Fusion/src/Analyzers/Models/SubgraphClass.cs rename to src/HotChocolate/Fusion/src/Composition.Analyzers/Models/SubgraphClass.cs diff --git a/src/HotChocolate/Fusion/src/Analyzers/Models/SubgraphInfo.cs b/src/HotChocolate/Fusion/src/Composition.Analyzers/Models/SubgraphInfo.cs similarity index 100% rename from src/HotChocolate/Fusion/src/Analyzers/Models/SubgraphInfo.cs rename to src/HotChocolate/Fusion/src/Composition.Analyzers/Models/SubgraphInfo.cs diff --git a/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.Designer.cs b/src/HotChocolate/Fusion/src/Composition.Analyzers/Properties/Resources.Designer.cs similarity index 92% rename from src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.Designer.cs rename to src/HotChocolate/Fusion/src/Composition.Analyzers/Properties/Resources.Designer.cs index d74bd580a15..5ae122945da 100644 --- a/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.Designer.cs +++ b/src/HotChocolate/Fusion/src/Composition.Analyzers/Properties/Resources.Designer.cs @@ -50,5 +50,11 @@ internal static string CliCode { return ResourceManager.GetString("CliCode", resourceCulture); } } + + internal static string Extensions { + get { + return ResourceManager.GetString("Extensions", resourceCulture); + } + } } } diff --git a/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.resx b/src/HotChocolate/Fusion/src/Composition.Analyzers/Properties/Resources.resx similarity index 90% rename from src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.resx rename to src/HotChocolate/Fusion/src/Composition.Analyzers/Properties/Resources.resx index 9540f7ab4bb..0362e573398 100644 --- a/src/HotChocolate/Fusion/src/Analyzers/Properties/Resources.resx +++ b/src/HotChocolate/Fusion/src/Composition.Analyzers/Properties/Resources.resx @@ -595,11 +595,14 @@ namespace HotChocolate.Fusion.Composition.Tooling where TProject : IProjectMetadata, new() => new(name, new TProject().ProjectPath, projects); } -} +} + + + using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting { - public static class Extensions + internal static class FusionExtensions { public static DistributedApplication Compose(this DistributedApplication application) { @@ -607,13 +610,54 @@ namespace Aspire.Hosting if (options.Args is ["compose"]) { - FusionGatewayConfgHelper.ConfigureAsync().Wait(); + global::HotChocolate.Fusion.Composition.Tooling.FusionGatewayConfgHelper.ConfigureAsync().Wait(); Environment.Exit(0); } return application; } + + public static IResourceBuilder<FusionGatewayResource> AddFusionGateway<TProject>( + this IDistributedApplicationBuilder builder, + string name) + where TProject : IProjectMetadata, new() + => new FusionGatewayResourceBuilder(builder.AddProject<TProject>(name)); + + public static IResourceBuilder<FusionGatewayResource> WithSubgraph( + this IResourceBuilder<FusionGatewayResource> builder, + IResourceBuilder<ProjectResource> subgraphProject) + => builder.WithReference(subgraphProject.GetEndpoint("http")); + + public static IResourceBuilder<FusionGatewayResource> WithSubgraph( + this IResourceBuilder<FusionGatewayResource> builder, + EndpointReference subgraphEndpoint) + => builder.WithReference(subgraphEndpoint); } -} + + internal sealed class FusionGatewayResource(ProjectResource projectResource) + : Resource(projectResource.Name) + , IResourceWithEnvironment + , IResourceWithServiceDiscovery + { + public ProjectResource ProjectResource { get; } = projectResource; + } + + internal sealed class FusionGatewayResourceBuilder( + IResourceBuilder<ProjectResource> projectResourceBuilder) + : IResourceBuilder<FusionGatewayResource> + { + public IResourceBuilder<FusionGatewayResource> WithAnnotation<TAnnotation>(TAnnotation annotation) + where TAnnotation : IResourceAnnotation + { + projectResourceBuilder.WithAnnotation(annotation); + return this; + } + + public IDistributedApplicationBuilder ApplicationBuilder => projectResourceBuilder.ApplicationBuilder; + + public FusionGatewayResource Resource { get; } = new(projectResourceBuilder.Resource); + } +} + \ No newline at end of file diff --git a/src/HotChocolate/Fusion/src/Analyzers/Properties/launchSettings.json b/src/HotChocolate/Fusion/src/Composition.Analyzers/Properties/launchSettings.json similarity index 100% rename from src/HotChocolate/Fusion/src/Analyzers/Properties/launchSettings.json rename to src/HotChocolate/Fusion/src/Composition.Analyzers/Properties/launchSettings.json diff --git a/src/HotChocolate/Fusion/src/Analyzers/WellKnownTypeNames.cs b/src/HotChocolate/Fusion/src/Composition.Analyzers/WellKnownTypeNames.cs similarity index 100% rename from src/HotChocolate/Fusion/src/Analyzers/WellKnownTypeNames.cs rename to src/HotChocolate/Fusion/src/Composition.Analyzers/WellKnownTypeNames.cs diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj b/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/HotChocolate.Fusion.Composition.Analyzers.Tests.csproj similarity index 60% rename from src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj rename to src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/HotChocolate.Fusion.Composition.Analyzers.Tests.csproj index 6c6b47f9fa8..45101e72bd5 100644 --- a/src/HotChocolate/Fusion/test/Analyzers.Tests/HotChocolate.Fusion.Analyzers.Tests.csproj +++ b/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/HotChocolate.Fusion.Composition.Analyzers.Tests.csproj @@ -2,20 +2,11 @@ net8.0 - enable - enable - - false - true true - - - - diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/Program.cs b/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/Program.cs similarity index 60% rename from src/HotChocolate/Fusion/test/Analyzers.Tests/Program.cs rename to src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/Program.cs index f93248ffc84..62e13817bfe 100644 --- a/src/HotChocolate/Fusion/test/Analyzers.Tests/Program.cs +++ b/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/Program.cs @@ -1,3 +1,4 @@ + namespace HotChocolate.Fusion.Analyzers.Tests; public class Program @@ -61,7 +62,6 @@ public static void Foo(string[] args) .WithEnvironment("Identity__Url", identityHttpsEndpoint); // Fusion - /* builder .AddFusionGateway("gateway") .WithSubgraph(basketApi) @@ -70,51 +70,9 @@ public static void Foo(string[] args) .WithSubgraph(orderingApi) .WithSubgraph(purchaseApi) .WithEnvironment("Identity__Url", identityHttpsEndpoint); - */ + - builder.Build().Compose().Run(); + // builder.Build().Compose().Run(); } } -file static class FusionExtensions -{ - public static IResourceBuilder AddFusionGateway( - this IDistributedApplicationBuilder builder, - string name) - where TProject : IProjectMetadata, new() - => new FusionGatewayResourceBuilder(builder.AddProject(name)); - - public static IResourceBuilder WithSubgraph( - this IResourceBuilder builder, - IResourceBuilder subgraphProject) - => builder.WithReference(subgraphProject.GetEndpoint("http")); - - public static IResourceBuilder WithSubgraph( - this IResourceBuilder builder, - EndpointReference subgraphEndpoint) - => builder.WithReference(subgraphEndpoint); -} - -file class FusionGatewayResource(ProjectResource projectResource) - : Resource(projectResource.Name) - , IResourceWithEnvironment - , IResourceWithServiceDiscovery -{ - public ProjectResource ProjectResource { get; } = projectResource; -} - -file class FusionGatewayResourceBuilder( - IResourceBuilder projectResourceBuilder) - : IResourceBuilder -{ - public IResourceBuilder WithAnnotation(TAnnotation annotation) - where TAnnotation : IResourceAnnotation - { - projectResourceBuilder.WithAnnotation(annotation); - return this; - } - - public IDistributedApplicationBuilder ApplicationBuilder => projectResourceBuilder.ApplicationBuilder; - - public FusionGatewayResource Resource { get; } = new(projectResourceBuilder.Resource); -} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/Projects.cs b/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/Projects.cs similarity index 100% rename from src/HotChocolate/Fusion/test/Analyzers.Tests/Projects.cs rename to src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/Projects.cs diff --git a/src/HotChocolate/Fusion/test/Analyzers.Tests/Test.cs b/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/Test.txt similarity index 100% rename from src/HotChocolate/Fusion/test/Analyzers.Tests/Test.cs rename to src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/Test.txt diff --git a/src/HotChocolate/Fusion/test/Directory.Build.props b/src/HotChocolate/Fusion/test/Directory.Build.props index 036ac8b42b1..4290ce4eb6f 100644 --- a/src/HotChocolate/Fusion/test/Directory.Build.props +++ b/src/HotChocolate/Fusion/test/Directory.Build.props @@ -3,6 +3,7 @@ false + true enable enable false From 063dc4086597b5abaa59552071e1e2a12dc3113f Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 6 Mar 2024 16:14:09 -0800 Subject: [PATCH 5/5] edits --- .../HotChocolate.Fusion.Composition.Analyzers.Tests.csproj | 2 +- .../Fusion/test/Composition.Analyzers.Tests/Program.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/HotChocolate.Fusion.Composition.Analyzers.Tests.csproj b/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/HotChocolate.Fusion.Composition.Analyzers.Tests.csproj index 45101e72bd5..6d318b9ee74 100644 --- a/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/HotChocolate.Fusion.Composition.Analyzers.Tests.csproj +++ b/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/HotChocolate.Fusion.Composition.Analyzers.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/Program.cs b/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/Program.cs index 62e13817bfe..373ca42bff6 100644 --- a/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/Program.cs +++ b/src/HotChocolate/Fusion/test/Composition.Analyzers.Tests/Program.cs @@ -72,7 +72,7 @@ public static void Foo(string[] args) .WithEnvironment("Identity__Url", identityHttpsEndpoint); - // builder.Build().Compose().Run(); + builder.Build().Compose().Run(); } }