Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support directives declared stitched schemas #936

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using HotChocolate.Language;
using Snapshooter.Xunit;
using Xunit;

namespace HotChocolate.Stitching.Merge.Handlers
{
public class DirectiveTypeMergeHandlerTests
{
[Fact]
public void Merge_SimpleIdenticalDirectives_TypeMerges()
{
// arrange
DocumentNode schema_a =
Parser.Default.Parse("directive @test(arg: String) on OBJECT");
DocumentNode schema_b =
Parser.Default.Parse("directive @test(arg: String) on OBJECT");

var types = new List<IDirectiveTypeInfo>
{
new DirectiveTypeInfo(schema_a.Definitions.OfType<DirectiveDefinitionNode>().First(),
new SchemaInfo("Schema_A", schema_a)),
new DirectiveTypeInfo(schema_b.Definitions.OfType<DirectiveDefinitionNode>().First(),
new SchemaInfo("Schema_B", schema_b)),
};

var context = new SchemaMergeContext();

// act
var typeMerger = new DirectiveTypeMergeHandler((c, t) => { });
typeMerger.Merge(context, types);

// assert
SchemaSyntaxSerializer.Serialize(context.CreateSchema())
.MatchSnapshot();
}

[Fact]
public void Merge_DifferentArguments_ThrowsException()
{
// arrange
DocumentNode schema_a =
Parser.Default.Parse("directive @test(arg: Int) on OBJECT");
DocumentNode schema_b =
Parser.Default.Parse("directive @test(arg: String) on OBJECT");

var types = new List<IDirectiveTypeInfo>
{
new DirectiveTypeInfo(schema_a.Definitions.OfType<DirectiveDefinitionNode>().First(),
new SchemaInfo("Schema_A", schema_a)),
new DirectiveTypeInfo(schema_b.Definitions.OfType<DirectiveDefinitionNode>().First(),
new SchemaInfo("Schema_B", schema_b))
};

var context = new SchemaMergeContext();

// act
var typeMerger = new DirectiveTypeMergeHandler((c, t) => { });

Assert.Throws<InvalidOperationException>(() => typeMerger.Merge(context, types));
}

[Fact]
public void Merge_DifferentLocations_ThrowsException()
{
// arrange
DocumentNode schema_a =
Parser.Default.Parse("directive @test(arg: String) on OBJECT | INTERFACE");
DocumentNode schema_b =
Parser.Default.Parse("directive @test(arg: String) on OBJECT");

var types = new List<IDirectiveTypeInfo>
{
new DirectiveTypeInfo(schema_a.Definitions.OfType<DirectiveDefinitionNode>().First(),
new SchemaInfo("Schema_A", schema_a)),
new DirectiveTypeInfo(schema_b.Definitions.OfType<DirectiveDefinitionNode>().First(),
new SchemaInfo("Schema_B", schema_b))
};

var context = new SchemaMergeContext();

// act
var typeMerger = new DirectiveTypeMergeHandler((c, t) => { });

Assert.Throws<InvalidOperationException>(() => typeMerger.Merge(context, types));
}

[Fact]
public void Merge_DifferentRepeatable_ThrowsException()
{
// arrange
DocumentNode schema_a =
Parser.Default.Parse("directive @test(arg: String) repeatable on OBJECT");
DocumentNode schema_b =
Parser.Default.Parse("directive @test(arg: String) on OBJECT");

var types = new List<IDirectiveTypeInfo>
{
new DirectiveTypeInfo(schema_a.Definitions.OfType<DirectiveDefinitionNode>().First(),
new SchemaInfo("Schema_A", schema_a)),
new DirectiveTypeInfo(schema_b.Definitions.OfType<DirectiveDefinitionNode>().First(),
new SchemaInfo("Schema_B", schema_b))
};

var context = new SchemaMergeContext();

// act
var typeMerger = new DirectiveTypeMergeHandler((c, t) => { });

Assert.Throws<InvalidOperationException>(() => typeMerger.Merge(context, types));
}

[Fact]
public void Merge_ThreeDirectivessWhereTwoAreIdentical_TwoTypesAfterMerge()
{
// arrange
DocumentNode schema_a =
Parser.Default.Parse("directive @test(arg: String) on OBJECT");
DocumentNode schema_b =
Parser.Default.Parse("directive @test1(arg: String) on OBJECT");
DocumentNode schema_c =
Parser.Default.Parse("directive @test(arg: String) on OBJECT");

var types = new List<IDirectiveTypeInfo>
{
new DirectiveTypeInfo(schema_a.Definitions.OfType<DirectiveDefinitionNode>().First(),
new SchemaInfo("Schema_A", schema_a)),
new DirectiveTypeInfo(schema_b.Definitions.OfType<DirectiveDefinitionNode>().First(),
new SchemaInfo("Schema_B", schema_b)),
new DirectiveTypeInfo(schema_c.Definitions.OfType<DirectiveDefinitionNode>().First(),
new SchemaInfo("Schema_C", schema_c))
};

var context = new SchemaMergeContext();

// act
var typeMerger = new DirectiveTypeMergeHandler((c, t) => { });
typeMerger.Merge(context, types);

// assert
SchemaSyntaxSerializer.Serialize(context.CreateSchema())
.MatchSnapshot();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
directive @test(arg: String) on OBJECT
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
directive @test(arg: String) on OBJECT

directive @test1(arg: String) on OBJECT
Original file line number Diff line number Diff line change
Expand Up @@ -679,8 +679,6 @@ public async Task CustomDirectiveIsPassedOn()
serviceCollection.AddStitchedSchema(builder =>
builder.AddSchemaFromHttp("contract")
.AddSchemaFromHttp("customer")
.AddExtensionsFromString(
"directive @custom(d: DateTime) on FIELD")
.AddSchemaConfiguration(c =>
{
c.RegisterExtendedScalarTypes();
Expand Down Expand Up @@ -728,8 +726,6 @@ public async Task DateTimeIsHandledCorrectly()
serviceCollection.AddStitchedSchema(builder =>
builder.AddSchemaFromHttp("contract")
.AddSchemaFromHttp("customer")
.AddExtensionsFromString(
"directive @custom(d: DateTime) on FIELD")
.AddSchemaConfiguration(c =>
{
c.RegisterExtendedScalarTypes();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
}

type Query {
foo: String @delegate(schema: "server_1")
foo: String @bar @delegate(schema: "server_1")
}

directive @bar on FIELD_DEFINITION

directive @delegate(path: String "The name of the schema to which this field shall be delegated to." schema: Name!) on FIELD_DEFINITION

"The name scalar represents a valid GraphQL name as specified in the spec and can be used to refer to fields or types."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,17 @@ interface Contract @source(name: "Contract", schema: "contract") {
customerId: ID!
id: ID!
}

"Directs the executor to skip this field or fragment when the `if` argument is true."
directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

"Directs the executor to include this field or fragment only when the `if` argument is true."
directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

"The cost directives is used to express the complexity of a field."
directive @cost("Defines the complexity of the field." complexity: Int! = 1 "Defines field arguments that act as complexity multipliers." multipliers: [MultiplierPath!]) on FIELD_DEFINITION

"The @deprecated directive is used within the type system definition language to indicate deprecated portions of a GraphQL service’s schema,such as deprecated fields on a type or deprecated enum values."
directive @deprecated("Deprecations include a reason for why it is deprecated, which is formatted using Markdown syntax (as specified by CommonMark)." reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE

directive @custom(d: DateTime) on FIELD
19 changes: 19 additions & 0 deletions src/Stitching/Stitching/Merge/DirectiveTypeInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using HotChocolate.Language;

namespace HotChocolate.Stitching.Merge
{
public class DirectiveTypeInfo : IDirectiveTypeInfo
{
public DirectiveTypeInfo(
DirectiveDefinitionNode definition,
ISchemaInfo schema)
{
Definition = definition;
Schema = schema;
}

public DirectiveDefinitionNode Definition { get; }

public ISchemaInfo Schema { get; }
}
}
33 changes: 19 additions & 14 deletions src/Stitching/Stitching/Merge/Handlers/ComplexTypeMergeHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,29 @@ private static bool HasSameShape(
&& HasSameType(left.Type, right.Type)
&& left.Arguments.Count == right.Arguments.Count)
{
Dictionary<string, InputValueDefinitionNode> leftArgs =
left.Arguments.ToDictionary(t => t.Name.Value);
Dictionary<string, InputValueDefinitionNode> rightArgs =
right.Arguments.ToDictionary(t => t.Name.Value);
return HasSameArguments(left.Arguments, right.Arguments);
}
return false;
}

foreach (string name in leftArgs.Keys)
public static bool HasSameArguments(
IReadOnlyList<InputValueDefinitionNode> left,
IReadOnlyList<InputValueDefinitionNode> right)
{
var leftArgs = left.ToDictionary(t => t.Name.Value);
var rightArgs = right.ToDictionary(t => t.Name.Value);

foreach (string name in leftArgs.Keys)
{
InputValueDefinitionNode leftArgument = leftArgs[name];
if (!rightArgs.TryGetValue(name,
out InputValueDefinitionNode rightArgument)
|| !HasSameType(leftArgument.Type, rightArgument.Type))
{
InputValueDefinitionNode leftArgument = leftArgs[name];
if (!rightArgs.TryGetValue(name,
out InputValueDefinitionNode rightArgument)
|| !HasSameType(leftArgument.Type, rightArgument.Type))
{
return false;
}
return false;
}
return true;
}
return false;
return true;
}

private static bool HasSameType(ITypeNode left, ITypeNode right)
Expand Down
107 changes: 107 additions & 0 deletions src/Stitching/Stitching/Merge/Handlers/DirectiveTypeMergeHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Linq;
using HotChocolate.Language;

namespace HotChocolate.Stitching.Merge.Handlers
{
internal class DirectiveTypeMergeHandler
{
private readonly MergeDirectiveRuleDelegate _next;

public DirectiveTypeMergeHandler(MergeDirectiveRuleDelegate next)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
}

public void Merge(
ISchemaMergeContext context,
IReadOnlyList<IDirectiveTypeInfo> types)
{
var notMerged = types.ToList();

while (notMerged.Count > 0)
{
MergeNextType(context, notMerged);
}
}

private void MergeNextType(
ISchemaMergeContext context,
List<IDirectiveTypeInfo> notMerged)
{
IDirectiveTypeInfo left = notMerged[0];

var readyToMerge = new List<IDirectiveTypeInfo>();
readyToMerge.Add(left);

for (int i = 1; i < notMerged.Count; i++)
{
if (CanBeMerged(left.Definition, notMerged[i].Definition))
{
readyToMerge.Add(notMerged[i]);
}
}

NameString name = readyToMerge[0].Definition.Name.Value;

if (context.ContainsDirective(name))
{
throw new InvalidOperationException($"Unable to merge {name}, directive with this name already exists.");
}

MergeTypes(context, readyToMerge, name);
notMerged.RemoveAll(readyToMerge.Contains);
}

protected void MergeTypes(
ISchemaMergeContext context,
IReadOnlyList<IDirectiveTypeInfo> types,
NameString newTypeName)
{
var definitions = types
.Select(t => t.Definition)
.ToList();

DirectiveDefinitionNode definition =
definitions[0].Rename(
newTypeName,
types.Select(t => t.Schema.Name));

context.AddDirective(definition);
}

private static bool CanBeMerged(DirectiveDefinitionNode left, DirectiveDefinitionNode right)
{
if (!left.Name.Value.Equals(right.Name.Value, StringComparison.Ordinal))
{
return false;
}

if (left.Locations.Count != right.Locations.Count)
{
return false;
}

var leftLocations = left.Locations.Select(l => l.Value).OrderBy(l => l).ToList();
var rightLocations = right.Locations.Select(l => l.Value).OrderBy(l => l).ToList();

if (!leftLocations.SequenceEqual(rightLocations))
{
return false;
}

if (left.IsRepeatable != right.IsRepeatable)
{
return false;
}

if (left.Arguments.Count != right.Arguments.Count)
{
return false;
}

return ComplexTypeMergeHelpers.HasSameArguments(left.Arguments, right.Arguments);
}
}
}
10 changes: 10 additions & 0 deletions src/Stitching/Stitching/Merge/IDirectiveTypeInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using HotChocolate.Language;

namespace HotChocolate.Stitching.Merge
{
public interface IDirectiveTypeInfo
{
DirectiveDefinitionNode Definition { get; }
ISchemaInfo Schema { get; }
}
}
8 changes: 8 additions & 0 deletions src/Stitching/Stitching/Merge/MergeDirectiveRuleDelegate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Collections.Generic;

namespace HotChocolate.Stitching.Merge
{
public delegate void MergeDirectiveRuleDelegate(
ISchemaMergeContext context,
IReadOnlyList<IDirectiveTypeInfo> types);
}
Loading