From 9fa411d5da844ea0e60a8cf59ade36a42cff8b50 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Thu, 11 Jan 2024 21:12:08 +0200 Subject: [PATCH] Experimental @oneof directive support for input types --- .../GraphQL.Extensions.Experimental.csproj | 15 ++ .../OneOf/OneOfDirective.cs | 66 ++++++ .../OneOf/SchemaBuilderExtensions.cs | 12 + .../OneOf/ServiceCollectionExtensions.cs | 17 ++ src/GraphQL.Language/Nodes/ObjectValue.cs | 27 +-- .../GraphQLApplicationBuilder.cs | 5 +- .../WebSockets/ClientMethods.cs | 13 +- .../WebSockets/ServerMethods.cs | 32 ++- ...faultOperationPipelineBuilderExtensions.cs | 2 +- src/GraphQL/Executor.cs | 6 +- src/GraphQL/ServiceCollectionExtensions.cs | 31 +++ src/GraphQL/TypeSystem/SchemaBuilder.cs | 16 +- src/GraphQL/TypeSystem/TypeExtensions.cs | 2 +- src/GraphQL/Validation/ExecutionRules.cs | 45 +++- src/GraphQL/Validation/Validator.cs | 28 ++- src/GraphQL/Validation/ValidatorFeature.cs | 2 +- src/GraphQL/Validation/Visitor.cs | 2 +- src/GraphQL/Values.cs | 2 +- tanka-graphql.sln | 28 +++ tanka-graphql.sln.DotSettings | 1 + .../GlobalUsings.cs | 1 + ...aphQL.Extensions.Experimental.Tests.csproj | 31 +++ .../OneOf/ValidationRuleFacts.cs | 212 ++++++++++++++++++ tests/GraphQL.Language.Tests/ParserFacts.cs | 4 +- .../GettingStarted.cs | 4 + 25 files changed, 546 insertions(+), 58 deletions(-) create mode 100644 src/GraphQL.Extensions.Experimental/GraphQL.Extensions.Experimental.csproj create mode 100644 src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs create mode 100644 src/GraphQL.Extensions.Experimental/OneOf/SchemaBuilderExtensions.cs create mode 100644 src/GraphQL.Extensions.Experimental/OneOf/ServiceCollectionExtensions.cs create mode 100644 src/GraphQL/ServiceCollectionExtensions.cs create mode 100644 tests/GraphQL.Extensions.Experimental.Tests/GlobalUsings.cs create mode 100644 tests/GraphQL.Extensions.Experimental.Tests/GraphQL.Extensions.Experimental.Tests.csproj create mode 100644 tests/GraphQL.Extensions.Experimental.Tests/OneOf/ValidationRuleFacts.cs diff --git a/src/GraphQL.Extensions.Experimental/GraphQL.Extensions.Experimental.csproj b/src/GraphQL.Extensions.Experimental/GraphQL.Extensions.Experimental.csproj new file mode 100644 index 000000000..6cf67e9e3 --- /dev/null +++ b/src/GraphQL.Extensions.Experimental/GraphQL.Extensions.Experimental.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + true + + + + + + + + \ No newline at end of file diff --git a/src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs b/src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs new file mode 100644 index 000000000..c2ad41e0c --- /dev/null +++ b/src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs @@ -0,0 +1,66 @@ +using Tanka.GraphQL.Language; +using Tanka.GraphQL.Language.Nodes; +using Tanka.GraphQL.Language.Nodes.TypeSystem; +using Tanka.GraphQL.TypeSystem; +using Tanka.GraphQL.Validation; + +namespace Tanka.GraphQL.Extensions.Experimental.OneOf; + +public class OneOfDirective +{ + private static readonly List AllowedKinds = [NodeKind.ObjectValue, NodeKind.Variable]; + + public static DirectiveDefinition Directive => + $"directive @oneOf on {TypeSystemDirectiveLocations.INPUT_OBJECT}"; + + public static CombineRule OneOfValidationRule() + { + return (context, rule) => + { + rule.EnterArgument += argument => + { + InputValueDefinition? argumentDefinition = context.Tracker.ArgumentDefinition; + + if (argumentDefinition is null) + return; + + if (!AllowedKinds.Contains(argument.Value.Kind)) + return; + + if (context.Schema.GetNamedType(argumentDefinition.Type.Unwrap().Name) is not InputObjectDefinition + inputObject) + return; + + if (!inputObject.HasDirective(Directive.Name)) + return; + + if (argument.Value.Kind == NodeKind.Variable) + { + var variable = (Variable)argument.Value; + + if (context.VariableValues?.TryGetValue(variable.Name, out object? variableValue) != true) return; + + if (variableValue is not null) + { + var coercedValue = Values.CoerceValue( + context.Schema, + variableValue, + argumentDefinition.Type) as IReadOnlyDictionary; + + if (coercedValue?.Count(kv => kv.Value is not null) != 1) + context.Error("ONEOF001", + $"Invalid value for '@oneOf' input '{inputObject.Name}'. @oneOf input objects can only have one field value set."); + } + } + else + { + var objectValue = (ObjectValue)argument.Value; + + if (objectValue.Count != 1) + context.Error("ONEOF001", + $"Invalid value for '@oneOf' input '{inputObject.Name}'. @oneOf input objects can only have one field value set."); + } + }; + }; + } +} \ No newline at end of file diff --git a/src/GraphQL.Extensions.Experimental/OneOf/SchemaBuilderExtensions.cs b/src/GraphQL.Extensions.Experimental/OneOf/SchemaBuilderExtensions.cs new file mode 100644 index 000000000..ba6a79a4f --- /dev/null +++ b/src/GraphQL.Extensions.Experimental/OneOf/SchemaBuilderExtensions.cs @@ -0,0 +1,12 @@ +using Tanka.GraphQL.TypeSystem; + +namespace Tanka.GraphQL.Extensions.Experimental.OneOf; + +public static class SchemaBuilderExtensions +{ + public static SchemaBuilder AddOneOf(this SchemaBuilder builder) + { + builder.Add(OneOfDirective.Directive); + return builder; + } +} \ No newline at end of file diff --git a/src/GraphQL.Extensions.Experimental/OneOf/ServiceCollectionExtensions.cs b/src/GraphQL.Extensions.Experimental/OneOf/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..98bc2868a --- /dev/null +++ b/src/GraphQL.Extensions.Experimental/OneOf/ServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; + +using Tanka.GraphQL.Validation; + +namespace Tanka.GraphQL.Extensions.Experimental.OneOf; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddOneOf(this IServiceCollection services) + { + services.PostConfigure( + options => options.Rules.Add(OneOfDirective.OneOfValidationRule()) + ); + + return services; + } +} \ No newline at end of file diff --git a/src/GraphQL.Language/Nodes/ObjectValue.cs b/src/GraphQL.Language/Nodes/ObjectValue.cs index 77f5ac546..bf138af81 100644 --- a/src/GraphQL.Language/Nodes/ObjectValue.cs +++ b/src/GraphQL.Language/Nodes/ObjectValue.cs @@ -3,33 +3,26 @@ namespace Tanka.GraphQL.Language.Nodes; -public sealed class ObjectValue : ValueBase, ICollectionNode +public sealed class ObjectValue( + IReadOnlyList fields, + in Location? location = default) + : ValueBase, ICollectionNode { - //todo: remove? - public readonly IReadOnlyList Fields; - - public ObjectValue( - IReadOnlyList fields, - in Location? location = default) - { - Fields = fields; - Location = location; - } - public override NodeKind Kind => NodeKind.ObjectValue; - public override Location? Location { get; } + public override Location? Location { get; } = location; public IEnumerator GetEnumerator() { - return Fields.GetEnumerator(); + return fields.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { - return ((IEnumerable)Fields).GetEnumerator(); + return ((IEnumerable)fields).GetEnumerator(); } - public int Count => Fields.Count; - public ObjectField this[int index] => Fields[index]; + public int Count => fields.Count; + + public ObjectField this[int index] => fields[index]; } \ No newline at end of file diff --git a/src/GraphQL.Server/GraphQLApplicationBuilder.cs b/src/GraphQL.Server/GraphQLApplicationBuilder.cs index 6c315c9e3..e794249f0 100644 --- a/src/GraphQL.Server/GraphQLApplicationBuilder.cs +++ b/src/GraphQL.Server/GraphQLApplicationBuilder.cs @@ -14,7 +14,7 @@ public GraphQLApplicationBuilder(IServiceCollection applicationServices) ApplicationOptionsBuilder = ApplicationServices .AddOptions(); - AddCore(); + AddDefaultTankaGraphQLServerServices(); } public IServiceCollection ApplicationServices { get; } @@ -55,10 +55,11 @@ public GraphQLApplicationBuilder AddWebSockets() return this; } - private void AddCore() + private void AddDefaultTankaGraphQLServerServices() { ApplicationServices.TryAddSingleton(); ApplicationServices.TryAddSingleton(); ApplicationServices.TryAddSingleton(); + ApplicationServices.AddDefaultTankaGraphQLServices(); } } \ No newline at end of file diff --git a/src/GraphQL.Server/WebSockets/ClientMethods.cs b/src/GraphQL.Server/WebSockets/ClientMethods.cs index b8845861a..23b074936 100644 --- a/src/GraphQL.Server/WebSockets/ClientMethods.cs +++ b/src/GraphQL.Server/WebSockets/ClientMethods.cs @@ -1,17 +1,10 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; +using System.Threading.Channels; namespace Tanka.GraphQL.Server.WebSockets; -public class ClientMethods +public class ClientMethods(ChannelWriter writer) { - protected ChannelWriter Writer { get; } - - public ClientMethods(ChannelWriter writer) - { - Writer = writer; - } + protected ChannelWriter Writer { get; } = writer; public async Task ConnectionAck(ConnectionAck connectionAck, CancellationToken cancellationToken) { diff --git a/src/GraphQL.Server/WebSockets/ServerMethods.cs b/src/GraphQL.Server/WebSockets/ServerMethods.cs index 51b6053b0..6776fd058 100644 --- a/src/GraphQL.Server/WebSockets/ServerMethods.cs +++ b/src/GraphQL.Server/WebSockets/ServerMethods.cs @@ -1,14 +1,17 @@ using System.Collections.Concurrent; +using System.Diagnostics; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; -using Tanka.GraphQL.Fields; +using Tanka.GraphQL.Request; using Tanka.GraphQL.Server.WebSockets.WebSocketPipe; using Tanka.GraphQL.Validation; namespace Tanka.GraphQL.Server.WebSockets; -public class ServerMethods +public partial class ServerMethods { private readonly GraphQLRequestDelegate _requestDelegate; private readonly HttpContext _httpContext; @@ -20,11 +23,14 @@ public ServerMethods(WebSocketMessageChannel channel, GraphQLReques _httpContext = httpContext; Channel = channel; Client = new ClientMethods(Channel.Writer); + _logger = httpContext.RequestServices.GetRequiredService>(); } public ClientMethods Client { get; set; } public ConcurrentDictionary Subscriptions = new(); + + private readonly ILogger _logger; public async Task ConnectionInit(ConnectionInit connectionInit, CancellationToken cancellationToken) { @@ -57,6 +63,7 @@ await Channel.Complete( private async Task Execute(Subscribe subscribe, CancellationTokenSource unsubscribeOrAborted) { + _ = _logger.BeginScope(subscribe.Id); var cancellationToken = unsubscribeOrAborted.Token; var context = new GraphQLRequestContext { @@ -73,18 +80,27 @@ private async Task Execute(Subscribe subscribe, CancellationTokenSource unsubscr try { + ulong count = 0; + Log.Request(_logger, subscribe.Id, context.Request); await _requestDelegate(context); await using var enumerator = context.Response.GetAsyncEnumerator(cancellationToken); + long started = Stopwatch.GetTimestamp(); while (await enumerator.MoveNextAsync()) { + count++; + string elapsed = $"{Stopwatch.GetElapsedTime(started).TotalMilliseconds}ms"; + Log.ExecutionResult(_logger, subscribe.Id, enumerator.Current, elapsed); await Client.Next(new Next() { Id = subscribe.Id, Payload = enumerator.Current }, cancellationToken); + started = Stopwatch.GetTimestamp(); } if (!cancellationToken.IsCancellationRequested) { await Client.Complete(new Complete() { Id = subscribe.Id }, cancellationToken); } + + Log.Completed(_logger, subscribe.Id, count); } catch (OperationCanceledException) { @@ -141,4 +157,16 @@ public async Task Complete(Complete complete, CancellationToken cancellationToke await worker; } } + + private static partial class Log + { + [LoggerMessage(5, LogLevel.Debug, "Subscription({Id}) - Result({elapsed}): {result}")] + public static partial void ExecutionResult(ILogger logger, string id, ExecutionResult? result, string elapsed); + + [LoggerMessage(3, LogLevel.Debug, "Subscription({Id}) - Request: {request}")] + public static partial void Request(ILogger logger, string id, GraphQLRequest request); + + [LoggerMessage(10, LogLevel.Information, "Subscription({Id}) - Server stream completed. {count} messages sent.")] + public static partial void Completed(ILogger logger, string id, ulong count); + } } \ No newline at end of file diff --git a/src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs b/src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs index 6d14af519..0ca737722 100644 --- a/src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs +++ b/src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs @@ -94,7 +94,7 @@ public static OperationDelegateBuilder UseDefaults(this OperationDelegateBuilder public static OperationDelegateBuilder UseDefaultValidator(this OperationDelegateBuilder builder) { - var validator = new Validator3(ExecutionRules.All); + var validator = builder.ApplicationServices.GetRequiredService(); builder.Use(next => async context => { diff --git a/src/GraphQL/Executor.cs b/src/GraphQL/Executor.cs index fb98f11a7..431049d00 100644 --- a/src/GraphQL/Executor.cs +++ b/src/GraphQL/Executor.cs @@ -11,8 +11,6 @@ namespace Tanka.GraphQL; /// public partial class Executor { - private static readonly IServiceProvider EmptyProvider = new ServiceCollection().BuildServiceProvider(); - private readonly OperationDelegate _operationDelegate; /// @@ -29,7 +27,9 @@ public Executor(ISchema schema) : this(new ExecutorOptions { Schema = schema }) /// public Executor(ExecutorOptions options) { - OperationDelegateBuilder builder = new(options.ServiceProvider ?? EmptyProvider); + OperationDelegateBuilder builder = new(options.ServiceProvider ?? new ServiceCollection() + .AddDefaultTankaGraphQLServices() + .BuildServiceProvider()); if (options.TraceEnabled) builder.UseTrace(); diff --git a/src/GraphQL/ServiceCollectionExtensions.cs b/src/GraphQL/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..7589a7150 --- /dev/null +++ b/src/GraphQL/ServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +using Tanka.GraphQL.Validation; + +namespace Tanka.GraphQL; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddDefaultTankaGraphQLServices(this IServiceCollection services) + { + return services.AddDefaultValidator(); + } + + public static IServiceCollection AddDefaultValidator(this IServiceCollection services) + { + services.AddOptions(); + services.AddOptions(); + services.TryAddSingleton(p => + new AsyncValidator(p.GetRequiredService>())); + + return services; + } + + public static IServiceCollection AddDefaultValidatorRule(this IServiceCollection services, CombineRule rule) + { + services.PostConfigure(options => options.Rules.Add(rule)); + return services; + } +} \ No newline at end of file diff --git a/src/GraphQL/TypeSystem/SchemaBuilder.cs b/src/GraphQL/TypeSystem/SchemaBuilder.cs index 599f766d4..27c093d8c 100644 --- a/src/GraphQL/TypeSystem/SchemaBuilder.cs +++ b/src/GraphQL/TypeSystem/SchemaBuilder.cs @@ -153,40 +153,48 @@ public SchemaBuilder Add(SchemaExtension schemaExtension) /// Add type definition into the builder /// /// - public void Add(TypeDefinition typeDefinition) + public SchemaBuilder Add(TypeDefinition typeDefinition) { if (!_typeDefinitions.TryAdd(typeDefinition.Name, typeDefinition)) throw TypeAlreadyExists(typeDefinition.Name); + + return this; } /// /// Add type definitions into the builder /// /// - public void Add(TypeDefinition[] typeDefinitions) + public SchemaBuilder Add(TypeDefinition[] typeDefinitions) { foreach (var typeDefinition in typeDefinitions) Add(typeDefinition); + + return this; } /// /// Add directive definition into the builder /// /// - public void Add(DirectiveDefinition directiveDefinition) + public SchemaBuilder Add(DirectiveDefinition directiveDefinition) { if (!_directiveDefinitions.TryAdd(directiveDefinition.Name, directiveDefinition)) throw TypeAlreadyExists(directiveDefinition.Name); + + return this; } /// /// Add directive definitions into the builder /// /// - public void Add(DirectiveDefinition[] directiveDefinitions) + public SchemaBuilder Add(DirectiveDefinition[] directiveDefinitions) { foreach (var directiveDefinition in directiveDefinitions) Add(directiveDefinition); + + return this; } /// diff --git a/src/GraphQL/TypeSystem/TypeExtensions.cs b/src/GraphQL/TypeSystem/TypeExtensions.cs index e72748c45..d24a14a12 100644 --- a/src/GraphQL/TypeSystem/TypeExtensions.cs +++ b/src/GraphQL/TypeSystem/TypeExtensions.cs @@ -8,7 +8,7 @@ public static NamedType Unwrap(this TypeBase type) { return type switch { - NonNullType NonNullType => Unwrap(NonNullType.OfType), + NonNullType nonNullType => Unwrap(nonNullType.OfType), ListType list => Unwrap(list.OfType), _ => (NamedType)type }; diff --git a/src/GraphQL/Validation/ExecutionRules.cs b/src/GraphQL/Validation/ExecutionRules.cs index 1e56873a4..8239ec834 100644 --- a/src/GraphQL/Validation/ExecutionRules.cs +++ b/src/GraphQL/Validation/ExecutionRules.cs @@ -1,6 +1,7 @@ using Tanka.GraphQL.Language; using Tanka.GraphQL.Language.Nodes; using Tanka.GraphQL.Language.Nodes.TypeSystem; +using Tanka.GraphQL.Request; using Tanka.GraphQL.SelectionSets; using Tanka.GraphQL.ValueSerialization; @@ -406,8 +407,42 @@ void ValidateArguments( } // variables should be valid - if (argument.Value is Variable) - continue; + if (argument.Value is Variable variable) + { + if (ruleVisitorContext.VariableValues is null) + { + ruleVisitorContext.Error( + ValidationErrorCodes.R5421RequiredArguments, + "Arguments is required. An argument is required " + + "if the argument type is non‐null and does not have a default " + + "value. Otherwise, the argument is optional. " + + $"Value of argument '{argumentName}' cannot be null"); + + return; + } + + + if (!ruleVisitorContext.VariableValues.TryGetValue(variable.Name, out var variableValue)) + ruleVisitorContext.Error( + ValidationErrorCodes.R5421RequiredArguments, + "Arguments is required. An argument is required " + + "if the argument type is non‐null and does not have a default " + + "value. Otherwise, the argument is optional. " + + $"Value of argument '{argumentName}' cannot be null"); + else + { + if (variableValue is null) + { + ruleVisitorContext.Error( + ValidationErrorCodes.R5421RequiredArguments, + "Arguments is required. An argument is required " + + "if the argument type is non‐null and does not have a default " + + "value. Otherwise, the argument is optional. " + + $"Value of argument '{argumentName}' cannot be null"); + } + } + + } if (argument?.Value == null || argument.Value.Kind == NodeKind.NullValue) ruleVisitorContext.Error( @@ -864,7 +899,7 @@ public static CombineRule R561ValuesOfCorrectType() return; } - var fieldNodeMap = node.Fields.ToDictionary( + var fieldNodeMap = node.ToDictionary( f => f.Name); foreach (var fieldDef in context.Schema.GetInputFields( @@ -1034,7 +1069,7 @@ public static CombineRule R563InputObjectFieldUniqueness() { rule.EnterObjectValue += node => { - var fields = node.Fields.ToList(); + var fields = node.ToList(); foreach (var inputField in fields) { @@ -1063,7 +1098,7 @@ public static CombineRule R564InputObjectRequiredFields() if (inputObject == null) return; - var fields = node.Fields.ToDictionary(f => f.Name); + var fields = node.ToDictionary(f => f.Name); var fieldDefinitions = context.Schema.GetInputFields(inputObject.Name); foreach (var fieldDefinition in fieldDefinitions) diff --git a/src/GraphQL/Validation/Validator.cs b/src/GraphQL/Validation/Validator.cs index 55ce15d50..22fd92ff1 100644 --- a/src/GraphQL/Validation/Validator.cs +++ b/src/GraphQL/Validation/Validator.cs @@ -1,8 +1,10 @@ +using Microsoft.Extensions.Options; + using Tanka.GraphQL.Language.Nodes; namespace Tanka.GraphQL.Validation; -public interface IValidator3 +public interface IAsyncValidator { ValueTask Validate( ISchema schema, @@ -10,14 +12,14 @@ ValueTask Validate( IReadOnlyDictionary? variables); } -public class Validator3 : IValidator3 +public class AsyncValidatorOptions { - private readonly IEnumerable _rules; + public List Rules { get; set; } = [.. ExecutionRules.All]; +} - public Validator3(IEnumerable rules) - { - _rules = rules; - } +public class AsyncValidator : IAsyncValidator +{ + private readonly IOptions _optionsMonitor; public ValueTask Validate( ISchema schema, @@ -25,15 +27,25 @@ public ValueTask Validate( IReadOnlyDictionary? variables) { var visitor = new RulesWalker( - _rules, + _optionsMonitor.Value.Rules, schema, document, variables); return new(visitor.Validate()); } + + public AsyncValidator(IEnumerable rules) : this(Options.Create(new AsyncValidatorOptions() { Rules = [..rules]})) + { + } + + public AsyncValidator(IOptions optionsMonitor) + { + _optionsMonitor = optionsMonitor; + } } +[Obsolete("Use AsyncValidator")] public static class Validator { public static ValidationResult Validate( diff --git a/src/GraphQL/Validation/ValidatorFeature.cs b/src/GraphQL/Validation/ValidatorFeature.cs index 3a61e7aa1..17bdf2796 100644 --- a/src/GraphQL/Validation/ValidatorFeature.cs +++ b/src/GraphQL/Validation/ValidatorFeature.cs @@ -5,7 +5,7 @@ namespace Tanka.GraphQL.Validation; public class ValidatorFeature : IValidatorFeature { - public IValidator3? Validator { get; set; } + public IAsyncValidator? Validator { get; set; } public ValueTask Validate( ISchema schema, diff --git a/src/GraphQL/Validation/Visitor.cs b/src/GraphQL/Validation/Visitor.cs index 469f49879..58c7d6556 100644 --- a/src/GraphQL/Validation/Visitor.cs +++ b/src/GraphQL/Validation/Visitor.cs @@ -245,7 +245,7 @@ public virtual void Visit(ExecutableDocument ast) public virtual ObjectValue BeginVisitObjectValue( ObjectValue node) { - foreach (var objectField in node.Fields) BeginVisitNode(objectField); + foreach (var objectField in node) BeginVisitNode(objectField); return EndVisitObjectValue(node); } diff --git a/src/GraphQL/Values.cs b/src/GraphQL/Values.cs index 57b8e9c94..7e23f5b5f 100644 --- a/src/GraphQL/Values.cs +++ b/src/GraphQL/Values.cs @@ -90,7 +90,7 @@ public static class Values Dictionary result) { var fields = schema.GetInputFields(input.Name); - var valueFields = objectValue.Fields.ToDictionary(f => f.Name.Value, f => f); + var valueFields = objectValue.ToDictionary(f => f.Name.Value, f => f); foreach (var inputField in fields) { diff --git a/tanka-graphql.sln b/tanka-graphql.sln index 8684098e1..fe59c7712 100644 --- a/tanka-graphql.sln +++ b/tanka-graphql.sln @@ -74,6 +74,10 @@ 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}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Extensions.Experimental", "src\GraphQL.Extensions.Experimental\GraphQL.Extensions.Experimental.csproj", "{35D039EE-6718-43E2-83CD-00ACA4644FB0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -360,6 +364,30 @@ Global {4A12194D-8289-462C-94B8-8ABDE2D8283A}.Release|x64.Build.0 = Release|Any CPU {4A12194D-8289-462C-94B8-8ABDE2D8283A}.Release|x86.ActiveCfg = Release|Any CPU {4A12194D-8289-462C-94B8-8ABDE2D8283A}.Release|x86.Build.0 = Release|Any CPU + {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Debug|x64.Build.0 = Debug|Any CPU + {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Debug|x86.Build.0 = Debug|Any CPU + {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Release|Any CPU.Build.0 = Release|Any CPU + {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Release|x64.ActiveCfg = Release|Any CPU + {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Release|x64.Build.0 = Release|Any CPU + {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Release|x86.ActiveCfg = Release|Any CPU + {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Release|x86.Build.0 = Release|Any CPU + {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Debug|x64.ActiveCfg = Debug|Any CPU + {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Debug|x64.Build.0 = Debug|Any CPU + {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Debug|x86.ActiveCfg = Debug|Any CPU + {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Debug|x86.Build.0 = Debug|Any CPU + {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Release|Any CPU.Build.0 = Release|Any CPU + {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Release|x64.ActiveCfg = Release|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/tanka-graphql.sln.DotSettings b/tanka-graphql.sln.DotSettings index 188dd4195..f8787e29f 100644 --- a/tanka-graphql.sln.DotSettings +++ b/tanka-graphql.sln.DotSettings @@ -6,6 +6,7 @@ True True True + True True True True diff --git a/tests/GraphQL.Extensions.Experimental.Tests/GlobalUsings.cs b/tests/GraphQL.Extensions.Experimental.Tests/GlobalUsings.cs new file mode 100644 index 000000000..8c927eb74 --- /dev/null +++ b/tests/GraphQL.Extensions.Experimental.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/tests/GraphQL.Extensions.Experimental.Tests/GraphQL.Extensions.Experimental.Tests.csproj b/tests/GraphQL.Extensions.Experimental.Tests/GraphQL.Extensions.Experimental.Tests.csproj new file mode 100644 index 000000000..1200debea --- /dev/null +++ b/tests/GraphQL.Extensions.Experimental.Tests/GraphQL.Extensions.Experimental.Tests.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/tests/GraphQL.Extensions.Experimental.Tests/OneOf/ValidationRuleFacts.cs b/tests/GraphQL.Extensions.Experimental.Tests/OneOf/ValidationRuleFacts.cs new file mode 100644 index 000000000..2485e7d2c --- /dev/null +++ b/tests/GraphQL.Extensions.Experimental.Tests/OneOf/ValidationRuleFacts.cs @@ -0,0 +1,212 @@ +using Tanka.GraphQL.Extensions.Experimental.OneOf; +using Tanka.GraphQL.TypeSystem; +using Tanka.GraphQL.Validation; + +namespace Tanka.GraphQL.Extensions.Experimental.Tests.OneOf; + +public class ValidationRuleFacts +{ + [Fact] + public async Task Valid_when_one_field_set() + { + /* Given */ + ISchema schema = await new SchemaBuilder() + .Add(OneOfDirective.Directive) + .Add(""" + input OneOfInput @oneOf { + a: String + b: String + } + + type Query { + oneOf(input: OneOfInput!): String + } + """) + .Build(new SchemaBuildOptions()); + + var validator = new AsyncValidator([..ExecutionRules.All, OneOfDirective.OneOfValidationRule()]); + + /* When */ + ValidationResult result = await validator.Validate(schema, """ + { + oneOf(input: { a: "a" }) + } + """, + new Dictionary() + ); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public async Task Valid_when_one_field_set_for_nullable() + { + /* Given */ + ISchema schema = await new SchemaBuilder() + .Add(OneOfDirective.Directive) + .Add(""" + input OneOfInput @oneOf { + a: String + b: String + } + + type Query { + oneOf(input: OneOfInput): String + } + """) + .Build(new SchemaBuildOptions()); + + var validator = new AsyncValidator([.. ExecutionRules.All, OneOfDirective.OneOfValidationRule()]); + + /* When */ + ValidationResult result = await validator.Validate(schema, """ + { + oneOf(input: { a: "a" }) + } + """, + new Dictionary() + ); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public async Task Invalid_when_two_field_set() + { + /* Given */ + ISchema schema = await new SchemaBuilder() + .Add(OneOfDirective.Directive) + .Add(""" + input OneOfInput @oneOf { + a: String + b: String + } + + type Query { + oneOf(input: OneOfInput!): String + } + """) + .Build(new SchemaBuildOptions()); + + var validator = new AsyncValidator([.. ExecutionRules.All, OneOfDirective.OneOfValidationRule()]); + + /* When */ + ValidationResult result = await validator.Validate(schema, """ + { + oneOf(input: { a: "a", b: "b" }) + } + """, + new Dictionary() + ); + + /* Then */ + Assert.False(result.IsValid); + Assert.Equal("ONEOF001", result.Errors.Single().Code); + } + + [Fact] + public async Task Invalid_when_two_field_set_for_nullable() + { + /* Given */ + ISchema schema = await new SchemaBuilder() + .Add(OneOfDirective.Directive) + .Add(""" + input OneOfInput @oneOf { + a: String + b: String + } + + type Query { + oneOf(input: OneOfInput): String + } + """) + .Build(new SchemaBuildOptions()); + + var validator = new AsyncValidator([.. ExecutionRules.All, OneOfDirective.OneOfValidationRule()]); + + /* When */ + ValidationResult result = await validator.Validate(schema, """ + { + oneOf(input: { a: "a", b: "b" }) + } + """, + new Dictionary() + ); + + /* Then */ + Assert.False(result.IsValid); + Assert.Equal("ONEOF001", result.Errors.Single().Code); + } + + [Fact] + public async Task Valid_when_one_field_set_as_variable() + { + /* Given */ + ISchema schema = await new SchemaBuilder() + .Add(OneOfDirective.Directive) + .Add(""" + input OneOfInput @oneOf { + a: String + b: String + } + + type Query { + oneOf(input: OneOfInput!): String + } + """) + .Build(new SchemaBuildOptions()); + + var validator = new AsyncValidator([.. ExecutionRules.All, OneOfDirective.OneOfValidationRule()]); + + /* When */ + ValidationResult result = await validator.Validate(schema, """ + query ($variable: OneOfInput!) { + oneOf(input: $variable) + } + """, + new Dictionary { ["variable"] = new Dictionary { ["a"] = "a" } } + ); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public async Task Invalid_when_two_field_set_as_variable() + { + /* Given */ + ISchema schema = await new SchemaBuilder() + .Add(OneOfDirective.Directive) + .Add(""" + input OneOfInput @oneOf { + a: String + b: String + } + + type Query { + oneOf(input: OneOfInput!): String + } + """) + .Build(new SchemaBuildOptions()); + + var validator = new AsyncValidator([.. ExecutionRules.All, OneOfDirective.OneOfValidationRule()]); + + /* When */ + ValidationResult result = await validator.Validate(schema, """ + query ($variable: OneOfInput!) { + oneOf(input: $variable) + } + """, + new Dictionary + { + ["variable"] = new Dictionary { ["a"] = "a", ["b"] = "b" } + } + ); + + /* Then */ + Assert.False(result.IsValid); + Assert.Equal("ONEOF001", result.Errors.Single().Code); + } +} \ No newline at end of file diff --git a/tests/GraphQL.Language.Tests/ParserFacts.cs b/tests/GraphQL.Language.Tests/ParserFacts.cs index 366153ae7..b123cbd80 100644 --- a/tests/GraphQL.Language.Tests/ParserFacts.cs +++ b/tests/GraphQL.Language.Tests/ParserFacts.cs @@ -856,7 +856,7 @@ public void Value_ObjectValue_Empty() /* Then */ var listValue = Assert.IsType(value); - Assert.Equal(0, listValue.Fields.Count); + Assert.Equal(0, listValue.Count); } [Fact] @@ -870,7 +870,7 @@ public void Value_ObjectValue_with_Fields() /* Then */ var listValue = Assert.IsType(value); - Assert.Equal(2, listValue.Fields.Count); + Assert.Equal(2, listValue.Count); } [Theory] diff --git a/tutorials/GraphQL.Tutorials.Getting-Started/GettingStarted.cs b/tutorials/GraphQL.Tutorials.Getting-Started/GettingStarted.cs index 0bb6a3380..2bdf1831c 100644 --- a/tutorials/GraphQL.Tutorials.Getting-Started/GettingStarted.cs +++ b/tutorials/GraphQL.Tutorials.Getting-Started/GettingStarted.cs @@ -6,7 +6,9 @@ using Tanka.GraphQL.Directives; using Tanka.GraphQL.Language.Nodes; using Tanka.GraphQL.Request; +using Tanka.GraphQL.Server; using Tanka.GraphQL.TypeSystem; +using Tanka.GraphQL.Validation; using Tanka.GraphQL.ValueResolution; using Tanka.GraphQL.ValueSerialization; using Xunit; @@ -322,6 +324,7 @@ type Query { { Schema = schema, ServiceProvider = new ServiceCollection() + .AddDefaultTankaGraphQLServices() .AddSingleton() .BuildServiceProvider() }).Execute(new GraphQLRequest("{name}")); @@ -362,6 +365,7 @@ type Query { { Schema = schema, ServiceProvider = new ServiceCollection() + .AddDefaultTankaGraphQLServices() .AddSingleton() .BuildServiceProvider() }).Execute(new GraphQLRequest("{name}"));