From f9a801ed18513a63b143182b2b5380d4b3f4f3c9 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Wed, 24 Jan 2024 21:30:30 +0200 Subject: [PATCH] Add subscription support to source generator (#1729) --- .../05-samples-sg-subscriptions.md | 13 + docs/4-code-generator/nav.md | 3 +- .../GraphQL.Samples.SG.InputType/Program.cs | 1 - .../GraphQL.Samples.SG.Subscription.csproj | 19 + .../Program.cs | 55 +++ .../Properties/launchSettings.json | 14 + .../appsettings.Development.json | 8 + .../appsettings.json | 9 + .../Internal/EquatableArray.cs | 115 ++++++ .../Internal/HashCode.cs | 384 ++++++++++++++++++ .../ObjectMethodDefinition.cs | 65 ++- .../ObjectTypeEmitter.cs | 179 ++++++-- .../ObjectTypeParser.cs | 39 +- .../TypeHelper.cs | 10 +- .../ValueResolution/SubscriberContext.cs | 18 +- tanka-graphql.sln | 17 +- .../ObjectGeneratorFacts.cs | 100 +++++ ...e_name#TestsPersonController.g.verified.cs | 22 +- ...pe_name#TestsQueryController.g.verified.cs | 27 +- ...o_namespace#PersonController.g.verified.cs | 23 +- ...no_namespace#QueryController.g.verified.cs | 28 +- ...orFacts.HelloWorld#InputType.g.verified.cs | 12 + ...rFacts.HelloWorld#ObjectType.g.verified.cs | 39 ++ ...SourceGeneratedTypesExtensions.verified.cs | 19 + ...loWorld#TestsQueryController.g.verified.cs | 31 ++ ...loWorld#TestsWorldController.g.verified.cs | 36 ++ ...om_AsyncEnumerable#InputType.g.verified.cs | 12 + ...m_AsyncEnumerable#ObjectType.g.verified.cs | 39 ++ ...SourceGeneratedTypesExtensions.verified.cs | 18 + ...merable#TestsWorldController.g.verified.cs | 37 ++ ...esolver#TestsQueryController.g.verified.cs | 27 +- ..._method_subscriber#InputType.g.verified.cs | 12 + ...method_subscriber#ObjectType.g.verified.cs | 39 ++ ...SourceGeneratedTypesExtensions.verified.cs | 18 + ...#TestsSubscriptionController.g.verified.cs | 37 ++ ...esolver#TestsQueryController.g.verified.cs | 22 +- 36 files changed, 1389 insertions(+), 158 deletions(-) create mode 100644 docs/4-code-generator/05-samples-sg-subscriptions.md create mode 100644 samples/GraphQL.Samples.SG.Subscription/GraphQL.Samples.SG.Subscription.csproj create mode 100644 samples/GraphQL.Samples.SG.Subscription/Program.cs create mode 100644 samples/GraphQL.Samples.SG.Subscription/Properties/launchSettings.json create mode 100644 samples/GraphQL.Samples.SG.Subscription/appsettings.Development.json create mode 100644 samples/GraphQL.Samples.SG.Subscription/appsettings.json create mode 100644 src/GraphQL.Server.SourceGenerators/Internal/EquatableArray.cs create mode 100644 src/GraphQL.Server.SourceGenerators/Internal/HashCode.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#InputType.g.verified.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#ObjectType.g.verified.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#Tests.SourceGeneratedTypesExtensions.verified.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#TestsQueryController.g.verified.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#TestsWorldController.g.verified.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#InputType.g.verified.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#ObjectType.g.verified.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#Tests.SourceGeneratedTypesExtensions.verified.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#TestsWorldController.g.verified.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#InputType.g.verified.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#ObjectType.g.verified.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#Tests.SourceGeneratedTypesExtensions.verified.cs create mode 100644 tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#TestsSubscriptionController.g.verified.cs diff --git a/docs/4-code-generator/05-samples-sg-subscriptions.md b/docs/4-code-generator/05-samples-sg-subscriptions.md new file mode 100644 index 000000000..4e0747c42 --- /dev/null +++ b/docs/4-code-generator/05-samples-sg-subscriptions.md @@ -0,0 +1,13 @@ +## Subscriptions + +Code generator will generate object definition and extensions methods for adding subscription +types to the schema. Generator will generate a subscriber for each `IAsyncEnumerable` method +of a class with `[ObjectType]` attribute. You can use provided extension method to add the +subscription type, subscribers and resolver to the schema. + + +### Tanka.GraphQL.Samples.SG.Subscription + +```csharp +#include::xref://samples:GraphQL.Samples.SG.Subscription/Program.cs +``` diff --git a/docs/4-code-generator/nav.md b/docs/4-code-generator/nav.md index 68fed406f..1aa9b1b07 100644 --- a/docs/4-code-generator/nav.md +++ b/docs/4-code-generator/nav.md @@ -1,4 +1,5 @@ - [Installation & basics](xref://01-samples-sg-basic.md) - [Arguments](xref://02-samples-sg-arguments.md) - [Namespace](xref://03-samples-sg-namespace.md) -- [Dependency injection](xref://04-samples-sg-services.md) \ No newline at end of file +- [Dependency injection](xref://04-samples-sg-services.md) +- [Subscriptions](xref://05-samples-sg-subscriptions.md) \ No newline at end of file diff --git a/samples/GraphQL.Samples.SG.InputType/Program.cs b/samples/GraphQL.Samples.SG.InputType/Program.cs index 7457a66ca..403e4187d 100644 --- a/samples/GraphQL.Samples.SG.InputType/Program.cs +++ b/samples/GraphQL.Samples.SG.InputType/Program.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using Microsoft.AspNetCore.Mvc; using Tanka.GraphQL; diff --git a/samples/GraphQL.Samples.SG.Subscription/GraphQL.Samples.SG.Subscription.csproj b/samples/GraphQL.Samples.SG.Subscription/GraphQL.Samples.SG.Subscription.csproj new file mode 100644 index 000000000..c86cd0d8f --- /dev/null +++ b/samples/GraphQL.Samples.SG.Subscription/GraphQL.Samples.SG.Subscription.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/samples/GraphQL.Samples.SG.Subscription/Program.cs b/samples/GraphQL.Samples.SG.Subscription/Program.cs new file mode 100644 index 000000000..da40d1ca0 --- /dev/null +++ b/samples/GraphQL.Samples.SG.Subscription/Program.cs @@ -0,0 +1,55 @@ +using System.Runtime.CompilerServices; + +using Tanka.GraphQL.Server; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +builder.AddTankaGraphQL() + .AddHttp() + .AddWebSockets() + .AddSchemaOptions("Default", options => + { + // This extension point is used by the generator to add + // type controllers + options.AddGeneratedTypes(types => + { + // Add generated controllers + types + .AddQueryController() + .AddSubscriptionController(); + }); + }); + +WebApplication app = builder.Build(); +app.UseWebSockets(); + +app.MapTankaGraphQL("/graphql", "Default"); +app.MapGraphiQL("/graphql/ui"); +app.Run(); + +[ObjectType] +public static class Subscription +{ + /// + /// This is subscription field producing random integers of count between from and to + /// + /// + public static async IAsyncEnumerable Random(int from, int to, int count, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var r = new Random(); + + for (var i = 0; i < count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return r.Next(from, to); + await Task.Delay(500, cancellationToken); + } + } +} + +[ObjectType] +public static class Query +{ + // this is required as the graphiql will error without a query field + public static string Hello() => "Hello World!"; +} \ No newline at end of file diff --git a/samples/GraphQL.Samples.SG.Subscription/Properties/launchSettings.json b/samples/GraphQL.Samples.SG.Subscription/Properties/launchSettings.json new file mode 100644 index 000000000..cd1675f07 --- /dev/null +++ b/samples/GraphQL.Samples.SG.Subscription/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "GraphQL.Samples.SG.Subscription": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "https://localhost:7239/graphql/ui", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:7239", + "dotnetRunMessages": true + } + } +} \ No newline at end of file diff --git a/samples/GraphQL.Samples.SG.Subscription/appsettings.Development.json b/samples/GraphQL.Samples.SG.Subscription/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/samples/GraphQL.Samples.SG.Subscription/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/GraphQL.Samples.SG.Subscription/appsettings.json b/samples/GraphQL.Samples.SG.Subscription/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/samples/GraphQL.Samples.SG.Subscription/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/GraphQL.Server.SourceGenerators/Internal/EquatableArray.cs b/src/GraphQL.Server.SourceGenerators/Internal/EquatableArray.cs new file mode 100644 index 000000000..b94942835 --- /dev/null +++ b/src/GraphQL.Server.SourceGenerators/Internal/EquatableArray.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Tanka.GraphQL.Server.SourceGenerators.Internal; + +public static class EquatableArrayExtensions +{ + public static EquatableArray ToEquatableArray(this IEnumerable array) + where T : IEquatable + { + return new(array.ToArray()); + + } +} + +/// +/// Origin https://github.com/andrewlock/NetEscapades.EnumGenerators +/// An immutable, equatable array. This is equivalent to but with value equality support. +/// +/// The type of values in the array. +public readonly struct EquatableArray : IEquatable>, IEnumerable + where T : IEquatable +{ + /// + /// The underlying array. + /// + private readonly T[]? _array; + + /// + /// Creates a new instance. + /// + /// The input to wrap. + public EquatableArray(T[] array) + { + _array = array; + } + + /// + public bool Equals(EquatableArray array) + { + return AsSpan().SequenceEqual(array.AsSpan()); + } + + /// + public override bool Equals(object? obj) + { + return obj is EquatableArray array && Equals(this, array); + } + + /// + public override int GetHashCode() + { + if (_array is not T[] array) + { + return 0; + } + + HashCode hashCode = default; + + foreach (T item in array) + { + hashCode.Add(item); + } + + return hashCode.ToHashCode(); + } + + /// + /// Returns a wrapping the current items. + /// + /// A wrapping the current items. + public ReadOnlySpan AsSpan() + { + return _array.AsSpan(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)(_array ?? Array.Empty())).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)(_array ?? Array.Empty())).GetEnumerator(); + } + + public int Count => _array?.Length ?? 0; + + /// + /// Checks whether two values are the same. + /// + /// The first value. + /// The second value. + /// Whether and are equal. + public static bool operator ==(EquatableArray left, EquatableArray right) + { + return left.Equals(right); + } + + /// + /// Checks whether two values are not the same. + /// + /// The first value. + /// The second value. + /// Whether and are not equal. + public static bool operator !=(EquatableArray left, EquatableArray right) + { + return !left.Equals(right); + } +} \ No newline at end of file diff --git a/src/GraphQL.Server.SourceGenerators/Internal/HashCode.cs b/src/GraphQL.Server.SourceGenerators/Internal/HashCode.cs new file mode 100644 index 000000000..52e96f465 --- /dev/null +++ b/src/GraphQL.Server.SourceGenerators/Internal/HashCode.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Tanka.GraphQL.Server.SourceGenerators.Internal; + +/// +/// Origin Shamelessly stolen from https://github.com/andrewlock/NetEscapades.EnumGenerators +/// Polyfill for .NET 6 HashCode +/// +internal struct HashCode +{ + private static readonly uint s_seed = GenerateGlobalSeed(); + + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private uint _v1, _v2, _v3, _v4; + private uint _queue1, _queue2, _queue3; + private uint _length; + + private static uint GenerateGlobalSeed() + { + var buffer = new byte[sizeof(uint)]; + new Random().NextBytes(buffer); + return BitConverter.ToUInt32(buffer, 0); + } + + public static int Combine(T1 value1) + { + // Provide a way of diffusing bits from something with a limited + // input hash space. For example, many enums only have a few + // possible hashes, only using the bottom few bits of the code. Some + // collections are built on the assumption that hashes are spread + // over a larger space, so diffusing the bits may help the + // collection work more efficiently. + + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 4; + + hash = QueueRound(hash, hc1); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 8; + + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 12; + + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + hash = QueueRound(hash, hc3); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 16; + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 20; + + hash = QueueRound(hash, hc5); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 24; + + hash = QueueRound(hash, hc5); + hash = QueueRound(hash, hc6); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + uint hc7 = (uint)(value7?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 28; + + hash = QueueRound(hash, hc5); + hash = QueueRound(hash, hc6); + hash = QueueRound(hash, hc7); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + uint hc7 = (uint)(value7?.GetHashCode() ?? 0); + uint hc8 = (uint)(value8?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + v1 = Round(v1, hc5); + v2 = Round(v2, hc6); + v3 = Round(v3, hc7); + v4 = Round(v4, hc8); + + uint hash = MixState(v1, v2, v3, v4); + hash += 32; + + hash = MixFinal(hash); + return (int)hash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) + { + v1 = s_seed + Prime1 + Prime2; + v2 = s_seed + Prime2; + v3 = s_seed; + v4 = s_seed - Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Round(uint hash, uint input) + { + return RotateLeft(hash + input * Prime2, 13) * Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint QueueRound(uint hash, uint queuedValue) + { + return RotateLeft(hash + queuedValue * Prime3, 17) * Prime4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixState(uint v1, uint v2, uint v3, uint v4) + { + return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + } + + private static uint MixEmptyState() + { + return s_seed + Prime5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + return hash; + } + + public void Add(T value) + { + Add(value?.GetHashCode() ?? 0); + } + + public void Add(T value, IEqualityComparer? comparer) + { + Add(value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); + } + + private void Add(int value) + { + // The original xxHash works as follows: + // 0. Initialize immediately. We can't do this in a struct (no + // default ctor). + // 1. Accumulate blocks of length 16 (4 uints) into 4 accumulators. + // 2. Accumulate remaining blocks of length 4 (1 uint) into the + // hash. + // 3. Accumulate remaining blocks of length 1 into the hash. + + // There is no need for #3 as this type only accepts ints. _queue1, + // _queue2 and _queue3 are basically a buffer so that when + // ToHashCode is called we can execute #2 correctly. + + // We need to initialize the xxHash32 state (_v1 to _v4) lazily (see + // #0) nd the last place that can be done if you look at the + // original code is just before the first block of 16 bytes is mixed + // in. The xxHash32 state is never used for streams containing fewer + // than 16 bytes. + + // To see what's really going on here, have a look at the Combine + // methods. + + uint val = (uint)value; + + // Storing the value of _length locally shaves of quite a few bytes + // in the resulting machine code. + uint previousLength = _length++; + uint position = previousLength % 4; + + // Switch can't be inlined. + + if (position == 0) + _queue1 = val; + else if (position == 1) + _queue2 = val; + else if (position == 2) + _queue3 = val; + else // position == 3 + { + if (previousLength == 3) + Initialize(out _v1, out _v2, out _v3, out _v4); + + _v1 = Round(_v1, _queue1); + _v2 = Round(_v2, _queue2); + _v3 = Round(_v3, _queue3); + _v4 = Round(_v4, val); + } + } + + public int ToHashCode() + { + // Storing the value of _length locally shaves of quite a few bytes + // in the resulting machine code. + uint length = _length; + + // position refers to the *next* queue position in this method, so + // position == 1 means that _queue1 is populated; _queue2 would have + // been populated on the next call to Add. + uint position = length % 4; + + // If the length is less than 4, _v1 to _v4 don't contain anything + // yet. xxHash32 treats this differently. + + uint hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4); + + // _length is incremented once per Add(Int32) and is therefore 4 + // times too small (xxHash length is in bytes, not ints). + + hash += length * 4; + + // Mix what remains in the queue + + // Switch can't be inlined right now, so use as few branches as + // possible by manually excluding impossible scenarios (position > 1 + // is always false if position is not > 0). + if (position > 0) + { + hash = QueueRound(hash, _queue1); + if (position > 1) + { + hash = QueueRound(hash, _queue2); + if (position > 2) + hash = QueueRound(hash, _queue3); + } + } + + hash = MixFinal(hash); + return (int)hash; + } + +#pragma warning disable 0809 + // Obsolete member 'memberA' overrides non-obsolete member 'memberB'. + // Disallowing GetHashCode and Equals is by design + + // * We decided to not override GetHashCode() to produce the hash code + // as this would be weird, both naming-wise as well as from a + // behavioral standpoint (GetHashCode() should return the object's + // hash code, not the one being computed). + + // * Even though ToHashCode() can be called safely multiple times on + // this implementation, it is not part of the contract. If the + // implementation has to change in the future we don't want to worry + // about people who might have incorrectly used this type. + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => throw new NotSupportedException("Hash code not supported"); + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) => throw new NotSupportedException("Equality not supported"); +#pragma warning restore 0809 + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateLeft(uint value, int offset) + => (value << offset) | (value >> (32 - offset)); +} \ No newline at end of file diff --git a/src/GraphQL.Server.SourceGenerators/ObjectMethodDefinition.cs b/src/GraphQL.Server.SourceGenerators/ObjectMethodDefinition.cs index e250828b1..aa837819b 100644 --- a/src/GraphQL.Server.SourceGenerators/ObjectMethodDefinition.cs +++ b/src/GraphQL.Server.SourceGenerators/ObjectMethodDefinition.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using Tanka.GraphQL.Server.SourceGenerators.Internal; namespace Tanka.GraphQL.Server.SourceGenerators; @@ -6,13 +6,72 @@ public record ObjectMethodDefinition { public string Name { get; set; } - public bool IsAsync { get; set; } + public MethodType Type { get; set; } - public List Parameters { get; set; } = new List(); + public EquatableArray Parameters { get; set; } = new(); public string ReturnType { get; init; } public string ClosestMatchingGraphQLTypeName { get; set; } public bool IsStatic { get; set; } + + /// + /// Should await the method call? + /// + public bool IsAsync => Type switch + { + MethodType.Void => false, + MethodType.EnumerableT => false, + MethodType.T => false, + MethodType.Unknown => false, + _ => true + }; + + public bool IsSubscription => Type is MethodType.AsyncEnumerableOfT; +} + +public enum MethodType +{ + /// + /// Task Method(...) + /// + Task, + + /// + /// ValueTask Method(...) + /// + ValueTask, + + /// + /// Task<T> Method(...) + /// + TaskOfT, + + /// + /// ValueTask<T> Method(...) + /// + ValueTaskOfT, + + /// + /// IAsyncEnumerable<T> Method(...) + /// + AsyncEnumerableOfT, + + /// + /// void Method(...) + /// + Void, + + /// + /// T Method(...) + /// + T, + + /// + /// IEnumerable<T> Method(...) + /// + EnumerableT, + + Unknown } \ No newline at end of file diff --git a/src/GraphQL.Server.SourceGenerators/ObjectTypeEmitter.cs b/src/GraphQL.Server.SourceGenerators/ObjectTypeEmitter.cs index d4efdeafc..1bc248b09 100644 --- a/src/GraphQL.Server.SourceGenerators/ObjectTypeEmitter.cs +++ b/src/GraphQL.Server.SourceGenerators/ObjectTypeEmitter.cs @@ -1,16 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace Tanka.GraphQL.Server.SourceGenerators; public class ObjectTypeEmitter { public const string ObjectTypeTemplate = """ + /// + #nullable enable {{usings}} {{namespace}} @@ -29,16 +30,31 @@ public static class {{name}}ControllerExtensions { builder.Builder.Configure(options => options.Builder.Add( "{{name}}", - new FieldsWithResolvers() - { - {{fieldsWithResolvers}} - })); + {{fields}} + )); return builder; } } + #nullable restore """; + public static string FieldsWithResolversTemplate(string fieldsWithResolvers) => + $$""" + new FieldsWithResolvers() + { + {{fieldsWithResolvers}} + } + """; + + public static string FieldsWithSubscribersTemplate(string fieldsWithSubscribers) => + $$""" + new FieldsWithSubscribers() + { + {{fieldsWithSubscribers}} + } + """; + public const string FieldWithResolverTemplate = """ { "{{fieldName}}: {{fieldType}}", {{resolverMethod}} } """; @@ -71,8 +87,8 @@ public static void Emit( { string properties = EmitProperties(definition); string methods = EmitMethods(definition); - string fieldsWithResolvers = EmitFieldsWithResolvers(definition); - + string fields = EmitFields(definition); + var builder = new StringBuilder(); string ns = string.IsNullOrEmpty(definition.Namespace) ? "" : $"{definition.Namespace}"; @@ -82,10 +98,63 @@ public static void Emit( .Replace("{{methods}}", methods) .Replace("{{namespace}}", string.IsNullOrEmpty(ns) ? "" : $"namespace {ns};") .Replace("{{name}}", definition.TargetType) - .Replace("{{fieldsWithResolvers}}", fieldsWithResolvers) + .Replace("{{fields}}", fields) ); - context.AddSource($"{ns}{definition.TargetType}Controller.g.cs", builder.ToString()); + var sourceText = CSharpSyntaxTree.ParseText(builder.ToString()).GetRoot().NormalizeWhitespace().ToFullString(); + context.AddSource($"{ns}{definition.TargetType}Controller.g.cs", sourceText); + } + + private static string EmitFields(ObjectControllerDefinition definition) + { + var builder = new IndentedStringBuilder(); + string fieldsWithResolvers = EmitFieldsWithResolvers(definition); + builder.AppendLine(FieldsWithResolversTemplate(fieldsWithResolvers)); + + if (definition.Methods.Any(m => m.IsSubscription)) + { + builder.AppendLine(","); + string fieldsWithSubscribers = EmitFieldsWithSubscribers(definition); + builder.AppendLine(FieldsWithSubscribersTemplate(fieldsWithSubscribers)); + } + + return builder.ToString(); + } + + private static string EmitFieldsWithSubscribers(ObjectControllerDefinition definition) + { + var builder = new IndentedStringBuilder(); + var subscriptionMethods = definition.Methods + .Where(m => m.IsSubscription + ).ToList(); + + for (var index = 0; index < subscriptionMethods.Count; index++) + { + ObjectMethodDefinition method = subscriptionMethods[index]; + + string fieldName = JsonNamingPolicy.CamelCase.ConvertName(method.Name); + string fieldType = method.ClosestMatchingGraphQLTypeName; + var fieldArguments = method.Parameters + .Where(p => p.FromArguments == true || p.IsPrimitive) + .Select(a => $"{a.Name}: {a.ClosestMatchingGraphQLTypeName}") + .ToList(); + + string fieldDefinition = + fieldArguments.Any() ? $"{fieldName}({string.Join(", ", fieldArguments)})" : fieldName; + + builder.Append(FieldWithResolverTemplate + .Replace("{{fieldName}}", fieldDefinition) + .Replace("{{fieldType}}", fieldType) + .Replace("{{resolverMethod}}", $"{definition.TargetType}Controller.{method.Name}") + ); + + if (definition.Methods.Count > 1 && index < definition.Methods.Count - 1) + builder.AppendLine(","); + else + builder.AppendLine(); + } + + return builder.ToString(); } private static string GetUsings(IReadOnlyList usings) @@ -148,10 +217,11 @@ private static string EmitFieldsWithResolvers(ObjectControllerDefinition definit string fieldDefinition = fieldArguments.Any() ? $"{fieldName}({string.Join(", ", fieldArguments)})" : fieldName; + string suffix = method.IsSubscription ? "Resolver" : string.Empty; builder.Append(FieldWithResolverTemplate .Replace("{{fieldName}}", fieldDefinition) .Replace("{{fieldType}}", fieldType) - .Replace("{{resolverMethod}}", $"{definition.TargetType}Controller.{method.Name}") + .Replace("{{resolverMethod}}", $"{definition.TargetType}Controller.{method.Name}{suffix}") ); if (definition.Methods.Count > 1 && index < definition.Methods.Count - 1) @@ -172,37 +242,68 @@ private static string EmitMethods(ObjectControllerDefinition definition) builder.IncrementIndent(); foreach (ObjectMethodDefinition method in definition.Methods) { - bool isAsync = method.IsAsync; - string asyncPrefix = isAsync ? "async " : string.Empty; - builder.AppendLine($"public static {asyncPrefix}ValueTask {method.Name}(ResolverContext context)"); - builder.AppendLine("{"); + EmitResolverMethods(builder, definition, method); + } - builder.IncrementIndent(); + builder.DecrementIndent(); + return builder.ToString(); + } - if (!method.IsStatic) - builder.AppendLine($"var objectValue = ({definition.TargetType})context.ObjectValue;"); + private static void EmitResolverMethods(IndentedStringBuilder builder, ObjectControllerDefinition definition, ObjectMethodDefinition method) + { + bool isAsync = method.IsAsync; + bool isSubscription = method.IsSubscription; + string asyncPrefix = (isAsync && !isSubscription) ? "async " : string.Empty; - string parameters = GetParameters(method); + string parameters = GetParameters(method); - if (!method.IsStatic) - builder.AppendLine(isAsync - ? $"context.ResolvedValue = await objectValue.{method.Name}{parameters}" - : $"context.ResolvedValue = objectValue.{method.Name}{parameters}"); - else - builder.AppendLine(isAsync - ? $"context.ResolvedValue = await {definition.TargetType}.{method.Name}{parameters}" - : $"context.ResolvedValue = {definition.TargetType}.{method.Name}{parameters}"); + string suffix = method.IsSubscription ? "Resolver" : string.Empty; + builder.AppendLine($"public static {asyncPrefix}ValueTask {method.Name}{suffix}(ResolverContext context)"); + builder.AppendLine("{"); + + builder.IncrementIndent(); + + if (isSubscription) + { + builder.AppendLine("context.ResolvedValue = context.ObjectValue;"); + builder.AppendLine("return default;"); + } + else + { + + var instanceOrStatic = method.IsStatic + ? definition.TargetType + : $"(({definition.TargetType})context.ObjectValue)"; + + builder.AppendLine(isAsync + ? $"context.ResolvedValue = await {instanceOrStatic}.{method.Name}{parameters};" + : $"context.ResolvedValue = {instanceOrStatic}.{method.Name}{parameters};"); if (!isAsync) builder.AppendLine("return default;"); + } + + builder.DecrementIndent(); + builder.AppendLine("}"); + + if (isSubscription) + { + builder.AppendLine($"public static ValueTask {method.Name}(SubscriberContext context, CancellationToken cancellationToken)"); + builder.AppendLine("{"); + builder.IncrementIndent(); + + var instanceOrStatic = method.IsStatic + ? definition.TargetType + : $"(({definition.TargetType})context.ObjectValue)"; + builder.AppendLine($"context.SetResult({instanceOrStatic}.{method.Name}{parameters});"); + + builder.AppendLine("return default;"); builder.DecrementIndent(); builder.AppendLine("}"); - builder.AppendLine(); } - builder.DecrementIndent(); - return builder.ToString(); + builder.AppendLine(); } /// @@ -248,20 +349,30 @@ private static string EmitProperties(ObjectControllerDefinition definition) private static string GetParameters(ObjectMethodDefinition method) { - if (!method.Parameters.Any()) return "();"; + if (!method.Parameters.Any()) return "()"; + bool isSubscription = method.IsSubscription; var builder = new IndentedStringBuilder(); builder.AppendLine("("); builder.IndentCount = 5; + var parameters = method.Parameters.AsSpan(); for (var index = 0; index < method.Parameters.Count; index++) { - ParameterDefinition parameter = method.Parameters[index]; + ParameterDefinition parameter = parameters[index]; if (parameter.Type.EndsWith("ResolverContext")) { builder.Append("context"); } + else if(parameter.Type.EndsWith("SubscriberContext")) + { + builder.Append("context"); + } + else if (parameter.Type.EndsWith("CancellationToken")) + { + builder.AppendLine(isSubscription ? "cancellationToken" : "context.RequestAborted"); + } else if (parameter.Type.EndsWith("IServiceProvider")) { builder.Append("context.RequestServices"); @@ -301,7 +412,7 @@ private static string GetParameters(ObjectMethodDefinition method) builder.AppendLine(); } - builder.AppendLine(");"); + builder.AppendLine(")"); return builder.ToString(); } } \ No newline at end of file diff --git a/src/GraphQL.Server.SourceGenerators/ObjectTypeParser.cs b/src/GraphQL.Server.SourceGenerators/ObjectTypeParser.cs index 38652831c..0b4302d6b 100644 --- a/src/GraphQL.Server.SourceGenerators/ObjectTypeParser.cs +++ b/src/GraphQL.Server.SourceGenerators/ObjectTypeParser.cs @@ -5,6 +5,8 @@ using Microsoft.CodeAnalysis.CSharp; using System; +using Tanka.GraphQL.Server.SourceGenerators.Internal; + namespace Tanka.GraphQL.Server.SourceGenerators { internal class ObjectTypeParser @@ -66,7 +68,7 @@ private static (List Properties, List p.Type is not null) .Select(p => new ParameterDefinition() @@ -78,7 +80,7 @@ private static (List Properties, List Properties, List MethodType.Task, + { Identifier.ValueText: "ValueTask" } => MethodType.ValueTask, + _ => MethodType.T + }; + } + + if (returnType is GenericNameSyntax namedTypeSyntax) + { + return namedTypeSyntax switch + { + + { Identifier.ValueText: "Task", TypeArgumentList.Arguments: not [] } => MethodType.TaskOfT, + { Identifier.ValueText: "ValueTask", TypeArgumentList.Arguments: not [] } => MethodType.ValueTaskOfT, + { Identifier.ValueText: "IAsyncEnumerable", TypeArgumentList.Arguments: not [] } => MethodType.AsyncEnumerableOfT, + { Identifier.ValueText: "IEnumerable", TypeArgumentList.Arguments: not [] } => MethodType.EnumerableT, + _ => MethodType.Unknown + }; + } + + return MethodType.Unknown; + } + private static string GetClosestMatchingGraphQLTypeName(SemanticModel model, TypeSyntax typeSyntax) { var typeSymbol = model.GetTypeInfo(typeSyntax).Type; diff --git a/src/GraphQL.Server.SourceGenerators/TypeHelper.cs b/src/GraphQL.Server.SourceGenerators/TypeHelper.cs index 91b003f62..dc2f4c111 100644 --- a/src/GraphQL.Server.SourceGenerators/TypeHelper.cs +++ b/src/GraphQL.Server.SourceGenerators/TypeHelper.cs @@ -10,11 +10,6 @@ namespace Tanka.GraphQL.Server.SourceGenerators; public class TypeHelper { - public static string GetGraphQLTypeName(TypeSyntax typeSyntax) - { - return null; - } - public static string GetGraphQLTypeName(ITypeSymbol typeSymbol) { // Handle arrays @@ -23,6 +18,11 @@ public static string GetGraphQLTypeName(ITypeSymbol typeSymbol) return $"[{GetGraphQLTypeName(arrayTypeSymbol.ElementType)}]"; } + if (typeSymbol is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.Name: "IAsyncEnumerable"} asyncEnumerable) + { + return GetGraphQLTypeName(asyncEnumerable.TypeArguments[0]); + } + if (typeSymbol is not { SpecialType: SpecialType.System_String }) { var ienumerableT = typeSymbol diff --git a/src/GraphQL/ValueResolution/SubscriberContext.cs b/src/GraphQL/ValueResolution/SubscriberContext.cs index 04d3c1038..f52a16a3f 100644 --- a/src/GraphQL/ValueResolution/SubscriberContext.cs +++ b/src/GraphQL/ValueResolution/SubscriberContext.cs @@ -1,6 +1,22 @@ -namespace Tanka.GraphQL.ValueResolution; +using System.Diagnostics; + +namespace Tanka.GraphQL.ValueResolution; public class SubscriberContext : ResolverContextBase { public IAsyncEnumerable? ResolvedValue { get; set; } + + public void SetResult(IAsyncEnumerable stream) + { + ResolvedValue = Cast(stream); + } + + [DebuggerStepThrough] + private async IAsyncEnumerable Cast(IAsyncEnumerable source) + { + await foreach (var item in source) + { + yield return item; + } + } } \ No newline at end of file diff --git a/tanka-graphql.sln b/tanka-graphql.sln index fe59c7712..e93d26167 100644 --- a/tanka-graphql.sln +++ b/tanka-graphql.sln @@ -74,10 +74,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Samples.SG.Services EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Samples.SG.Arguments", "samples\GraphQL.Samples.SG.Arguments\GraphQL.Samples.SG.Arguments.csproj", "{4A12194D-8289-462C-94B8-8ABDE2D8283A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Extensions.Experimental.Tests", "tests\GraphQL.Extensions.Experimental.Tests\GraphQL.Extensions.Experimental.Tests.csproj", "{4F85C0B5-2F59-46AE-993E-4719B2C954B6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Extensions.Experimental.Tests", "tests\GraphQL.Extensions.Experimental.Tests\GraphQL.Extensions.Experimental.Tests.csproj", "{4F85C0B5-2F59-46AE-993E-4719B2C954B6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Extensions.Experimental", "src\GraphQL.Extensions.Experimental\GraphQL.Extensions.Experimental.csproj", "{35D039EE-6718-43E2-83CD-00ACA4644FB0}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Samples.SG.Subscription", "samples\GraphQL.Samples.SG.Subscription\GraphQL.Samples.SG.Subscription.csproj", "{F15DEF44-8422-4CE0-9DF0-21B33E5858A8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -388,6 +390,18 @@ Global {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Release|x64.Build.0 = Release|Any CPU {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Release|x86.ActiveCfg = Release|Any CPU {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Release|x86.Build.0 = Release|Any CPU + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8}.Debug|x64.ActiveCfg = Debug|Any CPU + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8}.Debug|x64.Build.0 = Debug|Any CPU + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8}.Debug|x86.ActiveCfg = Debug|Any CPU + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8}.Debug|x86.Build.0 = Debug|Any CPU + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8}.Release|Any CPU.Build.0 = Release|Any CPU + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8}.Release|x64.ActiveCfg = Release|Any CPU + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8}.Release|x64.Build.0 = Release|Any CPU + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8}.Release|x86.ActiveCfg = Release|Any CPU + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -402,6 +416,7 @@ Global {AA03AB46-FF43-49AC-BEE2-FEC5A7BB8CDE} = {B9BE1B74-A36A-4A10-9BCE-E5EFDF6D15A8} {E136499A-90C7-47A9-8A25-EF2FCB1C0A24} = {B9BE1B74-A36A-4A10-9BCE-E5EFDF6D15A8} {4A12194D-8289-462C-94B8-8ABDE2D8283A} = {B9BE1B74-A36A-4A10-9BCE-E5EFDF6D15A8} + {F15DEF44-8422-4CE0-9DF0-21B33E5858A8} = {B9BE1B74-A36A-4A10-9BCE-E5EFDF6D15A8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6EC1BBAB-620C-44FB-A12E-E68F69D689B6} diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/ObjectGeneratorFacts.cs b/tests/GraphQL.Server.SourceGenerators.Tests/ObjectGeneratorFacts.cs index fbcc5ddea..493374a24 100644 --- a/tests/GraphQL.Server.SourceGenerators.Tests/ObjectGeneratorFacts.cs +++ b/tests/GraphQL.Server.SourceGenerators.Tests/ObjectGeneratorFacts.cs @@ -21,6 +21,31 @@ public static class Query return TestHelper.Verify(source); } + [Fact] + public Task StaticClass_Generate_method_subscriber() + { + var source = """ + using Tanka.GraphQL.Server; + + namespace Tests; + + [ObjectType] + public static class Subscription + { + public static IAsyncEnumerable Random(int from, int to, CancellationToken cancellationToken) + { + foreach(var i in Enumerable.Range(from, to)) + { + yield return i; + await Task.Delay(i*100); + } + } + } + """; + + return TestHelper.Verify(source); + } + [Fact] public Task Generate_ObjectType_type_name() { @@ -84,4 +109,79 @@ public static class Query return TestHelper.Verify(source); } + + [Fact] + public Task HelloWorld() + { + var source = """ + using Tanka.GraphQL.Server; + + namespace Tests; + + /// + /// Root query type by naming convention + /// + /// We define it as static class so that the generator does not try + /// to use the initialValue as the source of it. + /// + /// + [ObjectType] + public static class Query + { + public static World World() => new(); + } + + [ObjectType] + public class World + { + /// + /// Simple field with one string argument and string return type + /// + /// name: String! + /// String! + public string Hello(string name) => $"Hello {name}"; + + /// + /// This is the async version of the Hello method + /// + /// + /// + public async Task HelloAsync(string name) => await Task.FromResult($"Hello {name}"); + } + """; + + return TestHelper.Verify(source); + } + + [Fact] + public Task Random_AsyncEnumerable() + { + var source = """ + using Tanka.GraphQL.Server; + + namespace Tests; + + [ObjectType] + public class World + { + /// + /// This is subscription field producing random integers of count between from and to + /// + /// + public async IAsyncEnumerable Random(int from, int to, int count, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var r = new Random(); + + for (var i = 0; i < count; i++) + { + yield return r.Next(from, to); + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(i * 10, cancellationToken); + } + } + } + """; + + return TestHelper.Verify(source); + } } \ No newline at end of file diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name#TestsPersonController.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name#TestsPersonController.g.verified.cs index 1266da5e7..abd800975 100644 --- a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name#TestsPersonController.g.verified.cs +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name#TestsPersonController.g.verified.cs @@ -1,4 +1,7 @@ //HintName: TestsPersonController.g.cs +/// + +#nullable enable using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; @@ -7,9 +10,7 @@ using Tanka.GraphQL.Server; using Tanka.GraphQL.ValueResolution; - namespace Tests; - public static class PersonController { public static ValueTask Name(ResolverContext context) @@ -18,25 +19,14 @@ public static ValueTask Name(ResolverContext context) context.ResolvedValue = objectValue.Name; return default; } - - - - } public static class PersonControllerExtensions { - public static SourceGeneratedTypesBuilder AddPersonController( - this SourceGeneratedTypesBuilder builder) + public static SourceGeneratedTypesBuilder AddPersonController(this SourceGeneratedTypesBuilder builder) { - builder.Builder.Configure(options => options.Builder.Add( - "Person", - new FieldsWithResolvers() - { - { "name: String!", PersonController.Name } - - })); - + builder.Builder.Configure(options => options.Builder.Add("Person", new FieldsWithResolvers() { { "name: String!", PersonController.Name } })); return builder; } } +#nullable restore diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name#TestsQueryController.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name#TestsQueryController.g.verified.cs index e9e002713..4f7d40ede 100644 --- a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name#TestsQueryController.g.verified.cs +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name#TestsQueryController.g.verified.cs @@ -1,4 +1,7 @@ //HintName: TestsQueryController.g.cs +/// + +#nullable enable using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; @@ -7,38 +10,22 @@ using Tanka.GraphQL.Server; using Tanka.GraphQL.ValueResolution; - namespace Tests; - public static class QueryController { - - public static ValueTask Person(ResolverContext context) { - context.ResolvedValue = Query.Person( - context.GetArgument("id") - ); - + context.ResolvedValue = Query.Person(context.GetArgument("id")); return default; } - - } public static class QueryControllerExtensions { - public static SourceGeneratedTypesBuilder AddQueryController( - this SourceGeneratedTypesBuilder builder) + public static SourceGeneratedTypesBuilder AddQueryController(this SourceGeneratedTypesBuilder builder) { - builder.Builder.Configure(options => options.Builder.Add( - "Query", - new FieldsWithResolvers() - { - { "person(id: Int!): Person!", QueryController.Person } - - })); - + builder.Builder.Configure(options => options.Builder.Add("Query", new FieldsWithResolvers() { { "person(id: Int!): Person!", QueryController.Person } })); return builder; } } +#nullable restore diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name_no_namespace#PersonController.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name_no_namespace#PersonController.g.verified.cs index 78622c49a..a5693062b 100644 --- a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name_no_namespace#PersonController.g.verified.cs +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name_no_namespace#PersonController.g.verified.cs @@ -1,4 +1,7 @@ //HintName: PersonController.g.cs +/// + +#nullable enable using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; @@ -7,9 +10,6 @@ using Tanka.GraphQL.Server; using Tanka.GraphQL.ValueResolution; - - - public static class PersonController { public static ValueTask Name(ResolverContext context) @@ -18,25 +18,14 @@ public static ValueTask Name(ResolverContext context) context.ResolvedValue = objectValue.Name; return default; } - - - - } public static class PersonControllerExtensions { - public static SourceGeneratedTypesBuilder AddPersonController( - this SourceGeneratedTypesBuilder builder) + public static SourceGeneratedTypesBuilder AddPersonController(this SourceGeneratedTypesBuilder builder) { - builder.Builder.Configure(options => options.Builder.Add( - "Person", - new FieldsWithResolvers() - { - { "name: String!", PersonController.Name } - - })); - + builder.Builder.Configure(options => options.Builder.Add("Person", new FieldsWithResolvers() { { "name: String!", PersonController.Name } })); return builder; } } +#nullable restore diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name_no_namespace#QueryController.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name_no_namespace#QueryController.g.verified.cs index 695c53226..d445c2daf 100644 --- a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name_no_namespace#QueryController.g.verified.cs +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Generate_ObjectType_type_name_no_namespace#QueryController.g.verified.cs @@ -1,4 +1,7 @@ //HintName: QueryController.g.cs +/// + +#nullable enable using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; @@ -7,38 +10,21 @@ using Tanka.GraphQL.Server; using Tanka.GraphQL.ValueResolution; - - - public static class QueryController { - - public static ValueTask Person(ResolverContext context) { - context.ResolvedValue = Query.Person( - context.GetArgument("id") - ); - + context.ResolvedValue = Query.Person(context.GetArgument("id")); return default; } - - } public static class QueryControllerExtensions { - public static SourceGeneratedTypesBuilder AddQueryController( - this SourceGeneratedTypesBuilder builder) + public static SourceGeneratedTypesBuilder AddQueryController(this SourceGeneratedTypesBuilder builder) { - builder.Builder.Configure(options => options.Builder.Add( - "Query", - new FieldsWithResolvers() - { - { "person(id: Int!): Person!", QueryController.Person } - - })); - + builder.Builder.Configure(options => options.Builder.Add("Query", new FieldsWithResolvers() { { "person(id: Int!): Person!", QueryController.Person } })); return builder; } } +#nullable restore diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#InputType.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#InputType.g.verified.cs new file mode 100644 index 000000000..7f21cfdc9 --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#InputType.g.verified.cs @@ -0,0 +1,12 @@ +//HintName: InputType.g.cs +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Tanka.GraphQL.Executable; + +namespace Tanka.GraphQL.Server; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class InputTypeAttribute: Attribute +{ +} \ No newline at end of file diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#ObjectType.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#ObjectType.g.verified.cs new file mode 100644 index 000000000..c39935439 --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#ObjectType.g.verified.cs @@ -0,0 +1,39 @@ +//HintName: ObjectType.g.cs +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Tanka.GraphQL.Executable; + +namespace Tanka.GraphQL.Server; + +public static class SourceGeneratedExecutableSchemaExtensions +{ + public static OptionsBuilder AddGeneratedTypes( + this OptionsBuilder builder, + Action configureTypes) + { + var typesBuilder = new SourceGeneratedTypesBuilder(builder); + configureTypes(typesBuilder); + return builder; + } +} + +public class SourceGeneratedTypesBuilder +{ + public OptionsBuilder Builder { get; } + + public SourceGeneratedTypesBuilder(OptionsBuilder builder) + { + Builder = builder; + } +} + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class ObjectTypeAttribute: Attribute +{ +} + +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class FromArgumentsAttribute: Attribute +{ +} \ No newline at end of file diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#Tests.SourceGeneratedTypesExtensions.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#Tests.SourceGeneratedTypesExtensions.verified.cs new file mode 100644 index 000000000..8081a1356 --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#Tests.SourceGeneratedTypesExtensions.verified.cs @@ -0,0 +1,19 @@ +//HintName: Tests.SourceGeneratedTypesExtensions.cs +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Tanka.GraphQL.Server; +using Tanka.GraphQL.Executable; +using Tanka.GraphQL.ValueResolution; +using Tanka.GraphQL.Fields; + +namespace Tests; +public static class TestsSourceGeneratedTypesExtensions +{ + public static SourceGeneratedTypesBuilder AddTestsTypes(this SourceGeneratedTypesBuilder builder) + { + builder.AddQueryController(); + builder.AddWorldController(); + return builder; + } +} diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#TestsQueryController.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#TestsQueryController.g.verified.cs new file mode 100644 index 000000000..22a1fbc68 --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#TestsQueryController.g.verified.cs @@ -0,0 +1,31 @@ +//HintName: TestsQueryController.g.cs +/// + +#nullable enable +using Microsoft.Extensions.Options; +using System; +using System.Threading.Tasks; +using Tanka.GraphQL.Executable; +using Tanka.GraphQL.Fields; +using Tanka.GraphQL.Server; +using Tanka.GraphQL.ValueResolution; + +namespace Tests; +public static class QueryController +{ + public static ValueTask World(ResolverContext context) + { + context.ResolvedValue = Query.World(); + return default; + } +} + +public static class QueryControllerExtensions +{ + public static SourceGeneratedTypesBuilder AddQueryController(this SourceGeneratedTypesBuilder builder) + { + builder.Builder.Configure(options => options.Builder.Add("Query", new FieldsWithResolvers() { { "world: World!", QueryController.World } })); + return builder; + } +} +#nullable restore diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#TestsWorldController.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#TestsWorldController.g.verified.cs new file mode 100644 index 000000000..f92c5f1d0 --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.HelloWorld#TestsWorldController.g.verified.cs @@ -0,0 +1,36 @@ +//HintName: TestsWorldController.g.cs +/// + +#nullable enable +using Microsoft.Extensions.Options; +using System; +using System.Threading.Tasks; +using Tanka.GraphQL.Executable; +using Tanka.GraphQL.Fields; +using Tanka.GraphQL.Server; +using Tanka.GraphQL.ValueResolution; + +namespace Tests; +public static class WorldController +{ + public static ValueTask Hello(ResolverContext context) + { + context.ResolvedValue = ((World)context.ObjectValue).Hello(context.GetArgument("name")); + return default; + } + + public static async ValueTask HelloAsync(ResolverContext context) + { + context.ResolvedValue = await ((World)context.ObjectValue).HelloAsync(context.GetArgument("name")); + } +} + +public static class WorldControllerExtensions +{ + public static SourceGeneratedTypesBuilder AddWorldController(this SourceGeneratedTypesBuilder builder) + { + builder.Builder.Configure(options => options.Builder.Add("World", new FieldsWithResolvers() { { "hello(name: String!): String!", WorldController.Hello }, { "helloAsync(name: String!): String!", WorldController.HelloAsync } })); + return builder; + } +} +#nullable restore diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#InputType.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#InputType.g.verified.cs new file mode 100644 index 000000000..7f21cfdc9 --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#InputType.g.verified.cs @@ -0,0 +1,12 @@ +//HintName: InputType.g.cs +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Tanka.GraphQL.Executable; + +namespace Tanka.GraphQL.Server; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class InputTypeAttribute: Attribute +{ +} \ No newline at end of file diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#ObjectType.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#ObjectType.g.verified.cs new file mode 100644 index 000000000..c39935439 --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#ObjectType.g.verified.cs @@ -0,0 +1,39 @@ +//HintName: ObjectType.g.cs +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Tanka.GraphQL.Executable; + +namespace Tanka.GraphQL.Server; + +public static class SourceGeneratedExecutableSchemaExtensions +{ + public static OptionsBuilder AddGeneratedTypes( + this OptionsBuilder builder, + Action configureTypes) + { + var typesBuilder = new SourceGeneratedTypesBuilder(builder); + configureTypes(typesBuilder); + return builder; + } +} + +public class SourceGeneratedTypesBuilder +{ + public OptionsBuilder Builder { get; } + + public SourceGeneratedTypesBuilder(OptionsBuilder builder) + { + Builder = builder; + } +} + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class ObjectTypeAttribute: Attribute +{ +} + +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class FromArgumentsAttribute: Attribute +{ +} \ No newline at end of file diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#Tests.SourceGeneratedTypesExtensions.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#Tests.SourceGeneratedTypesExtensions.verified.cs new file mode 100644 index 000000000..3516cdd0e --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#Tests.SourceGeneratedTypesExtensions.verified.cs @@ -0,0 +1,18 @@ +//HintName: Tests.SourceGeneratedTypesExtensions.cs +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Tanka.GraphQL.Server; +using Tanka.GraphQL.Executable; +using Tanka.GraphQL.ValueResolution; +using Tanka.GraphQL.Fields; + +namespace Tests; +public static class TestsSourceGeneratedTypesExtensions +{ + public static SourceGeneratedTypesBuilder AddTestsTypes(this SourceGeneratedTypesBuilder builder) + { + builder.AddWorldController(); + return builder; + } +} diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#TestsWorldController.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#TestsWorldController.g.verified.cs new file mode 100644 index 000000000..5c611ea3b --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.Random_AsyncEnumerable#TestsWorldController.g.verified.cs @@ -0,0 +1,37 @@ +//HintName: TestsWorldController.g.cs +/// + +#nullable enable +using Microsoft.Extensions.Options; +using System; +using System.Threading.Tasks; +using Tanka.GraphQL.Executable; +using Tanka.GraphQL.Fields; +using Tanka.GraphQL.Server; +using Tanka.GraphQL.ValueResolution; + +namespace Tests; +public static class WorldController +{ + public static ValueTask RandomResolver(ResolverContext context) + { + context.ResolvedValue = context.ObjectValue; + return default; + } + + public static ValueTask Random(SubscriberContext context, CancellationToken cancellationToken) + { + context.SetResult(((World)context.ObjectValue).Random(context.GetArgument("from"), context.GetArgument("to"), context.GetArgument("count"), cancellationToken)); + return default; + } +} + +public static class WorldControllerExtensions +{ + public static SourceGeneratedTypesBuilder AddWorldController(this SourceGeneratedTypesBuilder builder) + { + builder.Builder.Configure(options => options.Builder.Add("World", new FieldsWithResolvers() { { "random(from: Int!, to: Int!, count: Int!): Int!", WorldController.RandomResolver } }, new FieldsWithSubscribers() { { "random(from: Int!, to: Int!, count: Int!): Int!", WorldController.Random } })); + return builder; + } +} +#nullable restore diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_resolver#TestsQueryController.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_resolver#TestsQueryController.g.verified.cs index 01a8fe4c1..6057acf0d 100644 --- a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_resolver#TestsQueryController.g.verified.cs +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_resolver#TestsQueryController.g.verified.cs @@ -1,4 +1,7 @@ //HintName: TestsQueryController.g.cs +/// + +#nullable enable using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; @@ -7,38 +10,22 @@ using Tanka.GraphQL.Server; using Tanka.GraphQL.ValueResolution; - namespace Tests; - public static class QueryController { - - public static ValueTask Id(ResolverContext context) { - context.ResolvedValue = Query.Id( - context.GetArgument("p1") - ); - + context.ResolvedValue = Query.Id(context.GetArgument("p1")); return default; } - - } public static class QueryControllerExtensions { - public static SourceGeneratedTypesBuilder AddQueryController( - this SourceGeneratedTypesBuilder builder) + public static SourceGeneratedTypesBuilder AddQueryController(this SourceGeneratedTypesBuilder builder) { - builder.Builder.Configure(options => options.Builder.Add( - "Query", - new FieldsWithResolvers() - { - { "id(p1: Int): Int!", QueryController.Id } - - })); - + builder.Builder.Configure(options => options.Builder.Add("Query", new FieldsWithResolvers() { { "id(p1: Int): Int!", QueryController.Id } })); return builder; } } +#nullable restore diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#InputType.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#InputType.g.verified.cs new file mode 100644 index 000000000..7f21cfdc9 --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#InputType.g.verified.cs @@ -0,0 +1,12 @@ +//HintName: InputType.g.cs +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Tanka.GraphQL.Executable; + +namespace Tanka.GraphQL.Server; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class InputTypeAttribute: Attribute +{ +} \ No newline at end of file diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#ObjectType.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#ObjectType.g.verified.cs new file mode 100644 index 000000000..c39935439 --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#ObjectType.g.verified.cs @@ -0,0 +1,39 @@ +//HintName: ObjectType.g.cs +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Tanka.GraphQL.Executable; + +namespace Tanka.GraphQL.Server; + +public static class SourceGeneratedExecutableSchemaExtensions +{ + public static OptionsBuilder AddGeneratedTypes( + this OptionsBuilder builder, + Action configureTypes) + { + var typesBuilder = new SourceGeneratedTypesBuilder(builder); + configureTypes(typesBuilder); + return builder; + } +} + +public class SourceGeneratedTypesBuilder +{ + public OptionsBuilder Builder { get; } + + public SourceGeneratedTypesBuilder(OptionsBuilder builder) + { + Builder = builder; + } +} + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class ObjectTypeAttribute: Attribute +{ +} + +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class FromArgumentsAttribute: Attribute +{ +} \ No newline at end of file diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#Tests.SourceGeneratedTypesExtensions.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#Tests.SourceGeneratedTypesExtensions.verified.cs new file mode 100644 index 000000000..9dda673c9 --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#Tests.SourceGeneratedTypesExtensions.verified.cs @@ -0,0 +1,18 @@ +//HintName: Tests.SourceGeneratedTypesExtensions.cs +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Tanka.GraphQL.Server; +using Tanka.GraphQL.Executable; +using Tanka.GraphQL.ValueResolution; +using Tanka.GraphQL.Fields; + +namespace Tests; +public static class TestsSourceGeneratedTypesExtensions +{ + public static SourceGeneratedTypesBuilder AddTestsTypes(this SourceGeneratedTypesBuilder builder) + { + builder.AddSubscriptionController(); + return builder; + } +} diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#TestsSubscriptionController.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#TestsSubscriptionController.g.verified.cs new file mode 100644 index 000000000..29d3e2c19 --- /dev/null +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_method_subscriber#TestsSubscriptionController.g.verified.cs @@ -0,0 +1,37 @@ +//HintName: TestsSubscriptionController.g.cs +/// + +#nullable enable +using Microsoft.Extensions.Options; +using System; +using System.Threading.Tasks; +using Tanka.GraphQL.Executable; +using Tanka.GraphQL.Fields; +using Tanka.GraphQL.Server; +using Tanka.GraphQL.ValueResolution; + +namespace Tests; +public static class SubscriptionController +{ + public static ValueTask RandomResolver(ResolverContext context) + { + context.ResolvedValue = context.ObjectValue; + return default; + } + + public static ValueTask Random(SubscriberContext context, CancellationToken cancellationToken) + { + context.SetResult(Subscription.Random(context.GetArgument("from"), context.GetArgument("to"), cancellationToken)); + return default; + } +} + +public static class SubscriptionControllerExtensions +{ + public static SourceGeneratedTypesBuilder AddSubscriptionController(this SourceGeneratedTypesBuilder builder) + { + builder.Builder.Configure(options => options.Builder.Add("Subscription", new FieldsWithResolvers() { { "random(from: Int!, to: Int!): Int!", SubscriptionController.RandomResolver } }, new FieldsWithSubscribers() { { "random(from: Int!, to: Int!): Int!", SubscriptionController.Random } })); + return builder; + } +} +#nullable restore diff --git a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_property_resolver#TestsQueryController.g.verified.cs b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_property_resolver#TestsQueryController.g.verified.cs index e99c0ef93..aa84b5e95 100644 --- a/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_property_resolver#TestsQueryController.g.verified.cs +++ b/tests/GraphQL.Server.SourceGenerators.Tests/Snapshots/ObjectGeneratorFacts.StaticClass_Generate_property_resolver#TestsQueryController.g.verified.cs @@ -1,4 +1,7 @@ //HintName: TestsQueryController.g.cs +/// + +#nullable enable using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; @@ -7,9 +10,7 @@ using Tanka.GraphQL.Server; using Tanka.GraphQL.ValueResolution; - namespace Tests; - public static class QueryController { public static ValueTask Id(ResolverContext context) @@ -17,25 +18,14 @@ public static ValueTask Id(ResolverContext context) context.ResolvedValue = Query.Id; return default; } - - - - } public static class QueryControllerExtensions { - public static SourceGeneratedTypesBuilder AddQueryController( - this SourceGeneratedTypesBuilder builder) + public static SourceGeneratedTypesBuilder AddQueryController(this SourceGeneratedTypesBuilder builder) { - builder.Builder.Configure(options => options.Builder.Add( - "Query", - new FieldsWithResolvers() - { - { "id: String!", QueryController.Id } - - })); - + builder.Builder.Configure(options => options.Builder.Add("Query", new FieldsWithResolvers() { { "id: String!", QueryController.Id } })); return builder; } } +#nullable restore