From 0379ff68a76cba911ffa8cd29d621606af72d559 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 21 Nov 2020 09:29:46 +0100 Subject: [PATCH] Refined Interface Handling (#2627) --- .../TypeInitializationInterceptor.cs | 5 +- .../src/Types/Configuration/TypeTrimmer.cs | 2 +- .../ComplexOutputTypeValidationHelper.cs | 6 +- ...nterfaceHasAtLeastOneImplementationRule.cs | 2 +- .../Types/Internal/TypeDependencyHelper.cs | 4 +- .../Properties/TypeResources.Designer.cs | 6 + .../src/Types/Properties/TypeResources.resx | 3 + .../Core/src/Types/SchemaBuilder.cs | 50 ++--- .../Core/src/Types/SchemaSerializer.cs | 8 +- .../Core/src/Types/SchemaTypes.cs | 2 +- .../Types/Contracts/IComplexOutputType.cs | 2 +- .../IComplexOutputTypeDefinition.cs | 2 + .../Definitions/ObjectTypeDefinition.cs | 2 + .../Definitions/TypeDefinitionBase~1.cs | 6 +- .../Types/Descriptors/ObjectTypeDescriptor.cs | 23 +- .../ObjectTypeExtensionDescriptor~1.cs | 1 + .../Types/Helpers/CompleteInterfacesHelper.cs | 35 +-- .../InterfaceCompletionTypeInterceptor.cs | 212 ++++++++++++++++++ .../Core/src/Types/Types/InterfaceType.cs | 22 +- .../Core/src/Types/Types/InterfaceType~1.cs | 2 +- .../src/Types/Types/Introspection/__Type.cs | 2 +- .../Core/src/Types/Types/ObjectType.cs | 27 ++- .../Core/src/Types/Types/Relay/NodeType.cs | 13 +- .../Core/test/Types.Tests/CodeFirstTests.cs | 51 ++++- .../Validation/InterfaceTypeValidation.cs | 5 +- .../Validation/ObjectTypeValidation.cs | 3 +- ...peValidation.Field_Is_Not_Implemented.snap | 27 +++ ..._Not_The_Interfaces_Of_Its_Interfaces.snap | 10 +- ...ot_The_Interfaces_Of_Its_Interfaces_2.snap | 10 +- .../Types.Tests/Types/InterfaceTypeTests.cs | 2 +- .../test/Types.Tests/Types/ObjectTypeTests.cs | 28 +-- .../CodeFirstTests.Infer_Interface_Usage.snap | 31 +++ ...th_Interfaces_Implementing_Interfaces.snap | 36 +++ .../Legacy/Visitors/SchemaSyntaxSerializer.cs | 10 + .../Parser/Utf8QueryParserTests.cs | 18 ++ ...ParserTests.ParserSimpleInterfaceType.snap | 2 +- .../Schemas/Contracts/ContractType.cs | 3 +- .../Schemas/Contracts/CustomDirectiveType.cs | 3 +- .../Contracts/LifeInsuranceContract.cs | 3 +- .../Contracts/LifeInsuranceContractType.cs | 3 +- .../Schemas/Contracts/QueryType.cs | 3 +- .../Schemas/Contracts/SomeOtherContract.cs | 3 +- .../Contracts/SomeOtherContractType.cs | 3 +- .../SchemaTests.ContractSchemaSnapshot.snap | 2 +- 44 files changed, 518 insertions(+), 175 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Types/InterfaceCompletionTypeInterceptor.cs create mode 100644 src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/InterfaceTypeValidation.Field_Is_Not_Implemented.snap create mode 100644 src/HotChocolate/Core/test/Types.Tests/__snapshots__/CodeFirstTests.Infer_Interface_Usage.snap create mode 100644 src/HotChocolate/Core/test/Types.Tests/__snapshots__/CodeFirstTests.Infer_Interface_Usage_With_Interfaces_Implementing_Interfaces.snap diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeInitializationInterceptor.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeInitializationInterceptor.cs index d2eba8b12cc..507ceaacf38 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeInitializationInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeInitializationInterceptor.cs @@ -5,8 +5,7 @@ namespace HotChocolate.Configuration { - public class TypeInterceptor - : ITypeInitializationInterceptor + public class TypeInterceptor : ITypeInitializationInterceptor { public virtual bool TriggerAggregations => false; @@ -23,7 +22,7 @@ public class TypeInterceptor IDictionary contextData) { } - + public virtual void OnTypesInitialized( IReadOnlyCollection discoveryContexts) { diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeTrimmer.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeTrimmer.cs index 35140d2a971..4a9d7232dce 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeTrimmer.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeTrimmer.cs @@ -110,7 +110,7 @@ private void VisitObject(ObjectType type) { VisitDirectives(type); - foreach (InterfaceType interfaceType in type.Interfaces) + foreach (InterfaceType interfaceType in type.Implements) { VisitInterface(interfaceType); } diff --git a/src/HotChocolate/Core/src/Types/Configuration/Validation/ComplexOutputTypeValidationHelper.cs b/src/HotChocolate/Core/src/Types/Configuration/Validation/ComplexOutputTypeValidationHelper.cs index ba02a7b9f7a..11f7209c9e5 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/Validation/ComplexOutputTypeValidationHelper.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/Validation/ComplexOutputTypeValidationHelper.cs @@ -55,9 +55,9 @@ internal static class ComplexOutputTypeValidationHelper IComplexOutputType type, ICollection errors) { - if (type.Interfaces.Count > 0) + if (type.Implements.Count > 0) { - foreach (IInterfaceType implementedType in type.Interfaces) + foreach (IInterfaceType implementedType in type.Implements) { ValidateImplementation(type, implementedType, errors); } @@ -131,7 +131,7 @@ internal static class ComplexOutputTypeValidationHelper IComplexOutputType type, IInterfaceType implementedType) { - foreach (var interfaceType in implementedType.Interfaces) + foreach (var interfaceType in implementedType.Implements) { if (!type.IsImplementing(interfaceType)) { diff --git a/src/HotChocolate/Core/src/Types/Configuration/Validation/InterfaceHasAtLeastOneImplementationRule.cs b/src/HotChocolate/Core/src/Types/Configuration/Validation/InterfaceHasAtLeastOneImplementationRule.cs index 8a1f9cd7ce2..159550b6bd4 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/Validation/InterfaceHasAtLeastOneImplementationRule.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/Validation/InterfaceHasAtLeastOneImplementationRule.cs @@ -25,7 +25,7 @@ internal class InterfaceHasAtLeastOneImplementationRule foreach (ObjectType objectType in typeSystemObjects.OfType()) { - foreach (InterfaceType interfaceType in objectType.Interfaces) + foreach (InterfaceType interfaceType in objectType.Implements) { interfaceTypes.Remove(interfaceType); } diff --git a/src/HotChocolate/Core/src/Types/Internal/TypeDependencyHelper.cs b/src/HotChocolate/Core/src/Types/Internal/TypeDependencyHelper.cs index 9a931af19f7..c7e3d18a11b 100644 --- a/src/HotChocolate/Core/src/Types/Internal/TypeDependencyHelper.cs +++ b/src/HotChocolate/Core/src/Types/Internal/TypeDependencyHelper.cs @@ -25,7 +25,7 @@ public static class TypeDependencyHelper context.RegisterDependencyRange( definition.Interfaces, - TypeDependencyKind.Default); + TypeDependencyKind.Completed); RegisterAdditionalDependencies(context, definition); RegisterDirectiveDependencies(context, definition); @@ -79,7 +79,7 @@ public static class TypeDependencyHelper context.RegisterDependencyRange( definition.Interfaces, - TypeDependencyKind.Default); + TypeDependencyKind.Completed); RegisterAdditionalDependencies(context, definition); RegisterDirectiveDependencies(context, definition); diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs index 34cd0bbe126..df9969c4e61 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs @@ -1136,5 +1136,11 @@ internal class TypeResources { return ResourceManager.GetString("SpecifiedByDirectiveType_UrlDescription", resourceCulture); } } + + internal static string NodeType_TypeDescription { + get { + return ResourceManager.GetString("NodeType_TypeDescription", resourceCulture); + } + } } } diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx index 57aae417d26..c782723cc97 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx @@ -668,4 +668,7 @@ Type: `{0}` The specifiedBy URL points to a human-readable specification. This field will only read a result for scalar types. + + The node interface is implemented by entities that have a global unique identifier. + diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs index 6e43965efb0..b688ed5bdd2 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs @@ -18,29 +18,23 @@ public partial class SchemaBuilder : ISchemaBuilder { private delegate ITypeReference CreateRef(ITypeInspector typeInspector); - private readonly Dictionary _contextData = - new Dictionary(); - private readonly List _globalComponents = - new List(); - private readonly List _documents = - new List(); - private readonly List _types = new List(); - private readonly List _resolverTypes = new List(); - private readonly Dictionary _operations = - new Dictionary(); - private readonly Dictionary _resolvers = - new Dictionary(); - private readonly Dictionary<(Type, string), List> _conventions = - new Dictionary<(Type, string), List>(); - private readonly Dictionary _clrTypes = - new Dictionary(); - private readonly List _schemaInterceptors = new List(); - private readonly List _typeInterceptors = new List + private readonly Dictionary _contextData = new(); + private readonly List _globalComponents = new(); + private readonly List _documents = new(); + private readonly List _types = new(); + private readonly List _resolverTypes = new(); + private readonly Dictionary _operations = new(); + private readonly Dictionary _resolvers = new(); + private readonly Dictionary<(Type, string), List> _conventions = new(); + private readonly Dictionary _clrTypes = new(); + private readonly List _schemaInterceptors = new(); + private readonly List _typeInterceptors = new() { - typeof(IntrospectionTypeInterceptor) + typeof(IntrospectionTypeInterceptor), + typeof(InterfaceCompletionTypeInterceptor) }; private readonly IBindingCompiler _bindingCompiler = new BindingCompiler(); - private SchemaOptions _options = new SchemaOptions(); + private SchemaOptions _options = new(); private IsOfTypeFallback _isOfType; private IServiceProvider _services; private CreateRef _schema; @@ -74,7 +68,7 @@ public ISchemaBuilder SetSchema(ISchema schema) if (schema is TypeSystemObjectBase) { - _schema = ti => new SchemaTypeReference(schema); + _schema = _ => new SchemaTypeReference(schema); } else { @@ -92,7 +86,7 @@ public ISchemaBuilder SetSchema(Action configure) throw new ArgumentNullException(nameof(configure)); } - _schema = ti => new SchemaTypeReference(new Schema(configure)); + _schema = _ => new SchemaTypeReference(new Schema(configure)); return this; } @@ -255,7 +249,7 @@ public ISchemaBuilder AddType(INamedType type) throw new ArgumentNullException(nameof(type)); } - _types.Add(ti => TypeReference.Create(type)); + _types.Add(_ => TypeReference.Create(type)); return this; } @@ -266,7 +260,7 @@ public ISchemaBuilder AddType(INamedTypeExtension type) throw new ArgumentNullException(nameof(type)); } - _types.Add(ti => TypeReference.Create(type)); + _types.Add(_ => TypeReference.Create(type)); return this; } @@ -277,7 +271,7 @@ public ISchemaBuilder AddDirectiveType(DirectiveType type) throw new ArgumentNullException(nameof(type)); } - _types.Add(ti => TypeReference.Create(type)); + _types.Add(_ => TypeReference.Create(type)); return this; } @@ -345,8 +339,8 @@ public ISchemaBuilder AddDirectiveType(DirectiveType type) } SchemaTypeReference reference = TypeReference.Create(type); - _operations.Add(operation, ti => reference); - _types.Add(ti => reference); + _operations.Add(operation, _ => reference); + _types.Add(_ => reference); return this; } @@ -491,6 +485,6 @@ public ISchemaBuilder TryAddSchemaInterceptor(ISchemaInterceptor interceptor) return this; } - public static SchemaBuilder New() => new SchemaBuilder(); + public static SchemaBuilder New() => new(); } } diff --git a/src/HotChocolate/Core/src/Types/SchemaSerializer.cs b/src/HotChocolate/Core/src/Types/SchemaSerializer.cs index 2fc2674c64b..79e930740d4 100644 --- a/src/HotChocolate/Core/src/Types/SchemaSerializer.cs +++ b/src/HotChocolate/Core/src/Types/SchemaSerializer.cs @@ -196,7 +196,7 @@ namedType switch .Select(SerializeDirective) .ToList(); - var interfaces = objectType.Interfaces + var interfaces = objectType.Implements .Select(SerializeNamedType) .ToList(); @@ -223,6 +223,10 @@ namedType switch .Select(SerializeDirective) .ToList(); + var interfaces = interfaceType.Implements + .Select(SerializeNamedType) + .ToList(); + var fields = interfaceType.Fields .Select(SerializeObjectField) .ToList(); @@ -233,7 +237,7 @@ namedType switch new NameNode(interfaceType.Name), SerializeDescription(interfaceType.Description), directives, - Array.Empty(), + interfaces, fields ); } diff --git a/src/HotChocolate/Core/src/Types/SchemaTypes.cs b/src/HotChocolate/Core/src/Types/SchemaTypes.cs index 8077e77a66f..49c8449645c 100644 --- a/src/HotChocolate/Core/src/Types/SchemaTypes.cs +++ b/src/HotChocolate/Core/src/Types/SchemaTypes.cs @@ -101,7 +101,7 @@ public bool TryGetClrType(NameString typeName, out Type clrType) { possibleTypes[objectType.Name] = new List { objectType }; - foreach (InterfaceType interfaceType in objectType.Interfaces) + foreach (InterfaceType interfaceType in objectType.Implements) { if (!possibleTypes.TryGetValue(interfaceType.Name, out List pt)) { diff --git a/src/HotChocolate/Core/src/Types/Types/Contracts/IComplexOutputType.cs b/src/HotChocolate/Core/src/Types/Types/Contracts/IComplexOutputType.cs index bd7e776e5ed..b0fb8d9d5d5 100644 --- a/src/HotChocolate/Core/src/Types/Types/Contracts/IComplexOutputType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Contracts/IComplexOutputType.cs @@ -13,7 +13,7 @@ public interface IComplexOutputType /// /// Gets the interfaces that are implemented by this type. /// - IReadOnlyList Interfaces { get; } + IReadOnlyList Implements { get; } /// /// Gets the field that this type exposes. diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/IComplexOutputTypeDefinition.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/IComplexOutputTypeDefinition.cs index e5dfd705e29..3889953b12a 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/IComplexOutputTypeDefinition.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/IComplexOutputTypeDefinition.cs @@ -7,6 +7,8 @@ namespace HotChocolate.Types.Descriptors.Definitions { public interface IComplexOutputTypeDefinition { + NameString Name { get; } + Type RuntimeType { get; } IList KnownClrTypes { get; } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ObjectTypeDefinition.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ObjectTypeDefinition.cs index 9339a5354e8..c7dff232965 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ObjectTypeDefinition.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ObjectTypeDefinition.cs @@ -24,6 +24,8 @@ public override Type RuntimeType public IsOfType IsOfType { get; set; } + public bool IsExtension { get; set; } + public IList Interfaces { get; } = new List(); diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/TypeDefinitionBase~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/TypeDefinitionBase~1.cs index e5132e83445..375ee4ab032 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/TypeDefinitionBase~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/TypeDefinitionBase~1.cs @@ -21,11 +21,7 @@ public virtual Type RuntimeType get => _clrType; set { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - _clrType = value; + _clrType = value ?? throw new ArgumentNullException(nameof(value)); } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs index bc833ed3494..e8674f53091 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs @@ -349,38 +349,37 @@ public IObjectTypeDescriptor Directive() public static ObjectTypeDescriptor New( IDescriptorContext context) => - new ObjectTypeDescriptor(context); + new(context); public static ObjectTypeDescriptor New( IDescriptorContext context, Type clrType) => - new ObjectTypeDescriptor(context, clrType); + new(context, clrType); public static ObjectTypeDescriptor New( IDescriptorContext context) => - new ObjectTypeDescriptor(context); + new(context); public static ObjectTypeExtensionDescriptor NewExtension( IDescriptorContext context) => - new ObjectTypeExtensionDescriptor(context); + new(context); public static ObjectTypeDescriptor FromSchemaType( IDescriptorContext context, - Type schemaType) - { - var descriptor = new ObjectTypeDescriptor(context, schemaType); - descriptor.Definition.RuntimeType = typeof(object); - return descriptor; - } + Type schemaType) => + new ObjectTypeDescriptor(context, schemaType) + { + Definition = { RuntimeType = typeof(object) } + }; public static ObjectTypeDescriptor From( IDescriptorContext context, ObjectTypeDefinition definition) => - new ObjectTypeDescriptor(context, definition); + new(context, definition); public static ObjectTypeDescriptor From( IDescriptorContext context, ObjectTypeDefinition definition) => - new ObjectTypeDescriptor(context, definition); + new(context, definition); } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeExtensionDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeExtensionDescriptor~1.cs index e7dd2e6d0a1..91ad09a75bb 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeExtensionDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeExtensionDescriptor~1.cs @@ -12,6 +12,7 @@ protected internal ObjectTypeExtensionDescriptor(IDescriptorContext context) Definition.Description = context.Naming.GetTypeDescription(typeof(T), TypeKind.Object); Definition.Fields.BindingBehavior = context.Options.DefaultBindingBehavior; Definition.FieldBindingType = typeof(T); + Definition.IsExtension = true; } } } diff --git a/src/HotChocolate/Core/src/Types/Types/Helpers/CompleteInterfacesHelper.cs b/src/HotChocolate/Core/src/Types/Types/Helpers/CompleteInterfacesHelper.cs index d72d40ff22d..f086964e3f8 100644 --- a/src/HotChocolate/Core/src/Types/Types/Helpers/CompleteInterfacesHelper.cs +++ b/src/HotChocolate/Core/src/Types/Types/Helpers/CompleteInterfacesHelper.cs @@ -13,7 +13,7 @@ namespace HotChocolate.Types { internal static class CompleteInterfacesHelper { - public static void Complete( + public static void CompleteInterfaces( ITypeCompletionContext context, IComplexOutputTypeDefinition definition, Type clrType, @@ -21,21 +21,6 @@ internal static class CompleteInterfacesHelper ITypeSystemObject interfaceOrObject, ISyntaxNode? node) { - if (clrType != typeof(object)) - { - TryInferInterfaceUsageFromClrType(context, clrType, interfaces); - } - - if (definition.KnownClrTypes.Count > 0) - { - definition.KnownClrTypes.Remove(typeof(object)); - - foreach (Type type in definition.KnownClrTypes.Distinct()) - { - TryInferInterfaceUsageFromClrType(context, type, interfaces); - } - } - foreach (ITypeReference interfaceRef in definition.Interfaces) { if (!context.TryGetType(interfaceRef, out InterfaceType type)) @@ -51,23 +36,5 @@ internal static class CompleteInterfacesHelper } } } - - private static void TryInferInterfaceUsageFromClrType( - ITypeCompletionContext context, - Type clrType, - ICollection interfaces) - { - foreach (Type interfaceType in clrType.GetInterfaces()) - { - if (context.TryGetType( - context.DescriptorContext.TypeInspector.GetTypeRef( - interfaceType, TypeContext.Output), - out InterfaceType type) && - !interfaces.Contains(type)) - { - interfaces.Add(type); - } - } - } } } diff --git a/src/HotChocolate/Core/src/Types/Types/InterfaceCompletionTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/InterfaceCompletionTypeInterceptor.cs new file mode 100644 index 00000000000..120d25264dd --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/InterfaceCompletionTypeInterceptor.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HotChocolate.Configuration; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Definitions; + +#nullable enable + +namespace HotChocolate.Types +{ + internal class InterfaceCompletionTypeInterceptor : TypeInterceptor + { + private readonly Dictionary _typeInfos = new(); + private readonly HashSet _allInterfaceRuntimeTypes = new(); + private readonly HashSet _interfaceRuntimeTypes = new(); + private readonly HashSet _completed = new(); + private readonly HashSet _completedFields = new(); + private readonly Queue _backlog = new(); + + public override bool TriggerAggregations => true; + + public override void OnAfterInitialize( + ITypeDiscoveryContext discoveryContext, + DefinitionBase? definition, + IDictionary contextData) + { + // we need to preserve the initialization context of all + // interface types and object types. + if (definition is IComplexOutputTypeDefinition typeDefinition) + { + _typeInfos.Add(discoveryContext.Type, new(discoveryContext, typeDefinition)); + } + } + + public override void OnTypesInitialized( + IReadOnlyCollection discoveryContexts) + { + // after all types have been initialized we will index the runtime + // types of all interfaces. + foreach (TypeInfo interfaceTypeInfo in _typeInfos.Values + .Where(t => t.Definition.RuntimeType is { } rt && + rt != typeof(object) && + t.Definition is InterfaceTypeDefinition)) + { + _allInterfaceRuntimeTypes.Add(interfaceTypeInfo.Definition.RuntimeType); + } + + // we now will use the runtime types to infer interface usage ... + foreach (TypeInfo typeInfo in _typeInfos.Values.Where(IsRelevant)) + { + _interfaceRuntimeTypes.Clear(); + + TryInferInterfaceFromRuntimeType( + GetRuntimeType(typeInfo), + _allInterfaceRuntimeTypes, + _interfaceRuntimeTypes); + + if (_interfaceRuntimeTypes.Count > 0) + { + // if we detect that this type implements an interface, + // we will register it as a dependency. + foreach (var typeDependency in _interfaceRuntimeTypes.Select( + t => new TypeDependency( + TypeReference.Create( + typeInfo.Context.TypeInspector.GetType(t), + TypeContext.Output), + TypeDependencyKind.Completed))) + { + typeInfo.Context.RegisterDependency(typeDependency); + typeInfo.Definition.Interfaces.Add(typeDependency.TypeReference); + } + } + } + } + + // defines if this type has a concrete runtime type. + private bool IsRelevant(TypeInfo typeInfo) + { + if (typeInfo.Definition is ObjectTypeDefinition {IsExtension: true} objectDef && + objectDef.FieldBindingType != typeof(object)) + { + return true; + } + + Type? runtimeType = typeInfo.Definition.RuntimeType; + return runtimeType is not null! && runtimeType != typeof(object); + } + + private Type GetRuntimeType(TypeInfo typeInfo) + { + if (typeInfo.Definition is ObjectTypeDefinition {IsExtension: true} objectDef) + { + return objectDef.FieldBindingType; + } + + return typeInfo.Definition.RuntimeType; + } + + public override void OnBeforeCompleteType( + ITypeCompletionContext completionContext, + DefinitionBase? definition, + IDictionary contextData) + { + if (definition is InterfaceTypeDefinition { Interfaces: { Count: > 0 } } typeDef) + { + _completed.Clear(); + _completedFields.Clear(); + _backlog.Clear(); + + foreach (var interfaceRef in typeDef.Interfaces) + { + if (completionContext.TryGetType(interfaceRef, out InterfaceType interfaceType)) + { + _completed.Add(interfaceType.Name); + _backlog.Enqueue(interfaceType); + } + } + + foreach (var field in typeDef.Fields) + { + _completedFields.Add(field.Name); + } + + CompleteInterfacesAndFields(typeDef); + } + + if (definition is ObjectTypeDefinition { Interfaces: { Count: > 0 } } objectTypeDef) + { + _completed.Clear(); + _completedFields.Clear(); + _backlog.Clear(); + + foreach (var interfaceRef in objectTypeDef.Interfaces) + { + if (completionContext.TryGetType(interfaceRef, out InterfaceType interfaceType)) + { + _completed.Add(interfaceType.Name); + _backlog.Enqueue(interfaceType); + } + } + + foreach (var field in objectTypeDef.Fields) + { + _completedFields.Add(field.Name); + } + + CompleteInterfacesAndFields(objectTypeDef); + } + } + + private void CompleteInterfacesAndFields(IComplexOutputTypeDefinition definition) + { + while(_backlog.Count > 0) + { + InterfaceType current = _backlog.Dequeue(); + TypeInfo typeInfo = _typeInfos[current]; + definition.Interfaces.Add(TypeReference.Create(current)); + + if (definition is InterfaceTypeDefinition interfaceDef) + { + foreach (var field in ((InterfaceTypeDefinition)typeInfo.Definition).Fields) + { + if (_completedFields.Add(field.Name)) + { + interfaceDef.Fields.Add(field); + } + } + } + + foreach (var interfaceType in current.Implements) + { + if (_completed.Add(interfaceType.Name)) + { + _backlog.Enqueue(interfaceType); + } + } + } + } + + private static void TryInferInterfaceFromRuntimeType( + Type runtimeType, + ICollection allInterfaces, + ICollection interfaces) + { + foreach (Type interfaceType in runtimeType.GetInterfaces()) + { + if (allInterfaces.Contains(interfaceType)) + { + interfaces.Add(interfaceType); + } + } + } + + private readonly struct TypeInfo + { + public TypeInfo( + ITypeDiscoveryContext context, + IComplexOutputTypeDefinition definition) + { + Context = context; + Definition = definition; + } + + public ITypeDiscoveryContext Context { get; } + + public IComplexOutputTypeDefinition Definition { get; } + + public override string? ToString() => Definition.Name; + } + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/InterfaceType.cs b/src/HotChocolate/Core/src/Types/Types/InterfaceType.cs index 6b6a11ae005..caf108bc033 100644 --- a/src/HotChocolate/Core/src/Types/Types/InterfaceType.cs +++ b/src/HotChocolate/Core/src/Types/Types/InterfaceType.cs @@ -7,6 +7,7 @@ using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; +using static HotChocolate.Types.CompleteInterfacesHelper; #nullable enable @@ -15,9 +16,8 @@ namespace HotChocolate.Types public class InterfaceType : NamedTypeBase , IInterfaceType - , IHasRuntimeType { - private readonly List _interfaces = new List(); + private readonly List _implements = new(); private Action? _configure; private ResolveAbstractType? _resolveAbstractType; @@ -37,9 +37,9 @@ public InterfaceType(Action configure) ISyntaxNode? IHasSyntaxNode.SyntaxNode => SyntaxNode; - public IReadOnlyList Interfaces => _interfaces; + public IReadOnlyList Implements => _implements; - IReadOnlyList IComplexOutputType.Interfaces => _interfaces; + IReadOnlyList IComplexOutputType.Implements => _implements; public InterfaceTypeDefinitionNode? SyntaxNode { get; private set; } @@ -48,13 +48,13 @@ public InterfaceType(Action configure) IFieldCollection IComplexOutputType.Fields => Fields; public bool IsImplementing(NameString interfaceTypeName) => - _interfaces.Any(t => t.Name.Equals(interfaceTypeName)); + _implements.Any(t => t.Name.Equals(interfaceTypeName)); public bool IsImplementing(InterfaceType interfaceType) => - _interfaces.IndexOf(interfaceType) != -1; + _implements.IndexOf(interfaceType) != -1; public bool IsImplementing(IInterfaceType interfaceType) => - interfaceType is InterfaceType i && _interfaces.IndexOf(i) != -1; + interfaceType is InterfaceType i && _implements.IndexOf(i) != -1; public override bool IsAssignableFrom(INamedType namedType) { @@ -125,12 +125,8 @@ protected virtual void Configure(IInterfaceTypeDescriptor descriptor) definition.Fields.Select(t => new InterfaceField(t, sortFieldsByName)), sortFieldsByName); - CompleteAbstractTypeResolver( - context, - definition.ResolveAbstractType); - - CompleteInterfacesHelper.Complete( - context, definition, RuntimeType, _interfaces, this, SyntaxNode); + CompleteAbstractTypeResolver(context, definition.ResolveAbstractType); + CompleteInterfaces(context, definition, RuntimeType, _implements, this, SyntaxNode); FieldInitHelper.CompleteFields(context, definition, Fields); } diff --git a/src/HotChocolate/Core/src/Types/Types/InterfaceType~1.cs b/src/HotChocolate/Core/src/Types/Types/InterfaceType~1.cs index 49db56e92bd..17c2b471833 100644 --- a/src/HotChocolate/Core/src/Types/Types/InterfaceType~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/InterfaceType~1.cs @@ -8,7 +8,7 @@ namespace HotChocolate.Types public class InterfaceType : InterfaceType { - private Action> _configure; + private readonly Action> _configure; public InterfaceType() { diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs index c980c9fb9f7..97b55877475 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs @@ -95,7 +95,7 @@ private class Resolvers } public IEnumerable? GetInterfaces([Parent] IType type) => - type is IComplexOutputType complexType ? complexType.Interfaces : null; + type is IComplexOutputType complexType ? complexType.Implements : null; public IEnumerable? GetPossibleTypes(ISchema schema, [Parent]INamedType type) => type.IsAbstractType() ? schema.GetPossibleTypes(type) : null; diff --git a/src/HotChocolate/Core/src/Types/Types/ObjectType.cs b/src/HotChocolate/Core/src/Types/Types/ObjectType.cs index 16c001f7716..51715357d72 100644 --- a/src/HotChocolate/Core/src/Types/Types/ObjectType.cs +++ b/src/HotChocolate/Core/src/Types/Types/ObjectType.cs @@ -7,6 +7,8 @@ using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; +using static HotChocolate.Types.FieldInitHelper; +using static HotChocolate.Types.CompleteInterfacesHelper; using static HotChocolate.Utilities.ErrorHelper; #nullable enable @@ -17,7 +19,7 @@ public class ObjectType : NamedTypeBase , IObjectType { - private readonly List _interfaces = new List(); + private readonly List _implements = new(); private Action? _configure; private IsOfType? _isOfType; @@ -39,9 +41,9 @@ public ObjectType(Action configure) ISyntaxNode? IHasSyntaxNode.SyntaxNode => SyntaxNode; - public IReadOnlyList Interfaces => _interfaces; + public IReadOnlyList Implements => _implements; - IReadOnlyList IComplexOutputType.Interfaces => Interfaces; + IReadOnlyList IComplexOutputType.Implements => Implements; public FieldCollection Fields { get; private set; } @@ -53,13 +55,13 @@ public ObjectType(Action configure) _isOfType!.Invoke(context, resolverResult); public bool IsImplementing(NameString interfaceTypeName) => - _interfaces.Any(t => t.Name.Equals(interfaceTypeName)); + _implements.Any(t => t.Name.Equals(interfaceTypeName)); public bool IsImplementing(InterfaceType interfaceType) => - _interfaces.IndexOf(interfaceType) != -1; + _implements.IndexOf(interfaceType) != -1; public bool IsImplementing(IInterfaceType interfaceType) => - interfaceType is InterfaceType i && _interfaces.IndexOf(i) != -1; + interfaceType is InterfaceType i && _implements.IndexOf(i) != -1; protected override ObjectTypeDefinition CreateDefinition( ITypeDiscoveryContext context) @@ -94,19 +96,21 @@ public ObjectType(Action configure) _isOfType = definition.IsOfType; SyntaxNode = definition.SyntaxNode; + // create fields with the specified sorting settings ... var sortByName = context.DescriptorContext.Options.SortFieldsByName; var fields = definition.Fields.Select(t => new ObjectField(t, sortByName)).ToList(); Fields = new FieldCollection(fields, sortByName); - CompleteInterfacesHelper.Complete( - context, definition, RuntimeType, _interfaces, this, SyntaxNode); + // resolve interface references + CompleteInterfaces(context, definition, RuntimeType, _implements, this, SyntaxNode); - CompleteIsOfType(context); - FieldInitHelper.CompleteFields(context, definition, Fields); + // complete the type resolver and fields + CompleteTypeResolver(context); + CompleteFields(context, definition, Fields); } } - private void CompleteIsOfType(ITypeCompletionContext context) + private void CompleteTypeResolver(ITypeCompletionContext context) { if (_isOfType is null) { @@ -149,6 +153,7 @@ private void CompleteIsOfType(ITypeCompletionContext context) { return true; } + return RuntimeType.IsInstanceOfType(result); } diff --git a/src/HotChocolate/Core/src/Types/Types/Relay/NodeType.cs b/src/HotChocolate/Core/src/Types/Types/Relay/NodeType.cs index bad63b00890..d8cf430567f 100644 --- a/src/HotChocolate/Core/src/Types/Types/Relay/NodeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Relay/NodeType.cs @@ -1,17 +1,16 @@ -namespace HotChocolate.Types.Relay +using HotChocolate.Properties; + +namespace HotChocolate.Types.Relay { - public class NodeType - : InterfaceType + public class NodeType : InterfaceType { protected override void Configure( IInterfaceTypeDescriptor descriptor) { descriptor .Name(Names.Node) - .Description( - "The node interface is implemented by entities that have " + - "a global unique identifier."); - + .Description(TypeResources.NodeType_TypeDescription); + descriptor .Field(Names.Id) .Type>(); diff --git a/src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs b/src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs index 38161f1db73..5e1e38abbb3 100644 --- a/src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs @@ -74,6 +74,29 @@ public void Remove_ClrType_Bindings_That_Are_Not_Used() Assert.False(exists); } + [Fact] + public void Infer_Interface_Usage() + { + SchemaBuilder.New() + .AddQueryType() + .AddType() + .Create() + .Print() + .MatchSnapshot(); + } + + [Fact] + public void Infer_Interface_Usage_With_Interfaces_Implementing_Interfaces() + { + SchemaBuilder.New() + .AddQueryType() + .AddType() + .AddType() + .Create() + .Print() + .MatchSnapshot(); + } + public class Query { public string SayHello(string name) => @@ -94,15 +117,15 @@ public class QueryWithGenerics public Task GetPet(int id, CancellationToken cancellationToken) => throw new NotImplementedException(); - // The arguments are needed to make the compiler apply attributes as expected - // for this use case. It's not entirely clear what combination of arguments + // The arguments are needed to make the compiler apply attributes as expected + // for this use case. It's not entirely clear what combination of arguments // for this and other fields on the class makes it behave this way // We want the compiler to apply these attributes to the GetPets method // [NullableContext(2)] // [return: Nullable(1)] public Task> GetPets( int? arg1, bool? arg2, bool? arg3, string? arg4, - GenericWrapper? arg5, Greetings? arg6, + GenericWrapper? arg5, Greetings? arg6, CancellationToken cancellationToken) => throw new NotImplementedException(); } @@ -163,5 +186,27 @@ public class ModelWithDateTime public DateTime Bar { get; set; } } + + public class QueryInterfaces + { + public IFoo GetFoo() => new Foo(); + } + + public class Foo : IFoo + { + public string GetBar() => "Bar"; + + public string GetFoo() => "Foo"; + } + + public interface IFoo : IBar + { + string GetFoo(); + } + + public interface IBar + { + string GetBar(); + } } } diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/InterfaceTypeValidation.cs b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/InterfaceTypeValidation.cs index 7ceb4eeb495..f876ceacded 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/InterfaceTypeValidation.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/InterfaceTypeValidation.cs @@ -41,7 +41,7 @@ interface FooInterface { } [Fact] - public void Interface_Implements_Not_The_Interfaces_Of_Its_Interfaces() + public void Field_Is_Not_Implemented() { ExpectError(@" type Query { @@ -57,7 +57,7 @@ interface B implements C { def : String } - interface A implements B { + interface A implements B & C { abc(a: String): String cde: String def : String @@ -65,7 +65,6 @@ interface A implements B { type Foo implements A & B & C { abc(a: String): String - def: String cde: String } "); diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/ObjectTypeValidation.cs b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/ObjectTypeValidation.cs index 40f8e29b577..de939cfb80d 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/ObjectTypeValidation.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/ObjectTypeValidation.cs @@ -296,9 +296,8 @@ interface A implements B { abc(a: String): String } - type Foo implements A { + type Foo implements A & B { abc(a: String): String - cde: String } "); } diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/InterfaceTypeValidation.Field_Is_Not_Implemented.snap b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/InterfaceTypeValidation.Field_Is_Not_Implemented.snap new file mode 100644 index 00000000000..54cab1cac55 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/InterfaceTypeValidation.Field_Is_Not_Implemented.snap @@ -0,0 +1,27 @@ +{ + "message": "The field `def` must be implement by object type `Foo`.", + "type": "Foo", + "extensions": { + "implementedField": "def", + "specifiedBy": "http://spec.graphql.org/June2018/#sec-Objects.Type-Validation" + } +} + +{ + "message": "The field `def` must be implement by object type `Foo`.", + "type": "Foo", + "extensions": { + "implementedField": "def", + "specifiedBy": "http://spec.graphql.org/June2018/#sec-Objects.Type-Validation" + } +} + +{ + "message": "The field `def` must be implement by object type `Foo`.", + "type": "Foo", + "extensions": { + "implementedField": "def", + "specifiedBy": "http://spec.graphql.org/June2018/#sec-Objects.Type-Validation" + } +} + diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/ObjectTypeValidation.Object_Implements_Not_The_Interfaces_Of_Its_Interfaces.snap b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/ObjectTypeValidation.Object_Implements_Not_The_Interfaces_Of_Its_Interfaces.snap index c8630e98a18..186dfca2ba0 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/ObjectTypeValidation.Object_Implements_Not_The_Interfaces_Of_Its_Interfaces.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/ObjectTypeValidation.Object_Implements_Not_The_Interfaces_Of_Its_Interfaces.snap @@ -1,18 +1,18 @@ { - "message": "The object type must also declare all interfaces declared by implemented interfaces.", + "message": "The field `cde` must be implement by object type `Foo`.", "type": "Foo", "extensions": { - "implementedType": "A", + "implementedField": "cde", "specifiedBy": "http://spec.graphql.org/June2018/#sec-Objects.Type-Validation" } } { - "message": "The field `cde` must be implement by interface type `A`.", - "type": "A", + "message": "The field `cde` must be implement by object type `Foo`.", + "type": "Foo", "extensions": { "implementedField": "cde", - "specifiedBy": "http://spec.graphql.org/June2018/#sec-Interfaces.Type-Validation" + "specifiedBy": "http://spec.graphql.org/June2018/#sec-Objects.Type-Validation" } } diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/ObjectTypeValidation.Object_Implements_Not_The_Interfaces_Of_Its_Interfaces_2.snap b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/ObjectTypeValidation.Object_Implements_Not_The_Interfaces_Of_Its_Interfaces_2.snap index c8630e98a18..186dfca2ba0 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/ObjectTypeValidation.Object_Implements_Not_The_Interfaces_Of_Its_Interfaces_2.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/Validation/__snapshots__/ObjectTypeValidation.Object_Implements_Not_The_Interfaces_Of_Its_Interfaces_2.snap @@ -1,18 +1,18 @@ { - "message": "The object type must also declare all interfaces declared by implemented interfaces.", + "message": "The field `cde` must be implement by object type `Foo`.", "type": "Foo", "extensions": { - "implementedType": "A", + "implementedField": "cde", "specifiedBy": "http://spec.graphql.org/June2018/#sec-Objects.Type-Validation" } } { - "message": "The field `cde` must be implement by interface type `A`.", - "type": "A", + "message": "The field `cde` must be implement by object type `Foo`.", + "type": "Foo", "extensions": { "implementedField": "cde", - "specifiedBy": "http://spec.graphql.org/June2018/#sec-Interfaces.Type-Validation" + "specifiedBy": "http://spec.graphql.org/June2018/#sec-Objects.Type-Validation" } } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/InterfaceTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/InterfaceTypeTests.cs index 4f13fbc3c2d..49976cc397c 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/InterfaceTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/InterfaceTypeTests.cs @@ -131,7 +131,7 @@ public void InferSchemaInterfaceTypeFromClrInterface() // assert ObjectType type = schema.GetType("FooImpl"); - Assert.Collection(type.Interfaces, + Assert.Collection(type.Implements, t => Assert.Equal("IFoo", t.Name)); } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/ObjectTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/ObjectTypeTests.cs index 90f87d6615a..480c3b4cc7c 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/ObjectTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/ObjectTypeTests.cs @@ -634,7 +634,7 @@ interface B { // assert ObjectType type = schema.GetType("C"); - Assert.Equal(2, type.Interfaces.Count); + Assert.Equal(2, type.Implements.Count); } [Fact] @@ -667,7 +667,7 @@ interface B { // assert ObjectType type = schema.GetType("C"); - Assert.Equal(2, type.Interfaces.Count); + Assert.Equal(2, type.Implements.Count); } [Fact] @@ -1190,7 +1190,7 @@ public void InferInterfaceImplementation() // assert Assert.IsType>( - fooType.Interfaces[0]); + fooType.Implements[0]); } [Fact] @@ -1672,9 +1672,16 @@ public void Infer_Argument_Default_Values() public void Inferred_Interfaces_From_Type_Extensions_Are_Merged() { SchemaBuilder.New() - .AddDocumentFromString("type Query { some: Some } type Some { foo: String }") + .AddDocumentFromString( + @"type Query { + some: Some + } + + type Some { + foo: String + }") .AddType() - .Use(next => context => default(ValueTask)) + .Use(_ => _ => default) .EnableRelaySupport() .Create() .ToString() @@ -1903,8 +1910,7 @@ public class QueryWithArgumentDefaults } [ExtendObjectType(Name = "Some")] - public class SomeTypeExtensionWithInterface - : INode + public class SomeTypeExtensionWithInterface : INode { [GraphQLType(typeof(NonNullType))] public string Id { get; } @@ -1913,13 +1919,7 @@ public class SomeTypeExtensionWithInterface public class QueryWithNestedList { public List> FooMatrix => - new List> - { - new List - { - new FooIgnore() - } - }; + new() { new() { new() } }; } public class ResolveWithQuery diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/CodeFirstTests.Infer_Interface_Usage.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/CodeFirstTests.Infer_Interface_Usage.snap new file mode 100644 index 00000000000..49b8ba5a61c --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/CodeFirstTests.Infer_Interface_Usage.snap @@ -0,0 +1,31 @@ +schema { + query: QueryInterfaces +} + +interface IFoo { + foo: String! +} + +type Foo implements IFoo { + bar: String! + foo: String! +} + +type QueryInterfaces { + foo: IFoo! +} + +"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." +directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +"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 + +"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 + +"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 + +"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." +directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! "Streamed when true." if: Boolean!) on FIELD diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/CodeFirstTests.Infer_Interface_Usage_With_Interfaces_Implementing_Interfaces.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/CodeFirstTests.Infer_Interface_Usage_With_Interfaces_Implementing_Interfaces.snap new file mode 100644 index 00000000000..a8a1bf7b00d --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/CodeFirstTests.Infer_Interface_Usage_With_Interfaces_Implementing_Interfaces.snap @@ -0,0 +1,36 @@ +schema { + query: QueryInterfaces +} + +interface IBar { + bar: String! +} + +interface IFoo implements IBar { + foo: String! + bar: String! +} + +type Foo implements IFoo & IBar { + bar: String! + foo: String! +} + +type QueryInterfaces { + foo: IFoo! +} + +"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." +directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +"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 + +"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 + +"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 + +"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." +directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! "Streamed when true." if: Boolean!) on FIELD diff --git a/src/HotChocolate/Language/src/Language/Legacy/Visitors/SchemaSyntaxSerializer.cs b/src/HotChocolate/Language/src/Language/Legacy/Visitors/SchemaSyntaxSerializer.cs index 845a2a595ef..c9c73e9c352 100644 --- a/src/HotChocolate/Language/src/Language/Legacy/Visitors/SchemaSyntaxSerializer.cs +++ b/src/HotChocolate/Language/src/Language/Legacy/Visitors/SchemaSyntaxSerializer.cs @@ -122,6 +122,16 @@ public SchemaSyntaxSerializer(bool useIndentation) writer.WriteSpace(); writer.WriteName(node.Name); + if (node.Interfaces.Count > 0) + { + writer.WriteSpace(); + writer.Write(Keywords.Implements); + writer.WriteSpace(); + writer.WriteMany(node.Interfaces, + (n, w) => writer.WriteNamedType(n), + " & "); + } + WriteDirectives(node.Directives, writer); WriteLeftBrace(writer); diff --git a/src/HotChocolate/Language/test/Language.Tests/Parser/Utf8QueryParserTests.cs b/src/HotChocolate/Language/test/Language.Tests/Parser/Utf8QueryParserTests.cs index 285e4f64758..58959affb61 100644 --- a/src/HotChocolate/Language/test/Language.Tests/Parser/Utf8QueryParserTests.cs +++ b/src/HotChocolate/Language/test/Language.Tests/Parser/Utf8QueryParserTests.cs @@ -476,5 +476,23 @@ public void RussianLiterals() // assert document.MatchSnapshot(); } + + [Fact(Skip = "Implement Parse Variable Directives")] + public void ParseVariablesWithDirective() + { + // arrange + byte[] sourceText = Encoding.UTF8.GetBytes( + @"query ($a: String! @foo) + a(a: $a) + "); + + // act + var parser = new Utf8GraphQLParser( + sourceText, ParserOptions.Default); + DocumentNode document = parser.Parse(); + + // assert + document.MatchSnapshot(); + } } } diff --git a/src/HotChocolate/Language/test/Language.Tests/Parser/__snapshots__/Utf8SchemaParserTests.ParserSimpleInterfaceType.snap b/src/HotChocolate/Language/test/Language.Tests/Parser/__snapshots__/Utf8SchemaParserTests.ParserSimpleInterfaceType.snap index 6fd25f66666..501fc93ed9a 100644 --- a/src/HotChocolate/Language/test/Language.Tests/Parser/__snapshots__/Utf8SchemaParserTests.ParserSimpleInterfaceType.snap +++ b/src/HotChocolate/Language/test/Language.Tests/Parser/__snapshots__/Utf8SchemaParserTests.ParserSimpleInterfaceType.snap @@ -1,4 +1,4 @@ -interface a @foo(a: "123") { +interface a implements e @foo(a: "123") { b: String @foo(a: "123") c(d: F = ENUMVALUE @foo(a: "123")): Int } diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/ContractType.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/ContractType.cs index b30fa9349c7..5e03db8598a 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/ContractType.cs +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/ContractType.cs @@ -3,8 +3,7 @@ namespace HotChocolate.Stitching.Schemas.Contracts { - public class ContractType - : InterfaceType + public class ContractType : InterfaceType { protected override void Configure(IInterfaceTypeDescriptor descriptor) { diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/CustomDirectiveType.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/CustomDirectiveType.cs index 0ea9a80758b..872128e55db 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/CustomDirectiveType.cs +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/CustomDirectiveType.cs @@ -3,8 +3,7 @@ namespace HotChocolate.Stitching.Schemas.Contracts { - public class CustomDirectiveType - : DirectiveType + public class CustomDirectiveType : DirectiveType { protected override void Configure(IDirectiveTypeDescriptor descriptor) { diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/LifeInsuranceContract.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/LifeInsuranceContract.cs index 67b947faa04..8a6624b0c4c 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/LifeInsuranceContract.cs +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/LifeInsuranceContract.cs @@ -1,7 +1,6 @@ namespace HotChocolate.Stitching.Schemas.Contracts { - public class LifeInsuranceContract - : IContract + public class LifeInsuranceContract : IContract { public string Id { get; set; } diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/LifeInsuranceContractType.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/LifeInsuranceContractType.cs index 14ac7d39a3b..4953357cee1 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/LifeInsuranceContractType.cs +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/LifeInsuranceContractType.cs @@ -5,8 +5,7 @@ namespace HotChocolate.Stitching.Schemas.Contracts { - public class LifeInsuranceContractType - : ObjectType + public class LifeInsuranceContractType : ObjectType { protected override void Configure( IObjectTypeDescriptor descriptor) diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/QueryType.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/QueryType.cs index f5072b468e1..57075db4347 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/QueryType.cs +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/QueryType.cs @@ -3,8 +3,7 @@ namespace HotChocolate.Stitching.Schemas.Contracts { - public class QueryType - : ObjectType + public class QueryType : ObjectType { protected override void Configure( IObjectTypeDescriptor descriptor) diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/SomeOtherContract.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/SomeOtherContract.cs index 9356e81eb53..15dde6bad75 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/SomeOtherContract.cs +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/SomeOtherContract.cs @@ -2,8 +2,7 @@ namespace HotChocolate.Stitching.Schemas.Contracts { - public class SomeOtherContract - : IContract + public class SomeOtherContract : IContract { public string Id { get; set; } diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/SomeOtherContractType.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/SomeOtherContractType.cs index efa130bd451..e2c8008926a 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/SomeOtherContractType.cs +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Contracts/SomeOtherContractType.cs @@ -4,8 +4,7 @@ namespace HotChocolate.Stitching.Schemas.Contracts { - public class SomeOtherContractType - : ObjectType + public class SomeOtherContractType : ObjectType { protected override void Configure( IObjectTypeDescriptor descriptor) diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/__snapshots__/SchemaTests.ContractSchemaSnapshot.snap b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/__snapshots__/SchemaTests.ContractSchemaSnapshot.snap index 67d84813d58..eff466d9dc9 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/__snapshots__/SchemaTests.ContractSchemaSnapshot.snap +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/__snapshots__/SchemaTests.ContractSchemaSnapshot.snap @@ -2,7 +2,7 @@ query: Query } -interface Contract { +interface Contract implements Node { id: ID! customerId: ID! }