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.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"data":{"hero":{"name":"R2-D2","id":"2001"}}}
Original file line number Diff line number Diff line change
@@ -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}
-----
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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);
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
}
}
Original file line number Diff line number Diff line change
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.