Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Applied directives #128

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/GraphQlClientGenerator.Console/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ private static RootCommand SetupGenerateCommand()
new Option<EnumValueNamingOption>("--enumValueNaming", () => EnumValueNamingOption.CSharp, "Use \"Original\" to avoid pretty C# name conversion for maximum deserialization compatibility"),
new Option<bool>("--includeDeprecatedFields", () => false, "Includes deprecated fields in generated query builders and data classes"),
new Option<bool>("--fileScopedNamespaces", () => false, "Specifies if file-scoped namespaces should be used in generated files (C# 10+)"),
regexScalarFieldTypeMappingConfigurationOption
regexScalarFieldTypeMappingConfigurationOption,
new Option<bool>("--includeAppliedDirectives", () => false, "Include applied directives in Introspection Query")
};

command.TreatUnmatchedTokensAsErrors = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}. ");
}

Expand Down
1 change: 1 addition & 0 deletions src/GraphQlClientGenerator.Console/ProgramOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
5 changes: 4 additions & 1 deletion src/GraphQlClientGenerator/GraphQlClientSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
55 changes: 39 additions & 16 deletions src/GraphQlClientGenerator/GraphQlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ public class GraphQlGenerator
public GraphQlGenerator(GraphQlGeneratorConfiguration configuration = null) =>
_configuration = configuration ?? new GraphQlGeneratorConfiguration();

public static async Task<GraphQlSchema> RetrieveSchema(HttpMethod method, string url, IEnumerable<KeyValuePair<string, string>> headers = null)
public static async Task<GraphQlSchema> RetrieveSchema(HttpMethod method, string url,bool includeAppliedDirectives, IEnumerable<KeyValuePair<string, string>> 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 };

Expand All @@ -84,8 +85,15 @@ public static GraphQlSchema DeserializeGraphQlSchema(string content)
{
try
{
var graphQlResult = JsonConvert.DeserializeObject<GraphQlResult>(content, SerializerSettings);

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 =
JsonConvert.DeserializeObject<GraphQlResult>(content, SerializerSettings)?.Data?.Schema
graphQlResult?.Data?.Schema
?? JsonConvert.DeserializeObject<GraphQlData>(content, SerializerSettings)?.Schema;

if (schema is null)
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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)
};

Expand Down Expand Up @@ -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<AppliedDirective> 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<AppliedDirective> appliedDirectives)
{
var clrDirectiveValue = GetClrDirectiveValue(appliedDirectives);

return clrDirectiveValue != null
? ConvertToTypeDescription(clrDirectiveValue)
: _configuration.ScalarFieldTypeMappingProvider.GetCustomScalarFieldType(_configuration, baseType, valueType, valueName);
}

private static string GetClrDirectiveValue(IEnumerable<AppliedDirective> 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");

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<AppliedDirective> 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<AppliedDirective> 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)
};

Expand Down
21 changes: 21 additions & 0 deletions src/GraphQlClientGenerator/GraphQlIntrospectionSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ namespace GraphQlClientGenerator;
public class GraphQlResult
{
public GraphQlData Data { get; set; }
public ICollection<GraphQlError> Errors { get; set; }
}

public class GraphQlError
{
public string Message { get; set; }
}

public class GraphQlData
Expand Down Expand Up @@ -47,6 +53,7 @@ public class GraphQlType : GraphQlTypeBase
public IList<GraphQlFieldType> Interfaces { get; set; }
public IList<GraphQlEnumValue> EnumValues { get; set; }
public IList<GraphQlFieldType> PossibleTypes { get; set; }
public ICollection<AppliedDirective> AppliedDirectives { get; set; }

internal bool IsBuiltIn => Name is not null && Name.StartsWith("__");
}
Expand All @@ -55,6 +62,7 @@ public abstract class GraphQlValueBase
{
public string Name { get; set; }
public string Description { get; set; }
public ICollection<AppliedDirective> AppliedDirectives { get; set; }
}

[DebuggerDisplay(nameof(GraphQlEnumValue) + " (" + nameof(Name) + "={" + nameof(Name) + ",nq}; " + nameof(Description) + "={" + nameof(Description) + ",nq})")]
Expand Down Expand Up @@ -111,6 +119,19 @@ public interface IGraphQlMember
string Name { get; }
string Description { get; }
GraphQlFieldType Type { get; }
ICollection<AppliedDirective> AppliedDirectives { get; }
}

public class AppliedDirective
{
public string Name { get; set; }
public ICollection<DirectiveArgument> Args { get; set; }
}

public class DirectiveArgument
{
public string Name { get; set; }
public string Value { get; set; }
}

public enum GraphQlDirectiveLocation
Expand Down
111 changes: 110 additions & 1 deletion src/GraphQlClientGenerator/IntrospectionQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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
}
}";
}