Skip to content

Commit

Permalink
Add support for defer if argument (#5230)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib committed Jul 11, 2022
1 parent 80350d8 commit 4d468b9
Show file tree
Hide file tree
Showing 16 changed files with 348 additions and 74 deletions.
Expand Up @@ -350,6 +350,68 @@ public async Task Ensure_Multipart_Format_Is_Correct_With_Defer()
result.Content.MatchSnapshot();
}

[Fact]
public async Task Ensure_Multipart_Format_Is_Correct_With_Defer_If_Condition_True()
{
// arrange
var server = CreateStarWarsServer();

// act
var result =
await server.PostRawAsync(new ClientQueryRequest
{
Query = @"
query ($if: Boolean!){
hero(episode: NEW_HOPE)
{
name
... on Droid @defer(label: ""my_id"", if: $if)
{
id
}
}
}",
Variables = new Dictionary<string, object>
{
["if"] = true
}
});

// assert
result.Content.MatchSnapshot();
}

[Fact]
public async Task Ensure_JSON_Format_Is_Correct_With_Defer_If_Condition_False()
{
// arrange
var server = CreateStarWarsServer();

// act
var result =
await server.PostRawAsync(new ClientQueryRequest
{
Query = @"
query ($if: Boolean!){
hero(episode: NEW_HOPE)
{
name
... on Droid @defer(label: ""my_id"", if: $if)
{
id
}
}
}",
Variables = new Dictionary<string, object>
{
["if"] = false
}
});

// assert
result.Content.MatchSnapshot();
}

[Fact]
public async Task Ensure_Multipart_Format_Is_Correct_With_Stream()
{
Expand Down
@@ -0,0 +1 @@
{"data":{"hero":{"name":"R2-D2","id":"2001"}}}
@@ -0,0 +1,9 @@
---
Content-Type: application/json; charset=utf-8

{"data":{"hero":{"name":"R2-D2"}},"hasNext":true}
---
Content-Type: application/json; charset=utf-8

{"label":"my_id","path":["hero"],"data":{"id":"2001"},"hasNext":false}
-----
Expand Up @@ -26,8 +26,15 @@ internal static class DirectiveCollectionExtensions
public static bool IsDeferrable(this FragmentSpreadNode fragmentSpreadNode) =>
fragmentSpreadNode.Directives.IsDeferrable();

public static bool IsDeferrable(this IReadOnlyList<DirectiveNode> directives) =>
directives.GetDeferDirective() is not null;
public static bool IsDeferrable(this IReadOnlyList<DirectiveNode> directives)
{
var directive = directives.GetDeferDirective();
var ifValue = directive?.GetIfArgumentValueOrDefault();

// a fragment is not deferrable if we do not find a defer directive or
// if the `if` of the defer directive is a bool literal with a false value.
return directive is not null && ifValue is not BooleanValueNode { Value: false };
}

public static bool IsStreamable(this FieldNode field) =>
field.Directives.GetStreamDirective() is not null;
Expand Down Expand Up @@ -105,8 +112,7 @@ VariableNode variable
this IReadOnlyList<DirectiveNode> directives,
IVariableValueCollection variables)
{
var directiveNode =
GetDirective(directives, WellKnownDirectives.Stream);
var directiveNode = GetDirective(directives, WellKnownDirectives.Stream);

if (directiveNode is not null)
{
Expand Down Expand Up @@ -156,6 +162,21 @@ VariableNode variable
return null;
}

internal static IValueNode? GetIfArgumentValueOrDefault(this DirectiveNode directive)
{
for (var i = 0; i < directive.Arguments.Count; i++)
{
var argument = directive.Arguments[i];

if (argument.Name.Value.EqualsOrdinal(WellKnownDirectives.IfArgument))
{
return argument.Value;
}
}

return null;
}

internal static DirectiveNode? GetDeferDirective(
this IReadOnlyList<DirectiveNode> directives) =>
GetDirective(directives, WellKnownDirectives.Defer);
Expand Down
6 changes: 5 additions & 1 deletion src/HotChocolate/Core/src/Execution/Processing/Fragment.cs
Expand Up @@ -7,6 +7,7 @@ namespace HotChocolate.Execution.Processing;
internal sealed class Fragment : IFragment
{
private readonly long _includeCondition;
private readonly long _deferIfCondition;

public Fragment(
int id,
Expand All @@ -16,6 +17,7 @@ internal sealed class Fragment : IFragment
int selectionSetId,
ISelectionSet selectionSet,
long includeCondition,
long deferIfCondition,
bool isInternal = false)
{
Id = id;
Expand All @@ -25,6 +27,7 @@ internal sealed class Fragment : IFragment
SelectionSetId = selectionSetId;
SelectionSet = selectionSet;
_includeCondition = includeCondition;
_deferIfCondition = deferIfCondition;
IsInternal = isInternal;
}

Expand All @@ -42,12 +45,13 @@ internal sealed class Fragment : IFragment

public bool IsInternal { get; }

public bool IsConditional => _includeCondition is not 0;
public bool IsConditional => _includeCondition is not 0 || _deferIfCondition is not 0;

public string? GetLabel(IVariableValueCollection variables)
=> Directives.GetDeferDirective(variables)?.Label;

public bool IsIncluded(long includeFlags, bool allowInternals = false)
=> (includeFlags & _includeCondition) == _includeCondition &&
(_deferIfCondition is 0 || (includeFlags & _deferIfCondition) != _deferIfCondition) &&
(!IsInternal || allowInternals);
}
Expand Up @@ -9,7 +9,7 @@ namespace HotChocolate.Execution.Processing;
/// </summary>
public readonly struct IncludeCondition : IEquatable<IncludeCondition>
{
private IncludeCondition(IValueNode skip, IValueNode include)
internal IncludeCondition(IValueNode skip, IValueNode include)
{
Skip = skip;
Include = include;
Expand All @@ -29,9 +29,7 @@ private IncludeCondition(IValueNode skip, IValueNode include)
/// If <see cref="Skip"/> and <see cref="Include"/> are null then
/// there is no valid include condition.
/// </summary>
public bool IsDefault
=> ReferenceEquals(Skip, null) &&
ReferenceEquals(Include, null);
public bool IsDefault => Skip is null && Include is null;

/// <summary>
/// Specifies if selections with this include condition are included with the
Expand Down Expand Up @@ -127,6 +125,11 @@ public override int GetHashCode()
/// </returns>
public static IncludeCondition FromSelection(ISelectionNode selection)
{
if (selection is null)
{
throw new ArgumentNullException(nameof(selection));
}

IValueNode? skip = null;
IValueNode? include = null;

Expand Down
Expand Up @@ -278,6 +278,15 @@ private void CompleteSelectionSet(CompilerContext context)
ResolveOptimizers(context.Optimizers, selection.Field)));
}
}

// We are waiting for the latest stream and defer spec discussions to be codified
// before we change the overall stream handling.
//
// For now we only allow streams on lists of composite types.
//if (selection.SyntaxNode.IsStreamable())
// {
// var streamDirective = selection.SyntaxNode.GetStreamDirective();
// }
}

selection.SetSelectionSetId(selectionSetId);
Expand Down Expand Up @@ -462,10 +471,21 @@ private void CollectFields(CompilerContext context)
(context.Schema.TryGetTypeFromAst(typeCondition, out IType typeCon) &&
DoesTypeApply(typeCon, context.Type)))
{
includeCondition |= GetSelectionIncludeCondition(selection, includeCondition);
includeCondition = GetSelectionIncludeCondition(selection, includeCondition);

if (directives.IsDeferrable())
{
var deferDirective = directives.GetDeferDirective();
var nullValue = NullValueNode.Default;
var ifValue = deferDirective?.GetIfArgumentValueOrDefault() ?? nullValue;

long ifConditionFlags = 0;
if (ifValue.Kind is not SyntaxKind.NullValue)
{
var ifCondition = new IncludeCondition(ifValue, nullValue);
ifConditionFlags = GetSelectionIncludeCondition(ifCondition, includeCondition);
}

var id = GetOrCreateSelectionSetId(selectionSet, context.Path);
var variants = GetOrCreateSelectionVariants(id);
var infos = new SelectionSetInfo[] { new(selectionSet, includeCondition) };
Expand All @@ -485,9 +505,20 @@ private void CollectFields(CompilerContext context)
directives,
selectionSetId: id,
variants.GetSelectionSet(context.Type),
includeCondition);
includeCondition,
ifConditionFlags);

context.Fragments.Add(fragment);

// if we have if condition flags there will be a runtime validation if something
// shall be deferred, so we need to prepare for both cases.
//
// this means that we will collect the fields with our if condition flags as
// if the fragment was not deferred.
if (ifConditionFlags is not 0)
{
CollectFields(context, selectionSet, ifConditionFlags);
}
}
else
{
Expand Down Expand Up @@ -608,6 +639,44 @@ private SelectionVariants GetOrCreateSelectionVariants(int selectionSetId)
return parentIncludeCondition;
}

private long GetSelectionIncludeCondition(
IncludeCondition condition,
long parentIncludeCondition)
{
var pos = Array.IndexOf(_includeConditions, condition);

if (pos == -1)
{
pos = _includeConditions.Length;

if (pos == 64)
{
throw new InvalidOperationException(OperationCompiler_ToManyIncludeConditions);
}

if (_includeConditions.Length == 0)
{
_includeConditions = new IncludeCondition[1];
}
else
{
Array.Resize(ref _includeConditions, pos + 1);
}

_includeConditions[pos] = condition;
}

long selectionIncludeCondition = 2 ^ pos;

if (parentIncludeCondition == 0)
{
return selectionIncludeCondition;
}

parentIncludeCondition |= selectionIncludeCondition;
return parentIncludeCondition;
}

private CompilerContext RentContext(CompilerContext context)
{
if (_deferContext is null)
Expand Down Expand Up @@ -681,7 +750,7 @@ private SelectionPath(string name, SelectionPath? parent = null)

public bool Equals(SelectionPath? other)
{
if (ReferenceEquals(null, other))
if (other is null)
{
return false;
}
Expand All @@ -705,7 +774,7 @@ public bool Equals(SelectionPath? other)
}

public override bool Equals(object? obj)
=> ReferenceEquals(this, obj) || obj is SelectionPath other && Equals(other);
=> ReferenceEquals(this, obj) || (obj is SelectionPath other && Equals(other));

public override int GetHashCode()
=> HashCode.Combine(Name, Parent);
Expand Down
12 changes: 11 additions & 1 deletion src/HotChocolate/Core/src/Execution/Processing/Selection.cs
Expand Up @@ -49,6 +49,11 @@ public class Selection : ISelection
: new[] { includeCondition };

_flags = isInternal ? Flags.Internal : Flags.None;

if (Type.IsListType())
{
_flags |= Flags.List;
}
}

protected Selection(Selection selection)
Expand Down Expand Up @@ -94,6 +99,9 @@ protected Selection(Selection selection)
/// <inheritdoc />
public TypeKind TypeKind => Type.Kind;

/// <inheritdoc />
public bool IsList => (_flags & Flags.List) == Flags.List;

/// <inheritdoc />
public FieldNode SyntaxNode { get; private set; }

Expand Down Expand Up @@ -298,6 +306,8 @@ private enum Flags
{
None = 0,
Internal = 1,
Sealed = 2
Sealed = 2,
List = 4,
Stream = 8
}
}
Expand Up @@ -119,13 +119,13 @@ private async ValueTask ExecuteResolverPipelineAsync(CancellationToken cancellat
return;
}

/*
// if we are not a list we do not need any further result processing.
if (!Selection.IsList)
if (!_selection.IsList)
{
return;
}

/*
if (Selection.IsStreamable)
{
StreamDirective streamDirective =
Expand Down

0 comments on commit 4d468b9

Please sign in to comment.