From 4352fc194532c98dd8b2bee3e9f29eb8da349567 Mon Sep 17 00:00:00 2001 From: Artur Gordashnikov Date: Sun, 29 Jan 2023 20:26:07 +0200 Subject: [PATCH 1/3] Applied directives --- .../Commands.cs | 3 +- .../GraphQlCSharpFileHelper.cs | 2 +- .../ProgramOptions.cs | 1 + .../GraphQlClientSourceGenerator.cs | 5 +- .../GraphQlGenerator.cs | 55 ++++++--- .../GraphQlIntrospectionSchema.cs | 21 ++++ .../IntrospectionQuery.cs | 111 +++++++++++++++++- 7 files changed, 178 insertions(+), 20 deletions(-) diff --git a/src/GraphQlClientGenerator.Console/Commands.cs b/src/GraphQlClientGenerator.Console/Commands.cs index a540702..3a3e49b 100644 --- a/src/GraphQlClientGenerator.Console/Commands.cs +++ b/src/GraphQlClientGenerator.Console/Commands.cs @@ -59,7 +59,8 @@ private static RootCommand SetupGenerateCommand() new Option("--enumValueNaming", () => EnumValueNamingOption.CSharp, "Use \"Original\" to avoid pretty C# name conversion for maximum deserialization compatibility"), new Option("--includeDeprecatedFields", () => false, "Includes deprecated fields in generated query builders and data classes"), new Option("--fileScopedNamespaces", () => false, "Specifies if file-scoped namespaces should be used in generated files (C# 10+)"), - regexScalarFieldTypeMappingConfigurationOption + regexScalarFieldTypeMappingConfigurationOption, + new Option("--includeAppliedDirectives", () => false, "Include applied directives in Introspection Query") }; command.TreatUnmatchedTokensAsErrors = true; diff --git a/src/GraphQlClientGenerator.Console/GraphQlCSharpFileHelper.cs b/src/GraphQlClientGenerator.Console/GraphQlCSharpFileHelper.cs index 706667f..1a165b2 100644 --- a/src/GraphQlClientGenerator.Console/GraphQlCSharpFileHelper.cs +++ b/src/GraphQlClientGenerator.Console/GraphQlCSharpFileHelper.cs @@ -39,7 +39,7 @@ private static async Task GenerateClientSourceCode(IConsole console, ProgramOpti if (!KeyValueParameterParser.TryGetCustomHeaders(options.Header, out var headers, out var headerParsingErrorMessage)) throw new InvalidOperationException(headerParsingErrorMessage); - schema = await GraphQlGenerator.RetrieveSchema(new HttpMethod(options.HttpMethod), options.ServiceUrl, headers); + schema = await GraphQlGenerator.RetrieveSchema(new HttpMethod(options.HttpMethod), options.ServiceUrl, options.IncludeAppliedDirectives, headers); console.Out.WriteLine($"GraphQL Schema retrieved from {options.ServiceUrl}. "); } diff --git a/src/GraphQlClientGenerator.Console/ProgramOptions.cs b/src/GraphQlClientGenerator.Console/ProgramOptions.cs index d26fc4c..72745b6 100644 --- a/src/GraphQlClientGenerator.Console/ProgramOptions.cs +++ b/src/GraphQlClientGenerator.Console/ProgramOptions.cs @@ -24,4 +24,5 @@ public class ProgramOptions public JsonPropertyGenerationOption JsonPropertyAttribute { get; set; } public EnumValueNamingOption EnumValueNaming { get; set; } public bool IncludeDeprecatedFields { get; set; } + public bool IncludeAppliedDirectives { get; set; } } \ No newline at end of file diff --git a/src/GraphQlClientGenerator/GraphQlClientSourceGenerator.cs b/src/GraphQlClientGenerator/GraphQlClientSourceGenerator.cs index b76de51..0545f67 100644 --- a/src/GraphQlClientGenerator/GraphQlClientSourceGenerator.cs +++ b/src/GraphQlClientGenerator/GraphQlClientSourceGenerator.cs @@ -225,7 +225,10 @@ public void Execute(GeneratorExecutionContext context) } else { - graphQlSchemas.Add((FileNameGraphQlClientSource, GraphQlGenerator.RetrieveSchema(new HttpMethod(httpMethod), serviceUrl, headers).GetAwaiter().GetResult())); + currentParameterName = "IncludeAppliedDirectives"; + context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + currentParameterName, out var includeAppliedDirectivesRaw); + var includeAppliedDirectives = !string.IsNullOrWhiteSpace(includeAppliedDirectivesRaw) && Convert.ToBoolean(includeAppliedDirectivesRaw); + graphQlSchemas.Add((FileNameGraphQlClientSource, GraphQlGenerator.RetrieveSchema(new HttpMethod(httpMethod), serviceUrl, includeAppliedDirectives, headers).GetAwaiter().GetResult())); context.ReportDiagnostic( Diagnostic.Create( DescriptorInfo, diff --git a/src/GraphQlClientGenerator/GraphQlGenerator.cs b/src/GraphQlClientGenerator/GraphQlGenerator.cs index 0e49406..c99a886 100644 --- a/src/GraphQlClientGenerator/GraphQlGenerator.cs +++ b/src/GraphQlClientGenerator/GraphQlGenerator.cs @@ -53,13 +53,14 @@ public class GraphQlGenerator public GraphQlGenerator(GraphQlGeneratorConfiguration configuration = null) => _configuration = configuration ?? new GraphQlGeneratorConfiguration(); - public static async Task RetrieveSchema(HttpMethod method, string url, IEnumerable> headers = null) + public static async Task RetrieveSchema(HttpMethod method, string url,bool includeAppliedDirectives, IEnumerable> headers = null) { StringContent requestContent = null; + var introspectionQueryText = IntrospectionQuery.Get(includeAppliedDirectives); if (method == HttpMethod.Get) - url += $"?&query={IntrospectionQuery.Text}"; + url += $"?&query={introspectionQueryText}"; else - requestContent = new StringContent(JsonConvert.SerializeObject(new { query = IntrospectionQuery.Text }), Encoding.UTF8, "application/json"); + requestContent = new StringContent(JsonConvert.SerializeObject(new { operationName = IntrospectionQuery.OperationName, query = introspectionQueryText }), Encoding.UTF8, "application/json"); using var request = new HttpRequestMessage(method, url) { Content = requestContent }; @@ -84,10 +85,17 @@ public static GraphQlSchema DeserializeGraphQlSchema(string content) { try { + var graphQlResult = JsonConvert.DeserializeObject(content, SerializerSettings); + var schema = - JsonConvert.DeserializeObject(content, SerializerSettings)?.Data?.Schema + graphQlResult?.Data?.Schema ?? JsonConvert.DeserializeObject(content, SerializerSettings)?.Schema; + if (graphQlResult != null && graphQlResult.Errors?.Any() == true) + { + throw new InvalidOperationException($"Errors from introspection query:{Environment.NewLine}{string.Join(Environment.NewLine, graphQlResult.Errors.Select(e => e.Message))}"); + } + if (schema is null) throw new ArgumentException("not a GraphQL schema", nameof(content)); @@ -191,7 +199,7 @@ private void ResolveNameCollisions(GenerationContext context) else continue; - var candidateClassName = NamingHelper.ToPascalCase(graphQlType.Name);; + var candidateClassName = NamingHelper.ToPascalCase(graphQlType.Name); var finalClassName = candidateClassName; var collisionIteration = 1; @@ -896,11 +904,11 @@ private ScalarFieldTypeDescription GetDataPropertyType(GenerationContext context var itemTypeName = GetCSharpClassName(context, unwrappedItemType.Name); var netItemType = - IsUnknownObjectScalar(baseType, member.Name, itemType) + IsUnknownObjectScalar(baseType, member.Name, itemType, member.AppliedDirectives) ? "object" : $"{(unwrappedItemType.Kind == GraphQlTypeKind.Interface ? "I" : null)}{_configuration.ClassPrefix}{itemTypeName}{_configuration.ClassSuffix}"; - var suggestedScalarNetType = ScalarToNetType(baseType, member.Name, itemType).NetTypeName.TrimEnd('?'); + var suggestedScalarNetType = ScalarToNetType(baseType, member.Name, itemType, member.AppliedDirectives).NetTypeName.TrimEnd('?'); if (!String.Equals(suggestedScalarNetType, "object") && !String.Equals(suggestedScalarNetType, "object?") && !suggestedScalarNetType.TrimEnd().EndsWith("System.Object") && !suggestedScalarNetType.TrimEnd().EndsWith("System.Object?")) netItemType = suggestedScalarNetType; @@ -927,7 +935,7 @@ private ScalarFieldTypeDescription GetScalarNetType(string scalarTypeName, Graph GraphQlTypeBase.GraphQlTypeScalarString => GetCustomScalarNetType(baseType, member.Type, member.Name), GraphQlTypeBase.GraphQlTypeScalarFloat => GetFloatNetType(baseType, member.Type, member.Name), GraphQlTypeBase.GraphQlTypeScalarBoolean => ConvertToTypeDescription(GetBooleanNetType(baseType, member.Type, member.Name)), - GraphQlTypeBase.GraphQlTypeScalarId => GetIdNetType(baseType, member.Type, member.Name), + GraphQlTypeBase.GraphQlTypeScalarId => GetIdNetType(baseType, member.Type, member.Name, member.AppliedDirectives), _ => GetCustomScalarNetType(baseType, member.Type, member.Name) }; @@ -959,16 +967,31 @@ private ScalarFieldTypeDescription GetIntegerNetType(GraphQlType baseType, Graph _ => throw new InvalidOperationException($"'{_configuration.IntegerTypeMapping}' not supported") }; - private ScalarFieldTypeDescription GetIdNetType(GraphQlType baseType, GraphQlTypeBase valueType, string valueName) => + private ScalarFieldTypeDescription GetIdNetType(GraphQlType baseType, GraphQlTypeBase valueType, string valueName, IEnumerable appliedDirectives) => _configuration.IdTypeMapping switch { IdTypeMapping.String => ConvertToTypeDescription(AddQuestionMarkIfNullableReferencesEnabled("string")), IdTypeMapping.Guid => ConvertToTypeDescription("Guid?"), IdTypeMapping.Object => ConvertToTypeDescription(AddQuestionMarkIfNullableReferencesEnabled("object")), - IdTypeMapping.Custom => _configuration.ScalarFieldTypeMappingProvider.GetCustomScalarFieldType(_configuration, baseType, valueType, valueName), + IdTypeMapping.Custom => GetIdNetTypeDescription(baseType, valueType, valueName,appliedDirectives), _ => throw new InvalidOperationException($"'{_configuration.IdTypeMapping}' not supported") }; + private ScalarFieldTypeDescription GetIdNetTypeDescription(GraphQlType baseType, GraphQlTypeBase valueType, string valueName, IEnumerable appliedDirectives) + { + var clrDirectiveValue = GetClrDirectiveValue(appliedDirectives); + + return clrDirectiveValue != null + ? ConvertToTypeDescription(clrDirectiveValue) + : _configuration.ScalarFieldTypeMappingProvider.GetCustomScalarFieldType(_configuration, baseType, valueType, valueName); + } + + private static string GetClrDirectiveValue(IEnumerable directives) + { + var clrDirective = directives?.SingleOrDefault(d => d.Name == "clrType"); + return clrDirective?.Args.FirstOrDefault(a => a.Name == "type")?.Value.Trim('"'); + } + private static InvalidOperationException ListItemTypeResolutionFailedException(string typeName, string fieldName) => FieldTypeResolutionFailedException(typeName, fieldName, "list item type was not resolved; nested collections too deep"); @@ -1033,7 +1056,7 @@ private void GenerateQueryBuilder(GenerationContext context, GraphQlType type, I var field = fields[i]; var fieldType = field.Type.UnwrapIfNonNull(); var isList = fieldType.Kind == GraphQlTypeKind.List; - var treatUnknownObjectAsComplex = IsUnknownObjectScalar(type, field.Name, fieldType) && !_configuration.TreatUnknownObjectAsScalar; + var treatUnknownObjectAsComplex = IsUnknownObjectScalar(type, field.Name, fieldType, field.AppliedDirectives) && !_configuration.TreatUnknownObjectAsScalar; var isComplex = isList || treatUnknownObjectAsComplex || IsComplexType(fieldType.Kind); writer.Write(fieldMetadataIndentation); @@ -1492,7 +1515,7 @@ private QueryBuilderParameterDefinition BuildMethodParameterDefinition(Generatio var argumentTypeDescription = unwrappedType.Kind == GraphQlTypeKind.Enum ? ConvertToTypeDescription($"{_configuration.ClassPrefix}{NamingHelper.ToPascalCase(unwrappedType.Name)}{_configuration.ClassSuffix}?") - : ScalarToNetType(baseType, argument.Name, argumentType); + : ScalarToNetType(baseType, argument.Name, argumentType, argument.AppliedDirectives); var argumentNetType = argumentTypeDescription.NetTypeName; @@ -1731,23 +1754,23 @@ private void GenerateCodeComments(TextWriter writer, string description, int ind } } - private bool IsUnknownObjectScalar(GraphQlType baseType, string valueName, GraphQlFieldType fieldType) + private bool IsUnknownObjectScalar(GraphQlType baseType, string valueName, GraphQlFieldType fieldType, ICollection appliedDirectives) { if (fieldType.UnwrapIfNonNull().Kind != GraphQlTypeKind.Scalar) return false; - var netType = ScalarToNetType(baseType, valueName, fieldType).NetTypeName; + var netType = ScalarToNetType(baseType, valueName, fieldType, appliedDirectives).NetTypeName; return netType == "object" || netType.TrimEnd().EndsWith("System.Object") || netType == "object?" || netType.TrimEnd().EndsWith("System.Object?"); } - private ScalarFieldTypeDescription ScalarToNetType(GraphQlType baseType, string valueName, GraphQlFieldType valueType) => + private ScalarFieldTypeDescription ScalarToNetType(GraphQlType baseType, string valueName, GraphQlFieldType valueType, ICollection appliedDirectives) => valueType.UnwrapIfNonNull().Name switch { GraphQlTypeBase.GraphQlTypeScalarInteger => GetIntegerNetType(baseType, valueType, valueName), GraphQlTypeBase.GraphQlTypeScalarString => GetCustomScalarNetType(baseType, valueType, valueName), GraphQlTypeBase.GraphQlTypeScalarFloat => GetFloatNetType(baseType, valueType, valueName), GraphQlTypeBase.GraphQlTypeScalarBoolean => ConvertToTypeDescription(GetBooleanNetType(baseType, valueType, valueName)), - GraphQlTypeBase.GraphQlTypeScalarId => GetIdNetType(baseType, valueType, valueName), + GraphQlTypeBase.GraphQlTypeScalarId => GetIdNetType(baseType, valueType, valueName, appliedDirectives), _ => GetCustomScalarNetType(baseType, valueType, valueName) }; diff --git a/src/GraphQlClientGenerator/GraphQlIntrospectionSchema.cs b/src/GraphQlClientGenerator/GraphQlIntrospectionSchema.cs index 902eb46..e71dd64 100644 --- a/src/GraphQlClientGenerator/GraphQlIntrospectionSchema.cs +++ b/src/GraphQlClientGenerator/GraphQlIntrospectionSchema.cs @@ -7,6 +7,12 @@ namespace GraphQlClientGenerator; public class GraphQlResult { public GraphQlData Data { get; set; } + public ICollection Errors { get; set; } +} + +public class GraphQlError +{ + public string Message { get; set; } } public class GraphQlData @@ -47,6 +53,7 @@ public class GraphQlType : GraphQlTypeBase public IList Interfaces { get; set; } public IList EnumValues { get; set; } public IList PossibleTypes { get; set; } + public ICollection AppliedDirectives { get; set; } internal bool IsBuiltIn => Name is not null && Name.StartsWith("__"); } @@ -55,6 +62,7 @@ public abstract class GraphQlValueBase { public string Name { get; set; } public string Description { get; set; } + public ICollection AppliedDirectives { get; set; } } [DebuggerDisplay(nameof(GraphQlEnumValue) + " (" + nameof(Name) + "={" + nameof(Name) + ",nq}; " + nameof(Description) + "={" + nameof(Description) + ",nq})")] @@ -111,6 +119,19 @@ public interface IGraphQlMember string Name { get; } string Description { get; } GraphQlFieldType Type { get; } + ICollection AppliedDirectives { get; } +} + +public class AppliedDirective +{ + public string Name { get; set; } + public ICollection Args { get; set; } +} + +public class DirectiveArgument +{ + public string Name { get; set; } + public string Value { get; set; } } public enum GraphQlDirectiveLocation diff --git a/src/GraphQlClientGenerator/IntrospectionQuery.cs b/src/GraphQlClientGenerator/IntrospectionQuery.cs index 9f30819..dc58f74 100644 --- a/src/GraphQlClientGenerator/IntrospectionQuery.cs +++ b/src/GraphQlClientGenerator/IntrospectionQuery.cs @@ -2,7 +2,12 @@ public static class IntrospectionQuery { - public const string Text = + public const string OperationName = "IntrospectionQuery"; + + public static string Get(bool includeAppliedDirectives) => + includeAppliedDirectives ? TextWithAppliedDirectives : Text; + + private const string Text = @"query IntrospectionQuery { __schema { queryType { name } @@ -90,4 +95,108 @@ fragment TypeRef on __Type { } } }"; + + private const string TextWithAppliedDirectives = + @"query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + locations + args { + ...InputValue + } + } + appliedDirectives{ + ...AppliedDirective + } + } + } + + fragment FullType on __Type { + kind + name + appliedDirectives{ + ...AppliedDirective + } + description + fields(includeDeprecated: true) { + name + description + appliedDirectives{ + ...AppliedDirective + } + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + + fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue + appliedDirectives{ + ...AppliedDirective + } + } + + fragment TypeRef on __Type { + kind + name + appliedDirectives{ + ...AppliedDirective + } + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + + fragment AppliedDirective on __AppliedDirective { + name + args { + name + value + } + }"; } \ No newline at end of file From a20341258c32ac1f8eafcd9e486f64ee9ba93fb4 Mon Sep 17 00:00:00 2001 From: Artur Gordashnikov Date: Sun, 29 Jan 2023 20:33:53 +0200 Subject: [PATCH 2/3] fix order --- src/GraphQlClientGenerator/GraphQlGenerator.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/GraphQlClientGenerator/GraphQlGenerator.cs b/src/GraphQlClientGenerator/GraphQlGenerator.cs index c99a886..da3b565 100644 --- a/src/GraphQlClientGenerator/GraphQlGenerator.cs +++ b/src/GraphQlClientGenerator/GraphQlGenerator.cs @@ -87,15 +87,15 @@ public static GraphQlSchema DeserializeGraphQlSchema(string content) { var graphQlResult = JsonConvert.DeserializeObject(content, SerializerSettings); - var schema = - graphQlResult?.Data?.Schema - ?? JsonConvert.DeserializeObject(content, SerializerSettings)?.Schema; - - if (graphQlResult != null && graphQlResult.Errors?.Any() == true) + if (graphQlResult?.Errors?.Any() == true) { throw new InvalidOperationException($"Errors from introspection query:{Environment.NewLine}{string.Join(Environment.NewLine, graphQlResult.Errors.Select(e => e.Message))}"); } + var schema = + graphQlResult?.Data?.Schema + ?? JsonConvert.DeserializeObject(content, SerializerSettings)?.Schema; + if (schema is null) throw new ArgumentException("not a GraphQL schema", nameof(content)); From 831d577d5872f02ba7bd2d278fcd274d868d2cc8 Mon Sep 17 00:00:00 2001 From: Artur Gordashnikov Date: Sun, 29 Jan 2023 20:38:22 +0200 Subject: [PATCH 3/3] cleanup --- src/GraphQlClientGenerator/GraphQlGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GraphQlClientGenerator/GraphQlGenerator.cs b/src/GraphQlClientGenerator/GraphQlGenerator.cs index da3b565..526fa87 100644 --- a/src/GraphQlClientGenerator/GraphQlGenerator.cs +++ b/src/GraphQlClientGenerator/GraphQlGenerator.cs @@ -973,7 +973,7 @@ private ScalarFieldTypeDescription GetIdNetType(GraphQlType baseType, GraphQlTyp IdTypeMapping.String => ConvertToTypeDescription(AddQuestionMarkIfNullableReferencesEnabled("string")), IdTypeMapping.Guid => ConvertToTypeDescription("Guid?"), IdTypeMapping.Object => ConvertToTypeDescription(AddQuestionMarkIfNullableReferencesEnabled("object")), - IdTypeMapping.Custom => GetIdNetTypeDescription(baseType, valueType, valueName,appliedDirectives), + IdTypeMapping.Custom => GetIdNetTypeDescription(baseType, valueType, valueName, appliedDirectives), _ => throw new InvalidOperationException($"'{_configuration.IdTypeMapping}' not supported") };