From a41812af2d571b2760f7d51d8a51eb2cf40e824d Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 5 Dec 2024 17:18:04 +0200 Subject: [PATCH 01/13] Added initial structure for Fusion vNext Composition --- dictionary.txt | 1 + src/Directory.Packages.props | 2 +- .../Fusion.Composition/CompositionContext.cs | 19 +++ .../Extensions/TypeDefinitionExtensions.cs | 16 ++ .../HotChocolate.Fusion.Composition.csproj | 25 ++- .../Logging/CompositionLog.cs | 24 +++ .../Logging/Contracts/ICompositionLog.cs | 25 +++ .../Logging/Contracts/ILoggingSession.cs | 12 ++ .../Fusion.Composition/Logging/LogEntry.cs | 68 +++++++++ .../Logging/LogEntryCodes.cs | 7 + .../Logging/LogEntryHelper.cs | 99 ++++++++++++ .../Fusion.Composition/Logging/LogSeverity.cs | 22 +++ .../Logging/LoggingSession.cs | 35 +++++ .../PostMergeValidation/PostMergeValidator.cs | 13 ++ .../Contracts/IPreMergeValidationRule.cs | 8 + .../PreMergeValidationContext.cs | 69 +++++++++ .../PreMergeValidation/PreMergeValidator.cs | 35 +++++ .../DisallowedInaccessibleElementsRule.cs | 99 ++++++++++++ .../Rules/OutputFieldTypesMergeableRule.cs | 38 +++++ .../CompositionResources.Designer.cs | 134 ++++++++++++++++ .../Properties/CompositionResources.resx | 45 ++++++ .../src/Fusion.Composition/Results/Error.cs | 6 + .../Fusion.Composition/Results/ErrorHelper.cs | 10 ++ .../src/Fusion.Composition/Results/Result.cs | 52 +++++++ .../Fusion.Composition/Results/Result~1.cs | 73 +++++++++ .../SatisfiabilityValidator.cs | 13 ++ .../src/Fusion.Composition/SchemaComposer.cs | 41 +++++ .../Fusion.Composition/SourceSchemaMerger.cs | 44 ++++++ .../SourceSchemaValidator.cs | 12 ++ .../Fusion.Composition/ValidationHelper.cs | 100 ++++++++++++ .../WellKnownDirectiveNames.cs | 6 + ...tChocolate.Fusion.Composition.Tests.csproj | 6 +- ...DisallowedInaccessibleElementsRuleTests.cs | 124 +++++++++++++++ .../OutputFieldTypesMergeableRuleTests.cs | 144 ++++++++++++++++++ .../ValidationHelperTests.cs | 78 ++++++++++ .../src/Skimmed/Extensions/TypeExtensions.cs | 9 ++ 36 files changed, 1511 insertions(+), 3 deletions(-) create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionContext.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/TypeDefinitionExtensions.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ILoggingSession.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntry.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogSeverity.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LoggingSession.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidation/PostMergeValidator.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Contracts/IPreMergeValidationRule.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Error.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/ErrorHelper.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result~1.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SatisfiabilityValidator.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaValidator.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/WellKnownDirectiveNames.cs create mode 100644 src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs create mode 100644 src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs create mode 100644 src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/ValidationHelperTests.cs diff --git a/dictionary.txt b/dictionary.txt index fdebf7dec77..0cdce2781d9 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -136,6 +136,7 @@ Rgba Rhai Roslynator runbooks +Satisfiability Senn shoooe Skywalker diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 90e301d4f3b..48d02d0f9a0 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -60,7 +60,7 @@ - + diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionContext.cs new file mode 100644 index 00000000000..1962a855449 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionContext.cs @@ -0,0 +1,19 @@ +using HotChocolate.Fusion.Logging.Contracts; +using HotChocolate.Skimmed; + +namespace HotChocolate.Fusion; + +internal sealed class CompositionContext( + SchemaDefinition[] schemaDefinitions, + ICompositionLog compositionLog) +{ + /// + /// Gets the schema definitions. + /// + public SchemaDefinition[] SchemaDefinitions { get; } = schemaDefinitions; + + /// + /// Gets the composition log. + /// + public ICompositionLog Log { get; } = compositionLog; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/TypeDefinitionExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/TypeDefinitionExtensions.cs new file mode 100644 index 00000000000..741b60e4fd1 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/TypeDefinitionExtensions.cs @@ -0,0 +1,16 @@ +using HotChocolate.Skimmed; + +namespace HotChocolate.Fusion.Extensions; + +internal static class TypeDefinitionExtensions +{ + public static ITypeDefinition InnerNullableType(this ITypeDefinition type) + { + return type switch + { + ListTypeDefinition listType => listType.ElementType.NullableType(), + NonNullTypeDefinition nonNullType => nonNullType.NullableType, + _ => type + }; + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj index f11759cfaec..f6bb555f9d9 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj @@ -1,8 +1,31 @@ - + HotChocolate.Fusion.Composition HotChocolate.Fusion + + + + + + + + + + + + ResXFileCodeGenerator + CompositionResources.Designer.cs + + + + + + True + True + CompositionResources.resx + + diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs new file mode 100644 index 00000000000..1b257536cb6 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs @@ -0,0 +1,24 @@ +using HotChocolate.Fusion.Logging.Contracts; + +namespace HotChocolate.Fusion.Logging; + +public sealed class CompositionLog : ICompositionLog +{ + public IList Entries { get; } = []; + + public int EntryCount => Entries.Count; + + public bool IsEmpty => Entries.Count == 0; + + public void Write(LogEntry entry) + { + ArgumentNullException.ThrowIfNull(entry); + + Entries.Add(entry); + } + + public ILoggingSession CreateSession() + { + return new LoggingSession(this); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs new file mode 100644 index 00000000000..ac5281dc307 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs @@ -0,0 +1,25 @@ +namespace HotChocolate.Fusion.Logging.Contracts; + +/// +/// Defines an interface for logging composition information, warnings, and errors. +/// +public interface ICompositionLog +{ + IList Entries { get; } + + int EntryCount { get; } + + bool IsEmpty { get; } + + /// + /// Writes the specified to the log. + /// + /// The to write. + void Write(LogEntry entry); + + /// + /// Creates a new logging session that keeps track of the number of info, warning, and error + /// entries logged. + /// + ILoggingSession CreateSession(); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ILoggingSession.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ILoggingSession.cs new file mode 100644 index 00000000000..56f1b5b7547 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ILoggingSession.cs @@ -0,0 +1,12 @@ +namespace HotChocolate.Fusion.Logging.Contracts; + +public interface ILoggingSession +{ + int InfoCount { get; } + + int WarningCount { get; } + + int ErrorCount { get; } + + void Write(LogEntry entry); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntry.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntry.cs new file mode 100644 index 00000000000..9500970e779 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntry.cs @@ -0,0 +1,68 @@ +using HotChocolate.Skimmed; + +namespace HotChocolate.Fusion.Logging; + +/// +/// Represents an entry in a composition log that describes an issue encountered during the +/// composition process. +/// +public sealed record LogEntry +{ + /// + /// Initializes a new instance of the record with the specified values. + /// + public LogEntry( + string message, + string code, + LogSeverity severity = LogSeverity.Error, + SchemaCoordinate? coordinate = null, + ITypeSystemMemberDefinition? member = null, + SchemaDefinition? schema = null, + object? extension = null) + { + Message = message; + Code = code; + Severity = severity; + Coordinate = coordinate; + Member = member; + Schema = schema; + Extension = extension; + } + + /// + /// Gets the message associated with this log entry. + /// + public string Message { get; } + + /// + /// Gets the code associated with this log entry. + /// + public string Code { get; } + + /// + /// Gets the severity of this log entry. + /// + public LogSeverity Severity { get; } + + /// + /// Gets the schema coordinate associated with this log entry. + /// + public SchemaCoordinate? Coordinate { get; } + + /// + /// Gets the type system member associated with this log entry. + /// + public ITypeSystemMemberDefinition? Member { get; } + + /// + /// Gets the schema associated with this log entry. + /// + public SchemaDefinition? Schema { get; } + + /// + /// Gets the extension object associated with this log entry. + /// + public object? Extension { get; } + + public override string ToString() => Message; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs new file mode 100644 index 00000000000..298f766cf33 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.Fusion.Logging; + +public static class LogEntryCodes +{ + public const string DisallowedInaccessible = "DISALLOWED_INACCESSIBLE"; + public const string OutputFieldTypesNotMergeable = "OUTPUT_FIELD_TYPES_NOT_MERGEABLE"; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs new file mode 100644 index 00000000000..605ad8dffac --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs @@ -0,0 +1,99 @@ +using HotChocolate.Skimmed; +using static HotChocolate.Fusion.Properties.CompositionResources; + +namespace HotChocolate.Fusion.Logging; + +internal static class LogEntryHelper +{ + public static LogEntry DisallowedInaccessibleScalar( + ScalarTypeDefinition scalar, + SchemaDefinition schema) + => new( + string.Format(LogEntryHelper_DisallowedInaccessibleScalar, scalar.Name), + LogEntryCodes.DisallowedInaccessible, + LogSeverity.Error, + new SchemaCoordinate(scalar.Name), + scalar, + schema); + + public static LogEntry DisallowedInaccessibleIntrospectionType( + INamedTypeDefinition type, + SchemaDefinition schema) + => new( + string.Format(LogEntryHelper_DisallowedInaccessibleIntrospectionType, type.Name), + LogEntryCodes.DisallowedInaccessible, + LogSeverity.Error, + new SchemaCoordinate(type.Name), + type, + schema); + + public static LogEntry DisallowedInaccessibleIntrospectionField( + OutputFieldDefinition field, + string typeName, + SchemaDefinition schema) + => new( + string.Format( + LogEntryHelper_DisallowedInaccessibleIntrospectionField, + field.Name, + typeName), + LogEntryCodes.DisallowedInaccessible, + LogSeverity.Error, + new SchemaCoordinate(typeName, field.Name), + field, + schema); + + public static LogEntry DisallowedInaccessibleIntrospectionArgument( + InputFieldDefinition argument, + string fieldName, + string typeName, + SchemaDefinition schema) + { + var coordinate = new SchemaCoordinate(typeName, fieldName, argument.Name); + + return new LogEntry( + string.Format( + LogEntryHelper_DisallowedInaccessibleIntrospectionArgument, + argument.Name, + coordinate), + LogEntryCodes.DisallowedInaccessible, + LogSeverity.Error, + coordinate, + argument, + schema); + } + + public static LogEntry DisallowedInaccessibleDirective( + Directive directive, + SchemaDefinition schema) + => new( + string.Format(LogEntryHelper_DisallowedInaccessibleDirective, directive.Name), + LogEntryCodes.DisallowedInaccessible, + LogSeverity.Error, + new SchemaCoordinate(directive.Name, ofDirective: true), + directive, + schema); + + public static LogEntry DisallowedInaccessibleDirectiveArgument( + ArgumentAssignment argument, + string directiveName, + SchemaDefinition schema) + => new( + string.Format( + LogEntryHelper_DisallowedInaccessibleDirectiveArgument, + argument.Name, + directiveName), + LogEntryCodes.DisallowedInaccessible, + LogSeverity.Error, + new SchemaCoordinate(directiveName, argumentName: argument.Name, ofDirective: true), + schema: schema); + + public static LogEntry OutputFieldTypesNotMergeable(string typeName, string fieldName) + => new( + string.Format( + LogEntryHelper_OutputFieldTypesNotMergeable, + typeName, + fieldName), + LogEntryCodes.OutputFieldTypesNotMergeable, + LogSeverity.Error, + new SchemaCoordinate(typeName, fieldName)); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogSeverity.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogSeverity.cs new file mode 100644 index 00000000000..70cc9994b49 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogSeverity.cs @@ -0,0 +1,22 @@ +namespace HotChocolate.Fusion.Logging; + +/// +/// Defines the severity of the log entry. +/// +public enum LogSeverity +{ + /// + /// The entry contains an informational message. + /// + Info, + + /// + /// The entry contains a warning message. + /// + Warning, + + /// + /// The entry contains an error message. + /// + Error +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LoggingSession.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LoggingSession.cs new file mode 100644 index 00000000000..1e4d1e1d014 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LoggingSession.cs @@ -0,0 +1,35 @@ +using HotChocolate.Fusion.Logging.Contracts; + +namespace HotChocolate.Fusion.Logging; + +public sealed class LoggingSession(ICompositionLog compositionLog) : ILoggingSession +{ + public int InfoCount { get; private set; } + + public int WarningCount { get; private set; } + + public int ErrorCount { get; private set; } + + public void Write(LogEntry entry) + { + switch (entry.Severity) + { + case LogSeverity.Info: + InfoCount++; + break; + + case LogSeverity.Warning: + WarningCount++; + break; + + case LogSeverity.Error: + ErrorCount++; + break; + + default: + throw new InvalidOperationException(); + } + + compositionLog.Write(entry); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidation/PostMergeValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidation/PostMergeValidator.cs new file mode 100644 index 00000000000..305e843cdc3 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidation/PostMergeValidator.cs @@ -0,0 +1,13 @@ +using HotChocolate.Fusion.Results; +using HotChocolate.Skimmed; + +namespace HotChocolate.Fusion.PostMergeValidation; + +internal sealed class PostMergeValidator +{ + public Result Validate(SchemaDefinition _) + { + // FIXME: Implement. + return Result.Success(); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Contracts/IPreMergeValidationRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Contracts/IPreMergeValidationRule.cs new file mode 100644 index 00000000000..0c2f43e675b --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Contracts/IPreMergeValidationRule.cs @@ -0,0 +1,8 @@ +using HotChocolate.Fusion.Results; + +namespace HotChocolate.Fusion.PreMergeValidation.Contracts; + +internal interface IPreMergeValidationRule +{ + Result Run(PreMergeValidationContext context); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs new file mode 100644 index 00000000000..bc34101eeb3 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs @@ -0,0 +1,69 @@ +using HotChocolate.Fusion.Logging.Contracts; +using HotChocolate.Skimmed; + +namespace HotChocolate.Fusion.PreMergeValidation; + +internal sealed class PreMergeValidationContext(CompositionContext context) +{ + public SchemaDefinition[] SchemaDefinitions => context.SchemaDefinitions; + public ICompositionLog Log => context.Log; + public IEnumerable OutputTypeInfo = []; + + public void Initialize() + { + InitializeOutputTypeInfo(); + } + + /// + /// Initializes a structure that makes it easier to access combined output types, fields, and + /// arguments for validation purposes. + /// + private void InitializeOutputTypeInfo() + { + OutputTypeInfo = SchemaDefinitions + .SelectMany(s => s.Types) + .Where(t => t.IsOutputType()) + .OfType() + .GroupBy(t => t.Name, (typeName, types) => + { + types = types.ToArray(); + + var fieldInfo = types + .SelectMany(t => t.Fields) + .GroupBy( + f => f.Name, + (fieldName, fields) => + { + fields = fields.ToArray(); + + var argumentInfo = fields + .SelectMany(f => f.Arguments) + .GroupBy( + a => a.Name, + (argumentName, arguments) => + new OutputArgumentInfo(argumentName, arguments.ToArray())); + + return new OutputFieldInfo( + fieldName, + fields.ToArray(), + argumentInfo.ToArray()); + }); + + return new OutputTypeInfo(typeName, types.ToArray(), fieldInfo.ToArray()); + }); + } +} + +internal record OutputTypeInfo( + string TypeName, + ComplexTypeDefinition[] Types, + OutputFieldInfo[] FieldInfo); + +internal record OutputFieldInfo( + string FieldName, + OutputFieldDefinition[] Fields, + OutputArgumentInfo[] Arguments); + +internal record OutputArgumentInfo( + string ArgumentName, + InputFieldDefinition[] Arguments); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs new file mode 100644 index 00000000000..9931aa12dd4 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs @@ -0,0 +1,35 @@ +using System.Collections.Immutable; +using HotChocolate.Fusion.PreMergeValidation.Contracts; +using HotChocolate.Fusion.PreMergeValidation.Rules; +using HotChocolate.Fusion.Results; + +namespace HotChocolate.Fusion.PreMergeValidation; + +internal sealed class PreMergeValidator +{ + private readonly ImmutableArray _validationRules = + [ + new DisallowedInaccessibleElementsRule(), + new OutputFieldTypesMergeableRule() + ]; + + public Result Validate(CompositionContext compositionContext) + { + var preMergeValidationContext = new PreMergeValidationContext(compositionContext); + preMergeValidationContext.Initialize(); + + var errors = new List(); + + foreach (var validationRule in _validationRules) + { + var result = validationRule.Run(preMergeValidationContext); + + if (result.IsFailure) + { + errors.AddRange(result.Errors); + } + } + + return errors; + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs new file mode 100644 index 00000000000..6b47364dbe3 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs @@ -0,0 +1,99 @@ +using HotChocolate.Fusion.PreMergeValidation.Contracts; +using HotChocolate.Fusion.Results; +using HotChocolate.Skimmed; +using static HotChocolate.Fusion.Logging.LogEntryHelper; + +namespace HotChocolate.Fusion.PreMergeValidation.Rules; + +/// +/// This rule ensures that certain essential elements of a GraphQL schema, particularly built-in +/// scalars, directives, and introspection types, cannot be marked as @inaccessible. These types are +/// fundamental to GraphQL. Making these elements inaccessible would break core GraphQL +/// functionality. +/// +/// +/// Specification +/// +internal sealed class DisallowedInaccessibleElementsRule : IPreMergeValidationRule +{ + public Result Run(PreMergeValidationContext context) + { + var loggingSession = context.Log.CreateSession(); + + foreach (var schema in context.SchemaDefinitions) + { + foreach (var type in schema.Types) + { + if (type is ScalarTypeDefinition { IsSpecScalar: true } scalar + && !ValidationHelper.IsAccessible(type)) + { + loggingSession.Write(DisallowedInaccessibleScalar(scalar, schema)); + } + + // FIXME: Better way to check for introspection type. + if (type.Name.StartsWith("__")) + { + if (!ValidationHelper.IsAccessible(type)) + { + loggingSession.Write(DisallowedInaccessibleIntrospectionType(type, schema)); + } + + if (type is ComplexTypeDefinition complexType) + { + foreach (var field in complexType.Fields) + { + if (!ValidationHelper.IsAccessible(field)) + { + loggingSession.Write( + DisallowedInaccessibleIntrospectionField( + field, + type.Name, + schema)); + } + + foreach (var argument in field.Arguments) + { + if (!ValidationHelper.IsAccessible(argument)) + { + loggingSession.Write( + DisallowedInaccessibleIntrospectionArgument( + argument, + field.Name, + type.Name, + schema)); + } + } + } + } + } + } + + foreach (var directive in schema.Directives) + { + if (BuiltIns.IsBuiltInDirective(directive.Name)) + { + if (!ValidationHelper.IsAccessible(directive)) + { + loggingSession.Write(DisallowedInaccessibleDirective(directive, schema)); + } + + foreach (var argument in directive.Arguments) + { + if (!ValidationHelper.IsAccessible(argument)) + { + loggingSession.Write( + DisallowedInaccessibleDirectiveArgument( + argument, + directive.Name, + schema)); + } + } + } + } + } + + return loggingSession.ErrorCount == 0 + ? Result.Success() + : ErrorHelper.PreMergeValidationRuleFailed(this); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs new file mode 100644 index 00000000000..a5616549280 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs @@ -0,0 +1,38 @@ +using HotChocolate.Fusion.Logging; +using HotChocolate.Fusion.PreMergeValidation.Contracts; +using HotChocolate.Fusion.Results; + +namespace HotChocolate.Fusion.PreMergeValidation.Rules; + +/// +/// Fields on objects or interfaces that have the same name are considered semantically equivalent +/// and mergeable when they have a mergeable field type. +/// +/// +/// Specification +/// +internal sealed class OutputFieldTypesMergeableRule : IPreMergeValidationRule +{ + public Result Run(PreMergeValidationContext context) + { + var loggingSession = context.Log.CreateSession(); + + foreach (var outputTypeInfo in context.OutputTypeInfo) + { + foreach (var fieldInfo in outputTypeInfo.FieldInfo) + { + if (!ValidationHelper.FieldsAreMergeable(fieldInfo.Fields)) + { + loggingSession.Write( + LogEntryHelper.OutputFieldTypesNotMergeable( + fieldInfo.FieldName, + outputTypeInfo.TypeName)); + } + } + } + + return loggingSession.ErrorCount == 0 + ? Result.Success() + : ErrorHelper.PreMergeValidationRuleFailed(this); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs new file mode 100644 index 00000000000..99a90d4a1f4 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs @@ -0,0 +1,134 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace HotChocolate.Fusion.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class CompositionResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal CompositionResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HotChocolate.Fusion.Properties.CompositionResources", typeof(CompositionResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Pre-merge validation rule '{0}' failed. View the composition log for details.. + /// + internal static string ErrorHelper_PreMergeValidationRuleFailed { + get { + return ResourceManager.GetString("ErrorHelper_PreMergeValidationRuleFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The built-in directive type '{0}' is not accessible.. + /// + internal static string LogEntryHelper_DisallowedInaccessibleDirective { + get { + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleDirective", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument '{0}' on built-in directive type '{1}' is not accessible.. + /// + internal static string LogEntryHelper_DisallowedInaccessibleDirectiveArgument { + get { + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleDirectiveArgument", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The introspection argument '{0}' with schema coordinate '{1}' is not accessible.. + /// + internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionArgument { + get { + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionArgument", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The introspection field '{0}' on type '{1}' is not accessible.. + /// + internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionField { + get { + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionField", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The introspection type '{0}' is not accessible.. + /// + internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionType { + get { + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The built-in scalar type '{0}' is not accessible.. + /// + internal static string LogEntryHelper_DisallowedInaccessibleScalar { + get { + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleScalar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Field '{0}' on type '{1}' is not mergeable.. + /// + internal static string LogEntryHelper_OutputFieldTypesNotMergeable { + get { + return ResourceManager.GetString("LogEntryHelper_OutputFieldTypesNotMergeable", resourceCulture); + } + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx new file mode 100644 index 00000000000..eac20ae36c3 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx @@ -0,0 +1,45 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Pre-merge validation rule '{0}' failed. View the composition log for details. + + + The built-in scalar type '{0}' is not accessible. + + + The introspection type '{0}' is not accessible. + + + The introspection field '{0}' on type '{1}' is not accessible. + + + The introspection argument '{0}' with schema coordinate '{1}' is not accessible. + + + The built-in directive type '{0}' is not accessible. + + + The argument '{0}' on built-in directive type '{1}' is not accessible. + + + Field '{0}' on type '{1}' is not mergeable. + + diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Error.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Error.cs new file mode 100644 index 00000000000..b743614be15 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Error.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Fusion.Results; + +public sealed class Error(string message) +{ + public string Message { get; } = message; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/ErrorHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/ErrorHelper.cs new file mode 100644 index 00000000000..5a84a057f7e --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/ErrorHelper.cs @@ -0,0 +1,10 @@ +using HotChocolate.Fusion.PreMergeValidation.Contracts; +using static HotChocolate.Fusion.Properties.CompositionResources; + +namespace HotChocolate.Fusion.Results; + +internal static class ErrorHelper +{ + public static Error PreMergeValidationRuleFailed(IPreMergeValidationRule rule) + => new(string.Format(ErrorHelper_PreMergeValidationRuleFailed, rule.GetType().Name)); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result.cs new file mode 100644 index 00000000000..9d3dbec7e8c --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result.cs @@ -0,0 +1,52 @@ +namespace HotChocolate.Fusion.Results; + +public readonly record struct Result +{ + public bool IsFailure { get; } + + public bool IsSuccess { get; } + + public List Errors { get; } = []; + + public Result() + { + IsSuccess = true; + } + + private Result(Error error) + { + Errors = [error]; + IsFailure = true; + } + + private Result(List errors) + { + if (errors.Count == 0) + { + IsSuccess = true; + } + else + { + Errors = [.. errors]; + IsFailure = true; + } + } + + public static Result Success() => new(); + + /// + /// Creates a from an error. + /// + public static implicit operator Result(Error error) + { + return new Result(error); + } + + /// + /// Creates a from a list of errors. + /// + public static implicit operator Result(List errors) + { + return new Result(errors); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result~1.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result~1.cs new file mode 100644 index 00000000000..0e527db3e7e --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result~1.cs @@ -0,0 +1,73 @@ +namespace HotChocolate.Fusion.Results; + +public readonly record struct Result +{ + public bool IsFailure { get; } + + public bool IsSuccess { get; } + + public List Errors { get; } = []; + + public TValue Value => IsSuccess + ? _value + : throw new Exception("Value may not be accessed on an unsuccessful result."); + + private readonly TValue _value = default!; + + private Result(TValue value) + { + _value = value; + IsSuccess = true; + } + + private Result(Error error) + { + Errors = [error]; + IsFailure = true; + } + + private Result(List errors) + { + if (errors.Count == 0) + { + IsSuccess = true; + } + else + { + Errors = [.. errors]; + IsFailure = true; + } + } + + /// + /// Creates a from a value. + /// + public static implicit operator Result(TValue value) + { + return new Result(value); + } + + /// + /// Creates a from an error. + /// + public static implicit operator Result(Error error) + { + return new Result(error); + } + + /// + /// Creates a from a list of errors. + /// + public static implicit operator Result(List errors) + { + return new Result(errors); + } + + /// + /// Creates a from a . + /// + public static implicit operator Result(Result result) + { + return new Result(result.Errors); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SatisfiabilityValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SatisfiabilityValidator.cs new file mode 100644 index 00000000000..b4691e76ace --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SatisfiabilityValidator.cs @@ -0,0 +1,13 @@ +using HotChocolate.Fusion.Results; +using HotChocolate.Skimmed; + +namespace HotChocolate.Fusion; + +internal sealed class SatisfiabilityValidator +{ + public Result Validate(SchemaDefinition _) + { + // FIXME: Implement. + return Result.Success(); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs new file mode 100644 index 00000000000..4bb627b94b6 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs @@ -0,0 +1,41 @@ +using HotChocolate.Fusion.Logging.Contracts; +using HotChocolate.Fusion.Results; +using HotChocolate.Skimmed; + +namespace HotChocolate.Fusion; + +public sealed class SchemaComposer +{ + public Result Compose( + SchemaDefinition[] schemaDefinitions, + ICompositionLog compositionLog) + { + var context = new CompositionContext(schemaDefinitions, compositionLog); + + // Validate Source Schemas + var validationResult = new SourceSchemaValidator().Validate(context); + + if (validationResult.IsFailure) + { + return validationResult; + } + + // Merge Source Schemas + var mergeResult = new SourceSchemaMerger().Merge(context); + + if (mergeResult.IsFailure) + { + return mergeResult; + } + + // Validate Satisfiability + var satisfiabilityResult = new SatisfiabilityValidator().Validate(mergeResult.Value); + + if (satisfiabilityResult.IsFailure) + { + return satisfiabilityResult; + } + + return mergeResult; + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs new file mode 100644 index 00000000000..541af4c7d48 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs @@ -0,0 +1,44 @@ +using HotChocolate.Fusion.PostMergeValidation; +using HotChocolate.Fusion.PreMergeValidation; +using HotChocolate.Fusion.Results; +using HotChocolate.Skimmed; + +namespace HotChocolate.Fusion; + +internal sealed class SourceSchemaMerger +{ + public Result Merge(CompositionContext context) + { + // Pre Merge Validation + var preMergeValidationResult = new PreMergeValidator().Validate(context); + + if (preMergeValidationResult.IsFailure) + { + return preMergeValidationResult; + } + + // Merge + var mergeResult = MergeSchemaDefinitions(context); + + if (mergeResult.IsFailure) + { + return mergeResult; + } + + // Post Merge Validation + var postMergeValidationResult = new PostMergeValidator().Validate(mergeResult.Value); + + if (postMergeValidationResult.IsFailure) + { + return postMergeValidationResult; + } + + return mergeResult; + } + + private Result MergeSchemaDefinitions(CompositionContext _) + { + // FIXME: Implement. + return new SchemaDefinition(); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaValidator.cs new file mode 100644 index 00000000000..77d1410f89d --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaValidator.cs @@ -0,0 +1,12 @@ +using HotChocolate.Fusion.Results; + +namespace HotChocolate.Fusion; + +internal sealed class SourceSchemaValidator +{ + public Result Validate(CompositionContext _) + { + // FIXME: Implement. + return Result.Success(); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs new file mode 100644 index 00000000000..25745fc40b3 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs @@ -0,0 +1,100 @@ +using HotChocolate.Fusion.Extensions; +using HotChocolate.Skimmed; + +namespace HotChocolate.Fusion; + +internal sealed class ValidationHelper +{ + public static bool FieldsAreMergeable(OutputFieldDefinition[] fields) + { + for (var i = 0; i < fields.Length - 1; i++) + { + var typeA = fields[i].Type; + var typeB = fields[i + 1].Type; + + if (!SameOutputTypeShape(typeA, typeB)) + { + return false; + } + } + + return true; + } + + public static bool IsAccessible(ArgumentAssignment _) + { + // FIXME: Requires support for directives on directive arguments. + return true; + } + + public static bool IsAccessible(Directive _) + { + // FIXME: Requires support for directives on directives. + return true; + } + + public static bool IsAccessible(IDirectivesProvider type) + { + return !type.Directives.ContainsName(WellKnownDirectiveNames.Inaccessible); + } + + public static bool SameOutputTypeShape(ITypeDefinition typeA, ITypeDefinition typeB) + { + var nullableTypeA = typeA.NullableType(); + var nullableTypeB = typeB.NullableType(); + + if (nullableTypeA.Kind != nullableTypeB.Kind) + { + // Different type kind. + return false; + } + + if (nullableTypeA is INamedTypeDefinition namedNullableTypeA + && nullableTypeB is INamedTypeDefinition namedNullableTypeB + && namedNullableTypeA.Name != namedNullableTypeB.Name) + { + // Different type name. + return false; + } + + while (true) + { + var innerNullableTypeA = nullableTypeA.InnerNullableType(); + var innerNullableTypeB = nullableTypeB.InnerNullableType(); + + // Note: InnerNullableType returns the type itself when there is no inner type. + // "If type A has an inner type but type B does not" (or vice versa). + if ((innerNullableTypeA != nullableTypeA && innerNullableTypeB == nullableTypeB) + || (innerNullableTypeB != nullableTypeB && innerNullableTypeA == nullableTypeA)) + { + // Different type depth. + return false; + } + + if (innerNullableTypeA == nullableTypeA) + { + // No more inner types. + break; + } + + if (innerNullableTypeA.Kind != innerNullableTypeB.Kind) + { + // Different type kind on inner type. + return false; + } + + if (innerNullableTypeA is INamedTypeDefinition namedNullableInnerTypeA + && innerNullableTypeB is INamedTypeDefinition namedNullableInnerTypeB + && namedNullableInnerTypeA.Name != namedNullableInnerTypeB.Name) + { + // Different type name on inner type. + return false; + } + + nullableTypeA = innerNullableTypeA; + nullableTypeB = innerNullableTypeB; + } + + return true; + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/WellKnownDirectiveNames.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/WellKnownDirectiveNames.cs new file mode 100644 index 00000000000..b4cf8adf77c --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/WellKnownDirectiveNames.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Fusion; + +internal static class WellKnownDirectiveNames +{ + public const string Inaccessible = "inaccessible"; +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/HotChocolate.Fusion.Composition.Tests.csproj b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/HotChocolate.Fusion.Composition.Tests.csproj index b2fc6a7f637..7c9e08f086f 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/HotChocolate.Fusion.Composition.Tests.csproj +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/HotChocolate.Fusion.Composition.Tests.csproj @@ -1,4 +1,4 @@ - + HotChocolate.Fusion.Composition.Tests @@ -11,4 +11,8 @@ + + + + diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs new file mode 100644 index 00000000000..5a890538d09 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs @@ -0,0 +1,124 @@ +using HotChocolate.Fusion; +using HotChocolate.Fusion.Logging; +using HotChocolate.Fusion.PreMergeValidation; +using HotChocolate.Fusion.PreMergeValidation.Rules; +using HotChocolate.Skimmed.Serialization; + +namespace HotChocolate.Composition.PreMergeValidation.Rules; + +public sealed class DisallowedInaccessibleElementsRuleTests +{ + [Test] + [MethodDataSource(nameof(ValidExamplesData))] + public async Task Examples_Valid(string[] sdl) + { + // arrange + var log = new CompositionLog(); + var context = new PreMergeValidationContext( + new CompositionContext(sdl.Select(SchemaParser.Parse).ToArray(), log)); + + context.Initialize(); + + // act + var result = new DisallowedInaccessibleElementsRule().Run(context); + + // assert + await Assert.That(result.IsSuccess).IsTrue(); + await Assert.That(log.IsEmpty).IsTrue(); + } + + [Test] + [MethodDataSource(nameof(InvalidExamplesData))] + public async Task Examples_Invalid(string[] sdl) + { + // arrange + var log = new CompositionLog(); + var context = new PreMergeValidationContext( + new CompositionContext(sdl.Select(SchemaParser.Parse).ToArray(), log)); + + context.Initialize(); + + // act + var result = new DisallowedInaccessibleElementsRule().Run(context); + + // assert + await Assert.That(result.IsFailure).IsTrue(); + await Assert.That(log.EntryCount).IsEqualTo(1); + await Assert.That(log.Entries[0].Code).IsEqualTo("DISALLOWED_INACCESSIBLE"); + await Assert.That(log.Entries[0].Severity).IsEqualTo(LogSeverity.Error); + } + + public static IEnumerable> ValidExamplesData() + { + return + [ + // Here, the String type is not marked as @inaccessible, which adheres to the rule. + () => + [ + """ + type Product { + price: Float + name: String + } + """ + ] + ]; + } + + public static IEnumerable> InvalidExamplesData() + { + return + [ + // In this example, the String scalar is marked as @inaccessible. This violates the rule + // because String is a required built-in type that cannot be inaccessible. + () => + [ + """ + scalar String @inaccessible + + type Product { + price: Float + name: String + } + """ + ], + // In this example, the introspection type __Type is marked as @inaccessible. This + // violates the rule because introspection types must remain accessible for GraphQL + // introspection queries to work. + () => + [ + """ + type __Type @inaccessible { + kind: __TypeKind! + name: String + fields(includeDeprecated: Boolean = false): [__Field!] + } + """ + ], + // Inaccessible introspection field. + () => + [ + """ + type __Type { + kind: __TypeKind! @inaccessible + name: String + fields(includeDeprecated: Boolean = false): [__Field!] + } + """ + ], + // Inaccessible introspection argument. + () => + [ + """ + type __Type { + kind: __TypeKind! + name: String + fields(includeDeprecated: Boolean = false @inaccessible): [__Field!] + } + """ + ] + // Inaccessible directive argument. + // Skip("Requires support for directives on directive arguments.") + ]; + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs new file mode 100644 index 00000000000..292244b425c --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs @@ -0,0 +1,144 @@ +using HotChocolate.Fusion; +using HotChocolate.Fusion.Logging; +using HotChocolate.Fusion.PreMergeValidation; +using HotChocolate.Fusion.PreMergeValidation.Rules; +using HotChocolate.Skimmed.Serialization; + +namespace HotChocolate.Composition.PreMergeValidation.Rules; + +public sealed class OutputFieldTypesMergeableRuleTests +{ + [Test] + [MethodDataSource(nameof(ValidExamplesData))] + public async Task Examples_Valid(string[] sdl) + { + // arrange + var log = new CompositionLog(); + var context = new PreMergeValidationContext( + new CompositionContext(sdl.Select(SchemaParser.Parse).ToArray(), log)); + + context.Initialize(); + + // act + var result = new OutputFieldTypesMergeableRule().Run(context); + + // assert + await Assert.That(result.IsSuccess).IsTrue(); + await Assert.That(log.IsEmpty).IsTrue(); + } + + [Test] + [MethodDataSource(nameof(InvalidExamplesData))] + public async Task Examples_Invalid(string[] sdl) + { + // arrange + var log = new CompositionLog(); + var context = new PreMergeValidationContext( + new CompositionContext(sdl.Select(SchemaParser.Parse).ToArray(), log)); + + context.Initialize(); + + // act + var result = new OutputFieldTypesMergeableRule().Run(context); + + // assert + await Assert.That(result.IsFailure).IsTrue(); + await Assert.That(log.EntryCount).IsEqualTo(1); + await Assert.That(log.Entries[0].Code).IsEqualTo("OUTPUT_FIELD_TYPES_NOT_MERGEABLE"); + await Assert.That(log.Entries[0].Severity).IsEqualTo(LogSeverity.Error); + } + + public static IEnumerable> ValidExamplesData() + { + return + [ + // Fields with the same type are mergeable. + () => + [ + """ + type User { + birthdate: String + } + """, + """ + type User { + birthdate: String + } + """ + ], + // Fields with different nullability are mergeable, resulting in a merged field with a + // nullable type. + () => + [ + """ + type User { + birthdate: String! + } + """, + """ + type User { + birthdate: String + } + """ + ], + () => + [ + """ + type User { + tags: [String!] + } + """, + """ + type User { + tags: [String]! + } + """, + """ + type User { + tags: [String] + } + """ + ] + ]; + } + + public static IEnumerable> InvalidExamplesData() + { + return + [ + // Fields are not mergeable if the named types are different in kind or name. + () => + [ + """ + type User { + birthdate: String! + } + """, + """ + type User { + birthdate: DateTime! + } + """ + ], + () => + [ + """ + type User { + tags: [Tag] + } + + type Tag { + value: String + } + """, + """ + type User { + tags: [Tag] + } + + scalar Tag + """ + ] + ]; + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/ValidationHelperTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/ValidationHelperTests.cs new file mode 100644 index 00000000000..6f3f078e63f --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/ValidationHelperTests.cs @@ -0,0 +1,78 @@ +using HotChocolate.Fusion; +using HotChocolate.Skimmed; +using HotChocolate.Skimmed.Serialization; + +namespace HotChocolate.Composition; + +public sealed class ValidationHelperTests +{ + [Test] + [Arguments("Int", "Int")] + [Arguments("[Int]", "[Int]")] + [Arguments("[[Int]]", "[[Int]]")] + // Different nullability. + [Arguments("Int", "Int!")] + [Arguments("[Int]", "[Int!]")] + [Arguments("[Int]", "[Int!]!")] + [Arguments("[[Int]]", "[[Int!]]")] + [Arguments("[[Int]]", "[[Int!]!]")] + [Arguments("[[Int]]", "[[Int!]!]!")] + public async Task SameOutputTypeShape_True(string sdlTypeA, string sdlTypeB) + { + // arrange + var schema1 = SchemaParser.Parse($$"""type Test { field: {{sdlTypeA}} }"""); + var schema2 = SchemaParser.Parse($$"""type Test { field: {{sdlTypeB}} }"""); + var typeA = ((ObjectTypeDefinition)schema1.Types["Test"]).Fields["field"].Type; + var typeB = ((ObjectTypeDefinition)schema2.Types["Test"]).Fields["field"].Type; + + // act + var result = ValidationHelper.SameOutputTypeShape(typeA, typeB); + + // assert + await Assert.That(result).IsTrue(); + } + + [Test] + // Different type kind. + [Arguments("Tag", "Tag")] + [Arguments("[Tag]", "[Tag]")] + [Arguments("[[Tag]]", "[[Tag]]")] + // Different type name. + [Arguments("String", "DateTime")] + [Arguments("[String]", "[DateTime]")] + [Arguments("[[String]]", "[[DateTime]]")] + // Different depth. + [Arguments("String", "[String]")] + [Arguments("String", "[[String]]")] + [Arguments("[String]", "[[String]]")] + [Arguments("[[String]]", "[[[String]]]")] + // Different depth and nullability. + [Arguments("String", "[String!]")] + [Arguments("String", "[String!]!")] + public async Task SameOutputTypeShape_False(string sdlTypeA, string sdlTypeB) + { + // arrange + var schema1 = SchemaParser.Parse( + $$""" + type Test { field: {{sdlTypeA}} } + + type Tag { value: String } + """); + + var schema2 = SchemaParser.Parse( + $$""" + type Test { field: {{sdlTypeB}} } + + scalar Tag + """); + + var typeA = ((ObjectTypeDefinition)schema1.Types["Test"]).Fields["field"].Type; + var typeB = ((ObjectTypeDefinition)schema2.Types["Test"]).Fields["field"].Type; + + // act + var result = ValidationHelper.SameOutputTypeShape(typeA, typeB); + + // assert + await Assert.That(result).IsFalse(); + } +} diff --git a/src/HotChocolate/Skimmed/src/Skimmed/Extensions/TypeExtensions.cs b/src/HotChocolate/Skimmed/src/Skimmed/Extensions/TypeExtensions.cs index 7b8fa0de483..a1a081157a9 100644 --- a/src/HotChocolate/Skimmed/src/Skimmed/Extensions/TypeExtensions.cs +++ b/src/HotChocolate/Skimmed/src/Skimmed/Extensions/TypeExtensions.cs @@ -48,6 +48,15 @@ public static ITypeDefinition InnerType(this ITypeDefinition type) _ => type, }; + public static ITypeDefinition NullableType(this ITypeDefinition type) + { + ArgumentNullException.ThrowIfNull(type); + + return type.Kind == TypeKind.NonNull + ? ((NonNullTypeDefinition)type).NullableType + : type; + } + public static INamedTypeDefinition NamedType(this ITypeDefinition type) { while (true) From 16b3790e04a89cf6cad4bc76423625ee4427b011 Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 5 Dec 2024 19:25:07 +0200 Subject: [PATCH 02/13] Renamed `Result` to `CompositionResult` --- .../PostMergeValidation/PostMergeValidator.cs | 4 +- .../Contracts/IPreMergeValidationRule.cs | 2 +- .../PreMergeValidation/PreMergeValidator.cs | 2 +- .../DisallowedInaccessibleElementsRule.cs | 4 +- .../Rules/OutputFieldTypesMergeableRule.cs | 4 +- .../Results/CompositionResult.cs | 52 +++++++++++++ .../Results/CompositionResult~1.cs | 73 +++++++++++++++++++ .../src/Fusion.Composition/Results/Result.cs | 52 ------------- .../Fusion.Composition/Results/Result~1.cs | 73 ------------------- .../SatisfiabilityValidator.cs | 4 +- .../src/Fusion.Composition/SchemaComposer.cs | 2 +- .../Fusion.Composition/SourceSchemaMerger.cs | 4 +- .../SourceSchemaValidator.cs | 4 +- 13 files changed, 140 insertions(+), 140 deletions(-) create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs delete mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result.cs delete mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result~1.cs diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidation/PostMergeValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidation/PostMergeValidator.cs index 305e843cdc3..fb5234a33b3 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidation/PostMergeValidator.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidation/PostMergeValidator.cs @@ -5,9 +5,9 @@ namespace HotChocolate.Fusion.PostMergeValidation; internal sealed class PostMergeValidator { - public Result Validate(SchemaDefinition _) + public CompositionResult Validate(SchemaDefinition _) { // FIXME: Implement. - return Result.Success(); + return CompositionResult.Success(); } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Contracts/IPreMergeValidationRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Contracts/IPreMergeValidationRule.cs index 0c2f43e675b..a9a60320a91 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Contracts/IPreMergeValidationRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Contracts/IPreMergeValidationRule.cs @@ -4,5 +4,5 @@ namespace HotChocolate.Fusion.PreMergeValidation.Contracts; internal interface IPreMergeValidationRule { - Result Run(PreMergeValidationContext context); + CompositionResult Run(PreMergeValidationContext context); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs index 9931aa12dd4..ecc263d61ef 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs @@ -13,7 +13,7 @@ internal sealed class PreMergeValidator new OutputFieldTypesMergeableRule() ]; - public Result Validate(CompositionContext compositionContext) + public CompositionResult Validate(CompositionContext compositionContext) { var preMergeValidationContext = new PreMergeValidationContext(compositionContext); preMergeValidationContext.Initialize(); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs index 6b47364dbe3..f4302e82a71 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs @@ -16,7 +16,7 @@ namespace HotChocolate.Fusion.PreMergeValidation.Rules; /// internal sealed class DisallowedInaccessibleElementsRule : IPreMergeValidationRule { - public Result Run(PreMergeValidationContext context) + public CompositionResult Run(PreMergeValidationContext context) { var loggingSession = context.Log.CreateSession(); @@ -93,7 +93,7 @@ public Result Run(PreMergeValidationContext context) } return loggingSession.ErrorCount == 0 - ? Result.Success() + ? CompositionResult.Success() : ErrorHelper.PreMergeValidationRuleFailed(this); } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs index a5616549280..50be776caa6 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs @@ -13,7 +13,7 @@ namespace HotChocolate.Fusion.PreMergeValidation.Rules; /// internal sealed class OutputFieldTypesMergeableRule : IPreMergeValidationRule { - public Result Run(PreMergeValidationContext context) + public CompositionResult Run(PreMergeValidationContext context) { var loggingSession = context.Log.CreateSession(); @@ -32,7 +32,7 @@ public Result Run(PreMergeValidationContext context) } return loggingSession.ErrorCount == 0 - ? Result.Success() + ? CompositionResult.Success() : ErrorHelper.PreMergeValidationRuleFailed(this); } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs new file mode 100644 index 00000000000..53f6fbb65d1 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs @@ -0,0 +1,52 @@ +namespace HotChocolate.Fusion.Results; + +public readonly record struct CompositionResult +{ + public bool IsFailure { get; } + + public bool IsSuccess { get; } + + public List Errors { get; } = []; + + public CompositionResult() + { + IsSuccess = true; + } + + private CompositionResult(Error error) + { + Errors = [error]; + IsFailure = true; + } + + private CompositionResult(List errors) + { + if (errors.Count == 0) + { + IsSuccess = true; + } + else + { + Errors = [.. errors]; + IsFailure = true; + } + } + + public static CompositionResult Success() => new(); + + /// + /// Creates a from an error. + /// + public static implicit operator CompositionResult(Error error) + { + return new CompositionResult(error); + } + + /// + /// Creates a from a list of errors. + /// + public static implicit operator CompositionResult(List errors) + { + return new CompositionResult(errors); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs new file mode 100644 index 00000000000..31163b7f49c --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs @@ -0,0 +1,73 @@ +namespace HotChocolate.Fusion.Results; + +public readonly record struct CompositionResult +{ + public bool IsFailure { get; } + + public bool IsSuccess { get; } + + public List Errors { get; } = []; + + public TValue Value => IsSuccess + ? _value + : throw new Exception("Value may not be accessed on an unsuccessful result."); + + private readonly TValue _value = default!; + + private CompositionResult(TValue value) + { + _value = value; + IsSuccess = true; + } + + private CompositionResult(Error error) + { + Errors = [error]; + IsFailure = true; + } + + private CompositionResult(List errors) + { + if (errors.Count == 0) + { + IsSuccess = true; + } + else + { + Errors = [.. errors]; + IsFailure = true; + } + } + + /// + /// Creates a from a value. + /// + public static implicit operator CompositionResult(TValue value) + { + return new CompositionResult(value); + } + + /// + /// Creates a from an error. + /// + public static implicit operator CompositionResult(Error error) + { + return new CompositionResult(error); + } + + /// + /// Creates a from a list of errors. + /// + public static implicit operator CompositionResult(List errors) + { + return new CompositionResult(errors); + } + + /// + /// Creates a from a . + /// + public static implicit operator CompositionResult(CompositionResult result) + { + return new CompositionResult(result.Errors); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result.cs deleted file mode 100644 index 9d3dbec7e8c..00000000000 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace HotChocolate.Fusion.Results; - -public readonly record struct Result -{ - public bool IsFailure { get; } - - public bool IsSuccess { get; } - - public List Errors { get; } = []; - - public Result() - { - IsSuccess = true; - } - - private Result(Error error) - { - Errors = [error]; - IsFailure = true; - } - - private Result(List errors) - { - if (errors.Count == 0) - { - IsSuccess = true; - } - else - { - Errors = [.. errors]; - IsFailure = true; - } - } - - public static Result Success() => new(); - - /// - /// Creates a from an error. - /// - public static implicit operator Result(Error error) - { - return new Result(error); - } - - /// - /// Creates a from a list of errors. - /// - public static implicit operator Result(List errors) - { - return new Result(errors); - } -} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result~1.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result~1.cs deleted file mode 100644 index 0e527db3e7e..00000000000 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Result~1.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace HotChocolate.Fusion.Results; - -public readonly record struct Result -{ - public bool IsFailure { get; } - - public bool IsSuccess { get; } - - public List Errors { get; } = []; - - public TValue Value => IsSuccess - ? _value - : throw new Exception("Value may not be accessed on an unsuccessful result."); - - private readonly TValue _value = default!; - - private Result(TValue value) - { - _value = value; - IsSuccess = true; - } - - private Result(Error error) - { - Errors = [error]; - IsFailure = true; - } - - private Result(List errors) - { - if (errors.Count == 0) - { - IsSuccess = true; - } - else - { - Errors = [.. errors]; - IsFailure = true; - } - } - - /// - /// Creates a from a value. - /// - public static implicit operator Result(TValue value) - { - return new Result(value); - } - - /// - /// Creates a from an error. - /// - public static implicit operator Result(Error error) - { - return new Result(error); - } - - /// - /// Creates a from a list of errors. - /// - public static implicit operator Result(List errors) - { - return new Result(errors); - } - - /// - /// Creates a from a . - /// - public static implicit operator Result(Result result) - { - return new Result(result.Errors); - } -} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SatisfiabilityValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SatisfiabilityValidator.cs index b4691e76ace..70be0ec4862 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SatisfiabilityValidator.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SatisfiabilityValidator.cs @@ -5,9 +5,9 @@ namespace HotChocolate.Fusion; internal sealed class SatisfiabilityValidator { - public Result Validate(SchemaDefinition _) + public CompositionResult Validate(SchemaDefinition _) { // FIXME: Implement. - return Result.Success(); + return CompositionResult.Success(); } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs index 4bb627b94b6..9ff27ee5cb9 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs @@ -6,7 +6,7 @@ namespace HotChocolate.Fusion; public sealed class SchemaComposer { - public Result Compose( + public CompositionResult Compose( SchemaDefinition[] schemaDefinitions, ICompositionLog compositionLog) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs index 541af4c7d48..eb2266bc711 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs @@ -7,7 +7,7 @@ namespace HotChocolate.Fusion; internal sealed class SourceSchemaMerger { - public Result Merge(CompositionContext context) + public CompositionResult Merge(CompositionContext context) { // Pre Merge Validation var preMergeValidationResult = new PreMergeValidator().Validate(context); @@ -36,7 +36,7 @@ public Result Merge(CompositionContext context) return mergeResult; } - private Result MergeSchemaDefinitions(CompositionContext _) + private CompositionResult MergeSchemaDefinitions(CompositionContext _) { // FIXME: Implement. return new SchemaDefinition(); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaValidator.cs index 77d1410f89d..67e91bc4258 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaValidator.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaValidator.cs @@ -4,9 +4,9 @@ namespace HotChocolate.Fusion; internal sealed class SourceSchemaValidator { - public Result Validate(CompositionContext _) + public CompositionResult Validate(CompositionContext _) { // FIXME: Implement. - return Result.Success(); + return CompositionResult.Success(); } } From faf4d3867a45a27d4326cd8547084205600a6ac6 Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 5 Dec 2024 19:50:27 +0200 Subject: [PATCH 03/13] Renamed `Error` to `CompositionError` --- .../Errors/CompositionError.cs | 6 ++++++ .../{Results => Errors}/ErrorHelper.cs | 4 ++-- .../PreMergeValidation/PreMergeValidator.cs | 3 ++- .../Rules/DisallowedInaccessibleElementsRule.cs | 1 + .../Rules/OutputFieldTypesMergeableRule.cs | 1 + .../Results/CompositionResult.cs | 16 +++++++++------- .../Results/CompositionResult~1.cs | 16 +++++++++------- .../src/Fusion.Composition/Results/Error.cs | 6 ------ 8 files changed, 30 insertions(+), 23 deletions(-) create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/CompositionError.cs rename src/HotChocolate/Fusion-vnext/src/Fusion.Composition/{Results => Errors}/ErrorHelper.cs (65%) delete mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Error.cs diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/CompositionError.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/CompositionError.cs new file mode 100644 index 00000000000..c4081105cd5 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/CompositionError.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Fusion.Errors; + +public sealed class CompositionError(string message) +{ + public string Message { get; } = message; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/ErrorHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/ErrorHelper.cs similarity index 65% rename from src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/ErrorHelper.cs rename to src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/ErrorHelper.cs index 5a84a057f7e..4841f244835 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/ErrorHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/ErrorHelper.cs @@ -1,10 +1,10 @@ using HotChocolate.Fusion.PreMergeValidation.Contracts; using static HotChocolate.Fusion.Properties.CompositionResources; -namespace HotChocolate.Fusion.Results; +namespace HotChocolate.Fusion.Errors; internal static class ErrorHelper { - public static Error PreMergeValidationRuleFailed(IPreMergeValidationRule rule) + public static CompositionError PreMergeValidationRuleFailed(IPreMergeValidationRule rule) => new(string.Format(ErrorHelper_PreMergeValidationRuleFailed, rule.GetType().Name)); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs index ecc263d61ef..1d812523f12 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using HotChocolate.Fusion.Errors; using HotChocolate.Fusion.PreMergeValidation.Contracts; using HotChocolate.Fusion.PreMergeValidation.Rules; using HotChocolate.Fusion.Results; @@ -18,7 +19,7 @@ public CompositionResult Validate(CompositionContext compositionContext) var preMergeValidationContext = new PreMergeValidationContext(compositionContext); preMergeValidationContext.Initialize(); - var errors = new List(); + var errors = new List(); foreach (var validationRule in _validationRules) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs index f4302e82a71..9e91fb776d2 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs @@ -1,3 +1,4 @@ +using HotChocolate.Fusion.Errors; using HotChocolate.Fusion.PreMergeValidation.Contracts; using HotChocolate.Fusion.Results; using HotChocolate.Skimmed; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs index 50be776caa6..e93c6e91b14 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs @@ -1,3 +1,4 @@ +using HotChocolate.Fusion.Errors; using HotChocolate.Fusion.Logging; using HotChocolate.Fusion.PreMergeValidation.Contracts; using HotChocolate.Fusion.Results; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs index 53f6fbb65d1..7ad673a0880 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs @@ -1,3 +1,5 @@ +using HotChocolate.Fusion.Errors; + namespace HotChocolate.Fusion.Results; public readonly record struct CompositionResult @@ -6,20 +8,20 @@ public readonly record struct CompositionResult public bool IsSuccess { get; } - public List Errors { get; } = []; + public List Errors { get; } = []; public CompositionResult() { IsSuccess = true; } - private CompositionResult(Error error) + private CompositionResult(CompositionError error) { Errors = [error]; IsFailure = true; } - private CompositionResult(List errors) + private CompositionResult(List errors) { if (errors.Count == 0) { @@ -35,17 +37,17 @@ private CompositionResult(List errors) public static CompositionResult Success() => new(); /// - /// Creates a from an error. + /// Creates a from a composition error. /// - public static implicit operator CompositionResult(Error error) + public static implicit operator CompositionResult(CompositionError error) { return new CompositionResult(error); } /// - /// Creates a from a list of errors. + /// Creates a from a list of composition errors. /// - public static implicit operator CompositionResult(List errors) + public static implicit operator CompositionResult(List errors) { return new CompositionResult(errors); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs index 31163b7f49c..4b323641379 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs @@ -1,3 +1,5 @@ +using HotChocolate.Fusion.Errors; + namespace HotChocolate.Fusion.Results; public readonly record struct CompositionResult @@ -6,7 +8,7 @@ public readonly record struct CompositionResult public bool IsSuccess { get; } - public List Errors { get; } = []; + public List Errors { get; } = []; public TValue Value => IsSuccess ? _value @@ -20,13 +22,13 @@ private CompositionResult(TValue value) IsSuccess = true; } - private CompositionResult(Error error) + private CompositionResult(CompositionError error) { Errors = [error]; IsFailure = true; } - private CompositionResult(List errors) + private CompositionResult(List errors) { if (errors.Count == 0) { @@ -48,17 +50,17 @@ public static implicit operator CompositionResult(TValue value) } /// - /// Creates a from an error. + /// Creates a from a composition error. /// - public static implicit operator CompositionResult(Error error) + public static implicit operator CompositionResult(CompositionError error) { return new CompositionResult(error); } /// - /// Creates a from a list of errors. + /// Creates a from a list of composition errors. /// - public static implicit operator CompositionResult(List errors) + public static implicit operator CompositionResult(List errors) { return new CompositionResult(errors); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Error.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Error.cs deleted file mode 100644 index b743614be15..00000000000 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/Error.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace HotChocolate.Fusion.Results; - -public sealed class Error(string message) -{ - public string Message { get; } = message; -} From 118aaefc9466bb32636e604af95b7890886df1e3 Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 5 Dec 2024 19:53:08 +0200 Subject: [PATCH 04/13] Removed spaces on empty line --- .../Fusion.Composition/HotChocolate.Fusion.Composition.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj index f6bb555f9d9..a7d4025054b 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj @@ -4,7 +4,7 @@ HotChocolate.Fusion.Composition HotChocolate.Fusion - + From 43ca1df049e4f2ccac300fe2ecd403acd813041d Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 6 Dec 2024 09:23:21 +0200 Subject: [PATCH 05/13] Removed public list of entries from CompositionLog --- .../Fusion.Composition/Logging/CompositionLog.cs | 15 +++++++++------ .../Logging/Contracts/ICompositionLog.cs | 4 ---- .../DisallowedInaccessibleElementsRuleTests.cs | 6 +++--- .../Rules/OutputFieldTypesMergeableRuleTests.cs | 6 +++--- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs index 1b257536cb6..2f9cc4b9cea 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs @@ -1,24 +1,27 @@ +using System.Collections; using HotChocolate.Fusion.Logging.Contracts; namespace HotChocolate.Fusion.Logging; -public sealed class CompositionLog : ICompositionLog +public sealed class CompositionLog : ICompositionLog, IEnumerable { - public IList Entries { get; } = []; + public bool IsEmpty => _entries.Count == 0; - public int EntryCount => Entries.Count; - - public bool IsEmpty => Entries.Count == 0; + private readonly List _entries = []; public void Write(LogEntry entry) { ArgumentNullException.ThrowIfNull(entry); - Entries.Add(entry); + _entries.Add(entry); } public ILoggingSession CreateSession() { return new LoggingSession(this); } + + public IEnumerator GetEnumerator() => _entries.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs index ac5281dc307..1251c6adf64 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs @@ -5,10 +5,6 @@ namespace HotChocolate.Fusion.Logging.Contracts; /// public interface ICompositionLog { - IList Entries { get; } - - int EntryCount { get; } - bool IsEmpty { get; } /// diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs index 5a890538d09..e793cbaa80c 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs @@ -43,9 +43,9 @@ public async Task Examples_Invalid(string[] sdl) // assert await Assert.That(result.IsFailure).IsTrue(); - await Assert.That(log.EntryCount).IsEqualTo(1); - await Assert.That(log.Entries[0].Code).IsEqualTo("DISALLOWED_INACCESSIBLE"); - await Assert.That(log.Entries[0].Severity).IsEqualTo(LogSeverity.Error); + await Assert.That(log.Count()).IsEqualTo(1); + await Assert.That(log.First().Code).IsEqualTo("DISALLOWED_INACCESSIBLE"); + await Assert.That(log.First().Severity).IsEqualTo(LogSeverity.Error); } public static IEnumerable> ValidExamplesData() diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs index 292244b425c..d1d6fb1d2d3 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs @@ -43,9 +43,9 @@ public async Task Examples_Invalid(string[] sdl) // assert await Assert.That(result.IsFailure).IsTrue(); - await Assert.That(log.EntryCount).IsEqualTo(1); - await Assert.That(log.Entries[0].Code).IsEqualTo("OUTPUT_FIELD_TYPES_NOT_MERGEABLE"); - await Assert.That(log.Entries[0].Severity).IsEqualTo(LogSeverity.Error); + await Assert.That(log.Count()).IsEqualTo(1); + await Assert.That(log.First().Code).IsEqualTo("OUTPUT_FIELD_TYPES_NOT_MERGEABLE"); + await Assert.That(log.First().Severity).IsEqualTo(LogSeverity.Error); } public static IEnumerable> ValidExamplesData() From ad04750ed06056afd087b4cf64f61239608723a4 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 6 Dec 2024 09:47:21 +0200 Subject: [PATCH 06/13] Added missing null checks --- .../Fusion-vnext/src/Fusion.Composition/Logging/LogEntry.cs | 3 +++ .../src/Fusion.Composition/Logging/LoggingSession.cs | 2 ++ .../src/Fusion.Composition/Results/CompositionResult.cs | 4 ++++ .../src/Fusion.Composition/Results/CompositionResult~1.cs | 4 ++++ .../Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs | 3 +++ 5 files changed, 16 insertions(+) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntry.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntry.cs index 9500970e779..cad3c9a3d28 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntry.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntry.cs @@ -20,6 +20,9 @@ public LogEntry( SchemaDefinition? schema = null, object? extension = null) { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(code); + Message = message; Code = code; Severity = severity; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LoggingSession.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LoggingSession.cs index 1e4d1e1d014..6ee1b23df63 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LoggingSession.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LoggingSession.cs @@ -12,6 +12,8 @@ public sealed class LoggingSession(ICompositionLog compositionLog) : ILoggingSes public void Write(LogEntry entry) { + ArgumentNullException.ThrowIfNull(entry); + switch (entry.Severity) { case LogSeverity.Info: diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs index 7ad673a0880..7f40c5e09d1 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs @@ -41,6 +41,8 @@ private CompositionResult(List errors) /// public static implicit operator CompositionResult(CompositionError error) { + ArgumentNullException.ThrowIfNull(error); + return new CompositionResult(error); } @@ -49,6 +51,8 @@ public static implicit operator CompositionResult(CompositionError error) /// public static implicit operator CompositionResult(List errors) { + ArgumentNullException.ThrowIfNull(errors); + return new CompositionResult(errors); } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs index 4b323641379..0b44d938e15 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs @@ -54,6 +54,8 @@ public static implicit operator CompositionResult(TValue value) /// public static implicit operator CompositionResult(CompositionError error) { + ArgumentNullException.ThrowIfNull(error); + return new CompositionResult(error); } @@ -62,6 +64,8 @@ public static implicit operator CompositionResult(CompositionError error /// public static implicit operator CompositionResult(List errors) { + ArgumentNullException.ThrowIfNull(errors); + return new CompositionResult(errors); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs index 9ff27ee5cb9..d8adb281e2c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs @@ -10,6 +10,9 @@ public CompositionResult Compose( SchemaDefinition[] schemaDefinitions, ICompositionLog compositionLog) { + ArgumentNullException.ThrowIfNull(schemaDefinitions); + ArgumentNullException.ThrowIfNull(compositionLog); + var context = new CompositionContext(schemaDefinitions, compositionLog); // Validate Source Schemas From 47238b1b41efc3e4dc937280382822be2cac712a Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 6 Dec 2024 11:30:47 +0200 Subject: [PATCH 07/13] Refactored InitializeOutputTypeInfo to avoid multiple enumeration --- .../PreMergeValidationContext.cs | 74 ++++++++++--------- .../Fusion.Composition/ValidationHelper.cs | 3 +- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs index bc34101eeb3..65adbbbb326 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using HotChocolate.Fusion.Logging.Contracts; using HotChocolate.Skimmed; @@ -7,7 +8,7 @@ internal sealed class PreMergeValidationContext(CompositionContext context) { public SchemaDefinition[] SchemaDefinitions => context.SchemaDefinitions; public ICompositionLog Log => context.Log; - public IEnumerable OutputTypeInfo = []; + public ImmutableArray OutputTypeInfo = []; public void Initialize() { @@ -20,50 +21,57 @@ public void Initialize() /// private void InitializeOutputTypeInfo() { - OutputTypeInfo = SchemaDefinitions - .SelectMany(s => s.Types) - .Where(t => t.IsOutputType()) - .OfType() - .GroupBy(t => t.Name, (typeName, types) => - { - types = types.ToArray(); + OutputTypeInfo = + [ + .. SchemaDefinitions + .SelectMany(s => s.Types) + .Where(t => t.IsOutputType()) + .OfType() + .GroupBy( + t => t.Name, + (typeName, types) => + { + types = types.ToImmutableArray(); - var fieldInfo = types - .SelectMany(t => t.Fields) - .GroupBy( - f => f.Name, - (fieldName, fields) => - { - fields = fields.ToArray(); + var fieldInfo = types + .SelectMany(t => t.Fields) + .GroupBy( + f => f.Name, + (fieldName, fields) => + { + fields = fields.ToImmutableArray(); - var argumentInfo = fields - .SelectMany(f => f.Arguments) - .GroupBy( - a => a.Name, - (argumentName, arguments) => - new OutputArgumentInfo(argumentName, arguments.ToArray())); + var argumentInfo = fields + .SelectMany(f => f.Arguments) + .GroupBy( + a => a.Name, + (argumentName, arguments) => + new OutputArgumentInfo( + argumentName, + [.. arguments])); - return new OutputFieldInfo( - fieldName, - fields.ToArray(), - argumentInfo.ToArray()); - }); + return new OutputFieldInfo( + fieldName, + [.. fields], + [.. argumentInfo]); + }); - return new OutputTypeInfo(typeName, types.ToArray(), fieldInfo.ToArray()); - }); + return new OutputTypeInfo(typeName, [.. types], [.. fieldInfo]); + }) + ]; } } internal record OutputTypeInfo( string TypeName, - ComplexTypeDefinition[] Types, - OutputFieldInfo[] FieldInfo); + ImmutableArray Types, + ImmutableArray FieldInfo); internal record OutputFieldInfo( string FieldName, - OutputFieldDefinition[] Fields, - OutputArgumentInfo[] Arguments); + ImmutableArray Fields, + ImmutableArray Arguments); internal record OutputArgumentInfo( string ArgumentName, - InputFieldDefinition[] Arguments); + ImmutableArray Arguments); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs index 25745fc40b3..fb50f76f965 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using HotChocolate.Fusion.Extensions; using HotChocolate.Skimmed; @@ -5,7 +6,7 @@ namespace HotChocolate.Fusion; internal sealed class ValidationHelper { - public static bool FieldsAreMergeable(OutputFieldDefinition[] fields) + public static bool FieldsAreMergeable(ImmutableArray fields) { for (var i = 0; i < fields.Length - 1; i++) { From 1a263c4bb40854e35bd10d4770f94b6642eaab41 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 6 Dec 2024 12:00:59 +0200 Subject: [PATCH 08/13] Used ImmutableArray internally --- .../src/Fusion.Composition/CompositionContext.cs | 5 +++-- .../PreMergeValidation/PreMergeValidationContext.cs | 2 +- .../Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs | 4 ++-- .../Rules/DisallowedInaccessibleElementsRuleTests.cs | 4 ++-- .../Rules/OutputFieldTypesMergeableRuleTests.cs | 4 ++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionContext.cs index 1962a855449..a356991c860 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionContext.cs @@ -1,16 +1,17 @@ +using System.Collections.Immutable; using HotChocolate.Fusion.Logging.Contracts; using HotChocolate.Skimmed; namespace HotChocolate.Fusion; internal sealed class CompositionContext( - SchemaDefinition[] schemaDefinitions, + ImmutableArray schemaDefinitions, ICompositionLog compositionLog) { /// /// Gets the schema definitions. /// - public SchemaDefinition[] SchemaDefinitions { get; } = schemaDefinitions; + public ImmutableArray SchemaDefinitions { get; } = schemaDefinitions; /// /// Gets the composition log. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs index 65adbbbb326..538df65d0ca 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs @@ -6,7 +6,7 @@ namespace HotChocolate.Fusion.PreMergeValidation; internal sealed class PreMergeValidationContext(CompositionContext context) { - public SchemaDefinition[] SchemaDefinitions => context.SchemaDefinitions; + public ImmutableArray SchemaDefinitions => context.SchemaDefinitions; public ICompositionLog Log => context.Log; public ImmutableArray OutputTypeInfo = []; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs index d8adb281e2c..e4525fba52c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs @@ -7,13 +7,13 @@ namespace HotChocolate.Fusion; public sealed class SchemaComposer { public CompositionResult Compose( - SchemaDefinition[] schemaDefinitions, + IEnumerable schemaDefinitions, ICompositionLog compositionLog) { ArgumentNullException.ThrowIfNull(schemaDefinitions); ArgumentNullException.ThrowIfNull(compositionLog); - var context = new CompositionContext(schemaDefinitions, compositionLog); + var context = new CompositionContext([.. schemaDefinitions], compositionLog); // Validate Source Schemas var validationResult = new SourceSchemaValidator().Validate(context); diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs index e793cbaa80c..284bc388f7d 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs @@ -15,7 +15,7 @@ public async Task Examples_Valid(string[] sdl) // arrange var log = new CompositionLog(); var context = new PreMergeValidationContext( - new CompositionContext(sdl.Select(SchemaParser.Parse).ToArray(), log)); + new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log)); context.Initialize(); @@ -34,7 +34,7 @@ public async Task Examples_Invalid(string[] sdl) // arrange var log = new CompositionLog(); var context = new PreMergeValidationContext( - new CompositionContext(sdl.Select(SchemaParser.Parse).ToArray(), log)); + new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log)); context.Initialize(); diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs index d1d6fb1d2d3..998ee86ef0d 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs @@ -15,7 +15,7 @@ public async Task Examples_Valid(string[] sdl) // arrange var log = new CompositionLog(); var context = new PreMergeValidationContext( - new CompositionContext(sdl.Select(SchemaParser.Parse).ToArray(), log)); + new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log)); context.Initialize(); @@ -34,7 +34,7 @@ public async Task Examples_Invalid(string[] sdl) // arrange var log = new CompositionLog(); var context = new PreMergeValidationContext( - new CompositionContext(sdl.Select(SchemaParser.Parse).ToArray(), log)); + new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log)); context.Initialize(); From fb5d7f02261f4bb1f55d313c8ea13bed7fe78feb Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 6 Dec 2024 12:34:49 +0200 Subject: [PATCH 09/13] Used an ImmutableArray for CompositionResult errors --- .../Results/CompositionResult.cs | 19 ++++++++++++++----- .../Results/CompositionResult~1.cs | 16 ++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs index 7f40c5e09d1..4bbe1f88e62 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using HotChocolate.Fusion.Errors; namespace HotChocolate.Fusion.Results; @@ -8,7 +9,7 @@ public readonly record struct CompositionResult public bool IsSuccess { get; } - public List Errors { get; } = []; + public ImmutableArray Errors { get; } = []; public CompositionResult() { @@ -21,15 +22,15 @@ private CompositionResult(CompositionError error) IsFailure = true; } - private CompositionResult(List errors) + private CompositionResult(ImmutableArray errors) { - if (errors.Count == 0) + if (errors.Length == 0) { IsSuccess = true; } else { - Errors = [.. errors]; + Errors = errors; IsFailure = true; } } @@ -46,6 +47,14 @@ public static implicit operator CompositionResult(CompositionError error) return new CompositionResult(error); } + /// + /// Creates a from an array of composition errors. + /// + public static implicit operator CompositionResult(ImmutableArray errors) + { + return new CompositionResult(errors); + } + /// /// Creates a from a list of composition errors. /// @@ -53,6 +62,6 @@ public static implicit operator CompositionResult(List errors) { ArgumentNullException.ThrowIfNull(errors); - return new CompositionResult(errors); + return new CompositionResult([.. errors]); } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs index 0b44d938e15..6e801fac65c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using HotChocolate.Fusion.Errors; namespace HotChocolate.Fusion.Results; @@ -8,7 +9,7 @@ public readonly record struct CompositionResult public bool IsSuccess { get; } - public List Errors { get; } = []; + public ImmutableArray Errors { get; } = []; public TValue Value => IsSuccess ? _value @@ -28,15 +29,15 @@ private CompositionResult(CompositionError error) IsFailure = true; } - private CompositionResult(List errors) + private CompositionResult(ImmutableArray errors) { - if (errors.Count == 0) + if (errors.Length == 0) { IsSuccess = true; } else { - Errors = [.. errors]; + Errors = errors; IsFailure = true; } } @@ -60,12 +61,11 @@ public static implicit operator CompositionResult(CompositionError error } /// - /// Creates a from a list of composition errors. + /// Creates a from an array of composition errors. /// - public static implicit operator CompositionResult(List errors) + public static implicit operator CompositionResult( + ImmutableArray errors) { - ArgumentNullException.ThrowIfNull(errors); - return new CompositionResult(errors); } From 82ddd2283b8646a974ac1ba2d222be6f30e91701 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 6 Dec 2024 12:59:38 +0200 Subject: [PATCH 10/13] Removed the code checking for inaccessible built-in directives --- .../src/Fusion.Composition/Logging/LogEntryHelper.cs | 11 ----------- .../Rules/DisallowedInaccessibleElementsRule.cs | 9 ++------- .../Properties/CompositionResources.Designer.cs | 9 --------- .../Properties/CompositionResources.resx | 3 --- .../src/Fusion.Composition/ValidationHelper.cs | 6 ------ 5 files changed, 2 insertions(+), 36 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs index 605ad8dffac..8c6044b4b85 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs @@ -62,17 +62,6 @@ public static LogEntry DisallowedInaccessibleIntrospectionArgument( schema); } - public static LogEntry DisallowedInaccessibleDirective( - Directive directive, - SchemaDefinition schema) - => new( - string.Format(LogEntryHelper_DisallowedInaccessibleDirective, directive.Name), - LogEntryCodes.DisallowedInaccessible, - LogSeverity.Error, - new SchemaCoordinate(directive.Name, ofDirective: true), - directive, - schema); - public static LogEntry DisallowedInaccessibleDirectiveArgument( ArgumentAssignment argument, string directiveName, diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs index 9e91fb776d2..9871532f48c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs @@ -8,8 +8,8 @@ namespace HotChocolate.Fusion.PreMergeValidation.Rules; /// /// This rule ensures that certain essential elements of a GraphQL schema, particularly built-in -/// scalars, directives, and introspection types, cannot be marked as @inaccessible. These types are -/// fundamental to GraphQL. Making these elements inaccessible would break core GraphQL +/// scalars, directive arguments, and introspection types, cannot be marked as @inaccessible. These +/// types are fundamental to GraphQL. Making these elements inaccessible would break core GraphQL /// functionality. /// /// @@ -73,11 +73,6 @@ public CompositionResult Run(PreMergeValidationContext context) { if (BuiltIns.IsBuiltInDirective(directive.Name)) { - if (!ValidationHelper.IsAccessible(directive)) - { - loggingSession.Write(DisallowedInaccessibleDirective(directive, schema)); - } - foreach (var argument in directive.Arguments) { if (!ValidationHelper.IsAccessible(argument)) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs index 99a90d4a1f4..b3a9cb1b02d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs @@ -68,15 +68,6 @@ internal static string ErrorHelper_PreMergeValidationRuleFailed { } } - /// - /// Looks up a localized string similar to The built-in directive type '{0}' is not accessible.. - /// - internal static string LogEntryHelper_DisallowedInaccessibleDirective { - get { - return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleDirective", resourceCulture); - } - } - /// /// Looks up a localized string similar to The argument '{0}' on built-in directive type '{1}' is not accessible.. /// diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx index eac20ae36c3..d291d0f232a 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx @@ -33,9 +33,6 @@ The introspection argument '{0}' with schema coordinate '{1}' is not accessible. - - The built-in directive type '{0}' is not accessible. - The argument '{0}' on built-in directive type '{1}' is not accessible. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs index fb50f76f965..ffc589f284a 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs @@ -28,12 +28,6 @@ public static bool IsAccessible(ArgumentAssignment _) return true; } - public static bool IsAccessible(Directive _) - { - // FIXME: Requires support for directives on directives. - return true; - } - public static bool IsAccessible(IDirectivesProvider type) { return !type.Directives.ContainsName(WellKnownDirectiveNames.Inaccessible); From dd28c92717fd8f6fd53f1826f09ab1d406a1d7ab Mon Sep 17 00:00:00 2001 From: Glen Date: Mon, 9 Dec 2024 10:45:08 +0200 Subject: [PATCH 11/13] Fixed check for inaccessible built-in directive arguments --- .../src/Fusion.Composition/Logging/LogEntryHelper.cs | 2 +- .../Rules/DisallowedInaccessibleElementsRule.cs | 2 +- .../src/Fusion.Composition/ValidationHelper.cs | 6 ------ .../Rules/DisallowedInaccessibleElementsRuleTests.cs | 10 ++++++++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs index 8c6044b4b85..f678870839e 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs @@ -63,7 +63,7 @@ public static LogEntry DisallowedInaccessibleIntrospectionArgument( } public static LogEntry DisallowedInaccessibleDirectiveArgument( - ArgumentAssignment argument, + InputFieldDefinition argument, string directiveName, SchemaDefinition schema) => new( diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs index 9871532f48c..2bd15ce472f 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs @@ -69,7 +69,7 @@ public CompositionResult Run(PreMergeValidationContext context) } } - foreach (var directive in schema.Directives) + foreach (var directive in schema.DirectiveDefinitions) { if (BuiltIns.IsBuiltInDirective(directive.Name)) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs index ffc589f284a..be3f0252ebe 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs @@ -22,12 +22,6 @@ public static bool FieldsAreMergeable(ImmutableArray fiel return true; } - public static bool IsAccessible(ArgumentAssignment _) - { - // FIXME: Requires support for directives on directive arguments. - return true; - } - public static bool IsAccessible(IDirectivesProvider type) { return !type.Directives.ContainsName(WellKnownDirectiveNames.Inaccessible); diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs index 284bc388f7d..b46fee89348 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs @@ -116,9 +116,15 @@ type __Type { fields(includeDeprecated: Boolean = false @inaccessible): [__Field!] } """ + ], + // Inaccessible built-in directive argument. + () => + [ + """ + directive @skip(if: Boolean! @inaccessible) + on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + """ ] - // Inaccessible directive argument. - // Skip("Requires support for directives on directive arguments.") ]; } } From a81ad6ec8e6eb35a0cbc7af4313f2e6358dec981 Mon Sep 17 00:00:00 2001 From: Glen Date: Mon, 9 Dec 2024 11:48:00 +0200 Subject: [PATCH 12/13] Added and used `IsIntrospectionType` method --- .../Rules/DisallowedInaccessibleElementsRule.cs | 3 +-- .../Skimmed/src/Skimmed/Contracts/ITypeDefinition.cs | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs index 2bd15ce472f..b01d1f11c78 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs @@ -31,8 +31,7 @@ public CompositionResult Run(PreMergeValidationContext context) loggingSession.Write(DisallowedInaccessibleScalar(scalar, schema)); } - // FIXME: Better way to check for introspection type. - if (type.Name.StartsWith("__")) + if (type.IsIntrospectionType) { if (!ValidationHelper.IsAccessible(type)) { diff --git a/src/HotChocolate/Skimmed/src/Skimmed/Contracts/ITypeDefinition.cs b/src/HotChocolate/Skimmed/src/Skimmed/Contracts/ITypeDefinition.cs index 26b0aca7ec5..5c0bd5e02ff 100644 --- a/src/HotChocolate/Skimmed/src/Skimmed/Contracts/ITypeDefinition.cs +++ b/src/HotChocolate/Skimmed/src/Skimmed/Contracts/ITypeDefinition.cs @@ -12,6 +12,11 @@ public interface ITypeDefinition : IEquatable /// TypeKind Kind { get; } + /// + /// Gets a value indicating whether the type is an introspection type. + /// + bool IsIntrospectionType => this is INamedTypeDefinition type && type.Name.StartsWith("__"); + /// /// Indicates whether the current object is equal to another object of the same type. /// From 0dfe2a923479fd2560eec160384c79075d1cb216 Mon Sep 17 00:00:00 2001 From: Glen Date: Mon, 9 Dec 2024 12:11:16 +0200 Subject: [PATCH 13/13] Aligned the `Same(Output)TypeShape` method with the spec --- .../Fusion.Composition/ValidationHelper.cs | 66 +++++++------------ .../ValidationHelperTests.cs | 8 +-- 2 files changed, 29 insertions(+), 45 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs index be3f0252ebe..85d98039087 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs @@ -1,5 +1,4 @@ using System.Collections.Immutable; -using HotChocolate.Fusion.Extensions; using HotChocolate.Skimmed; namespace HotChocolate.Fusion; @@ -13,7 +12,7 @@ public static bool FieldsAreMergeable(ImmutableArray fiel var typeA = fields[i].Type; var typeB = fields[i + 1].Type; - if (!SameOutputTypeShape(typeA, typeB)) + if (!SameTypeShape(typeA, typeB)) { return false; } @@ -27,63 +26,48 @@ public static bool IsAccessible(IDirectivesProvider type) return !type.Directives.ContainsName(WellKnownDirectiveNames.Inaccessible); } - public static bool SameOutputTypeShape(ITypeDefinition typeA, ITypeDefinition typeB) + public static bool SameTypeShape(ITypeDefinition typeA, ITypeDefinition typeB) { - var nullableTypeA = typeA.NullableType(); - var nullableTypeB = typeB.NullableType(); - - if (nullableTypeA.Kind != nullableTypeB.Kind) - { - // Different type kind. - return false; - } - - if (nullableTypeA is INamedTypeDefinition namedNullableTypeA - && nullableTypeB is INamedTypeDefinition namedNullableTypeB - && namedNullableTypeA.Name != namedNullableTypeB.Name) - { - // Different type name. - return false; - } - while (true) { - var innerNullableTypeA = nullableTypeA.InnerNullableType(); - var innerNullableTypeB = nullableTypeB.InnerNullableType(); + if (typeA is NonNullTypeDefinition && typeB is not NonNullTypeDefinition) + { + typeA = typeA.InnerType(); - // Note: InnerNullableType returns the type itself when there is no inner type. - // "If type A has an inner type but type B does not" (or vice versa). - if ((innerNullableTypeA != nullableTypeA && innerNullableTypeB == nullableTypeB) - || (innerNullableTypeB != nullableTypeB && innerNullableTypeA == nullableTypeA)) + continue; + } + + if (typeB is NonNullTypeDefinition && typeA is not NonNullTypeDefinition) { - // Different type depth. - return false; + typeB = typeB.InnerType(); + + continue; } - if (innerNullableTypeA == nullableTypeA) + if (typeA is ListTypeDefinition || typeB is ListTypeDefinition) { - // No more inner types. - break; + if (typeA is not ListTypeDefinition || typeB is not ListTypeDefinition) + { + return false; + } + + typeA = typeA.InnerType(); + typeB = typeB.InnerType(); + + continue; } - if (innerNullableTypeA.Kind != innerNullableTypeB.Kind) + if (typeA.Kind != typeB.Kind) { - // Different type kind on inner type. return false; } - if (innerNullableTypeA is INamedTypeDefinition namedNullableInnerTypeA - && innerNullableTypeB is INamedTypeDefinition namedNullableInnerTypeB - && namedNullableInnerTypeA.Name != namedNullableInnerTypeB.Name) + if (typeA.NamedType().Name != typeB.NamedType().Name) { - // Different type name on inner type. return false; } - nullableTypeA = innerNullableTypeA; - nullableTypeB = innerNullableTypeB; + return true; } - - return true; } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/ValidationHelperTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/ValidationHelperTests.cs index 6f3f078e63f..26ee73906b3 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/ValidationHelperTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/ValidationHelperTests.cs @@ -17,7 +17,7 @@ public sealed class ValidationHelperTests [Arguments("[[Int]]", "[[Int!]]")] [Arguments("[[Int]]", "[[Int!]!]")] [Arguments("[[Int]]", "[[Int!]!]!")] - public async Task SameOutputTypeShape_True(string sdlTypeA, string sdlTypeB) + public async Task SameTypeShape_True(string sdlTypeA, string sdlTypeB) { // arrange var schema1 = SchemaParser.Parse($$"""type Test { field: {{sdlTypeA}} }"""); @@ -26,7 +26,7 @@ public async Task SameOutputTypeShape_True(string sdlTypeA, string sdlTypeB) var typeB = ((ObjectTypeDefinition)schema2.Types["Test"]).Fields["field"].Type; // act - var result = ValidationHelper.SameOutputTypeShape(typeA, typeB); + var result = ValidationHelper.SameTypeShape(typeA, typeB); // assert await Assert.That(result).IsTrue(); @@ -49,7 +49,7 @@ public async Task SameOutputTypeShape_True(string sdlTypeA, string sdlTypeB) // Different depth and nullability. [Arguments("String", "[String!]")] [Arguments("String", "[String!]!")] - public async Task SameOutputTypeShape_False(string sdlTypeA, string sdlTypeB) + public async Task SameTypeShape_False(string sdlTypeA, string sdlTypeB) { // arrange var schema1 = SchemaParser.Parse( @@ -70,7 +70,7 @@ scalar Tag var typeB = ((ObjectTypeDefinition)schema2.Types["Test"]).Fields["field"].Type; // act - var result = ValidationHelper.SameOutputTypeShape(typeA, typeB); + var result = ValidationHelper.SameTypeShape(typeA, typeB); // assert await Assert.That(result).IsFalse();