Skip to content

Commit

Permalink
Experimental @OneOf directive support for input types
Browse files Browse the repository at this point in the history
  • Loading branch information
pekkah committed Jan 11, 2024
1 parent 18f7269 commit 9fa411d
Show file tree
Hide file tree
Showing 25 changed files with 546 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>true</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\GraphQL.Language\GraphQL.Language.csproj" />
<ProjectReference Include="..\GraphQL\GraphQL.csproj" />
</ItemGroup>

</Project>
66 changes: 66 additions & 0 deletions src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs
Original file line number Diff line number Diff line change
@@ -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<NodeKind> 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;

Check warning on line 25 in src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs#L25

Added line #L25 was not covered by tests

if (!AllowedKinds.Contains(argument.Value.Kind))
return;

Check warning on line 28 in src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs#L28

Added line #L28 was not covered by tests

if (context.Schema.GetNamedType(argumentDefinition.Type.Unwrap().Name) is not InputObjectDefinition
inputObject)
return;

Check warning on line 32 in src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs#L32

Added line #L32 was not covered by tests

if (!inputObject.HasDirective(Directive.Name))
return;

Check warning on line 35 in src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs#L35

Added line #L35 was not covered by tests

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<string, object?>;

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.");
}
};
};
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<AsyncValidatorOptions>(
options => options.Rules.Add(OneOfDirective.OneOfValidationRule())
);

return services;

Check warning on line 15 in src/GraphQL.Extensions.Experimental/OneOf/ServiceCollectionExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Extensions.Experimental/OneOf/ServiceCollectionExtensions.cs#L15

Added line #L15 was not covered by tests
}
}
27 changes: 10 additions & 17 deletions src/GraphQL.Language/Nodes/ObjectValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,26 @@

namespace Tanka.GraphQL.Language.Nodes;

public sealed class ObjectValue : ValueBase, ICollectionNode<ObjectField>
public sealed class ObjectValue(
IReadOnlyList<ObjectField> fields,
in Location? location = default)
: ValueBase, ICollectionNode<ObjectField>
{
//todo: remove?
public readonly IReadOnlyList<ObjectField> Fields;

public ObjectValue(
IReadOnlyList<ObjectField> 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<ObjectField> GetEnumerator()
{
return Fields.GetEnumerator();
return fields.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)Fields).GetEnumerator();
return ((IEnumerable)fields).GetEnumerator();

Check warning on line 22 in src/GraphQL.Language/Nodes/ObjectValue.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Language/Nodes/ObjectValue.cs#L22

Added line #L22 was not covered by tests
}

public int Count => Fields.Count;
public ObjectField this[int index] => Fields[index];
public int Count => fields.Count;

public ObjectField this[int index] => fields[index];
}
5 changes: 3 additions & 2 deletions src/GraphQL.Server/GraphQLApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public GraphQLApplicationBuilder(IServiceCollection applicationServices)
ApplicationOptionsBuilder = ApplicationServices
.AddOptions<GraphQLApplicationOptions>();

AddCore();
AddDefaultTankaGraphQLServerServices();

Check warning on line 17 in src/GraphQL.Server/GraphQLApplicationBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Server/GraphQLApplicationBuilder.cs#L17

Added line #L17 was not covered by tests
}

public IServiceCollection ApplicationServices { get; }
Expand Down Expand Up @@ -55,10 +55,11 @@ public GraphQLApplicationBuilder AddWebSockets()
return this;
}

private void AddCore()
private void AddDefaultTankaGraphQLServerServices()
{
ApplicationServices.TryAddSingleton<IHostedService, SchemaInitializer>();
ApplicationServices.TryAddSingleton<SchemaCollection>();
ApplicationServices.TryAddSingleton<GraphQLApplication>();
ApplicationServices.AddDefaultTankaGraphQLServices();

Check warning on line 63 in src/GraphQL.Server/GraphQLApplicationBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Server/GraphQLApplicationBuilder.cs#L63

Added line #L63 was not covered by tests
}
}
13 changes: 3 additions & 10 deletions src/GraphQL.Server/WebSockets/ClientMethods.cs
Original file line number Diff line number Diff line change
@@ -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<MessageBase> writer)

Check warning on line 5 in src/GraphQL.Server/WebSockets/ClientMethods.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Server/WebSockets/ClientMethods.cs#L5

Added line #L5 was not covered by tests
{
protected ChannelWriter<MessageBase> Writer { get; }

public ClientMethods(ChannelWriter<MessageBase> writer)
{
Writer = writer;
}
protected ChannelWriter<MessageBase> Writer { get; } = writer;

Check warning on line 7 in src/GraphQL.Server/WebSockets/ClientMethods.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Server/WebSockets/ClientMethods.cs#L7

Added line #L7 was not covered by tests

public async Task ConnectionAck(ConnectionAck connectionAck, CancellationToken cancellationToken)
{
Expand Down
32 changes: 30 additions & 2 deletions src/GraphQL.Server/WebSockets/ServerMethods.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,11 +23,14 @@ public ServerMethods(WebSocketMessageChannel<MessageBase> channel, GraphQLReques
_httpContext = httpContext;
Channel = channel;
Client = new ClientMethods(Channel.Writer);
_logger = httpContext.RequestServices.GetRequiredService<ILogger<ServerMethods>>();

Check warning on line 26 in src/GraphQL.Server/WebSockets/ServerMethods.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Server/WebSockets/ServerMethods.cs#L26

Added line #L26 was not covered by tests
}

public ClientMethods Client { get; set; }

public ConcurrentDictionary<string, (CancellationTokenSource Unsubscribe, Task Worker)> Subscriptions = new();

private readonly ILogger<ServerMethods> _logger;

public async Task ConnectionInit(ConnectionInit connectionInit, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -57,6 +63,7 @@ await Channel.Complete(

private async Task Execute(Subscribe subscribe, CancellationTokenSource unsubscribeOrAborted)
{
_ = _logger.BeginScope(subscribe.Id);

Check warning on line 66 in src/GraphQL.Server/WebSockets/ServerMethods.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Server/WebSockets/ServerMethods.cs#L66

Added line #L66 was not covered by tests
var cancellationToken = unsubscribeOrAborted.Token;
var context = new GraphQLRequestContext
{
Expand All @@ -73,18 +80,27 @@ private async Task Execute(Subscribe subscribe, CancellationTokenSource unsubscr

try
{
ulong count = 0;
Log.Request(_logger, subscribe.Id, context.Request);

Check warning on line 84 in src/GraphQL.Server/WebSockets/ServerMethods.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Server/WebSockets/ServerMethods.cs#L83-L84

Added lines #L83 - L84 were not covered by tests
await _requestDelegate(context);
await using var enumerator = context.Response.GetAsyncEnumerator(cancellationToken);

long started = Stopwatch.GetTimestamp();

Check warning on line 88 in src/GraphQL.Server/WebSockets/ServerMethods.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Server/WebSockets/ServerMethods.cs#L88

Added line #L88 was not covered by tests
while (await enumerator.MoveNextAsync())
{
count++;
string elapsed = $"{Stopwatch.GetElapsedTime(started).TotalMilliseconds}ms";
Log.ExecutionResult(_logger, subscribe.Id, enumerator.Current, elapsed);

Check warning on line 93 in src/GraphQL.Server/WebSockets/ServerMethods.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Server/WebSockets/ServerMethods.cs#L91-L93

Added lines #L91 - L93 were not covered by tests
await Client.Next(new Next() { Id = subscribe.Id, Payload = enumerator.Current }, cancellationToken);
started = Stopwatch.GetTimestamp();

Check warning on line 95 in src/GraphQL.Server/WebSockets/ServerMethods.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Server/WebSockets/ServerMethods.cs#L95

Added line #L95 was not covered by tests
}

if (!cancellationToken.IsCancellationRequested)
{
await Client.Complete(new Complete() { Id = subscribe.Id }, cancellationToken);
}

Log.Completed(_logger, subscribe.Id, count);

Check warning on line 103 in src/GraphQL.Server/WebSockets/ServerMethods.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL.Server/WebSockets/ServerMethods.cs#L103

Added line #L103 was not covered by tests
}
catch (OperationCanceledException)
{
Expand Down Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IAsyncValidator>();

builder.Use(next => async context =>
{
Expand Down
6 changes: 3 additions & 3 deletions src/GraphQL/Executor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ namespace Tanka.GraphQL;
/// </summary>
public partial class Executor
{
private static readonly IServiceProvider EmptyProvider = new ServiceCollection().BuildServiceProvider();

private readonly OperationDelegate _operationDelegate;

/// <summary>
Expand All @@ -29,7 +27,9 @@ public Executor(ISchema schema) : this(new ExecutorOptions { Schema = schema })
/// <param name="options"></param>
public Executor(ExecutorOptions options)
{
OperationDelegateBuilder builder = new(options.ServiceProvider ?? EmptyProvider);
OperationDelegateBuilder builder = new(options.ServiceProvider ?? new ServiceCollection()
.AddDefaultTankaGraphQLServices()
.BuildServiceProvider());

if (options.TraceEnabled)
builder.UseTrace();
Expand Down
31 changes: 31 additions & 0 deletions src/GraphQL/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<AsyncValidatorOptions>();
services.TryAddSingleton<IAsyncValidator>(p =>
new AsyncValidator(p.GetRequiredService<IOptions<AsyncValidatorOptions>>()));

return services;
}

public static IServiceCollection AddDefaultValidatorRule(this IServiceCollection services, CombineRule rule)
{
services.PostConfigure<AsyncValidatorOptions>(options => options.Rules.Add(rule));
return services;

Check warning on line 29 in src/GraphQL/ServiceCollectionExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/GraphQL/ServiceCollectionExtensions.cs#L28-L29

Added lines #L28 - L29 were not covered by tests
}
}
Loading

0 comments on commit 9fa411d

Please sign in to comment.