Skip to content

Commit 135ddde

Browse files
authored
IParseableInputObject with SG support (#1726)
1 parent f397fa8 commit 135ddde

File tree

39 files changed

+581
-124
lines changed

39 files changed

+581
-124
lines changed

samples/GraphQL.Samples.SG.Arguments/Program.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public static int[] RangeWithInputObject([FromArguments]QueryOptions options)
5050

5151

5252
[InputType]
53-
public class QueryOptions
53+
public partial class QueryOptions
5454
{
5555
public int Start { get; set; } = 0;
5656

samples/GraphQL.Samples.SG.InputType/GraphQL.Samples.SG.InputType.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10+
<ProjectReference Include="..\..\src\GraphQL.Extensions.Experimental\GraphQL.Extensions.Experimental.csproj" />
1011
<ProjectReference Include="..\..\src\GraphQL.Language\GraphQL.Language.csproj" />
1112
<ProjectReference Include="..\..\src\GraphQL.Server\GraphQL.Server.csproj" />
1213
<ProjectReference Include="..\..\src\GraphQL\GraphQL.csproj" />

samples/GraphQL.Samples.SG.InputType/Program.cs

+80-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System.Collections.Concurrent;
22
using Microsoft.AspNetCore.Mvc;
3+
4+
using Tanka.GraphQL;
5+
using Tanka.GraphQL.Extensions.Experimental.OneOf;
36
using Tanka.GraphQL.Server;
47

58
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
@@ -18,8 +21,20 @@
1821
// add types from current namespace
1922
types.AddGlobalTypes();
2023
});
24+
25+
options.PostConfigure(configure =>
26+
{
27+
// add oneOf input type
28+
configure.Builder.Schema.AddOneOf();
29+
configure.Builder.Add($$"""
30+
extend input {{nameof(OneOfInput)}} @oneOf
31+
""");
32+
});
2133
});
2234

35+
// add validation rule for @oneOf directive
36+
builder.Services.AddDefaultValidatorRule(OneOfDirective.OneOfValidationRule());
37+
2338
WebApplication app = builder.Build();
2439
app.UseWebSockets();
2540

@@ -68,6 +83,43 @@ public static Message Post([FromArguments]InputMessage input, [FromServices]Db d
6883
db.Messages.Add(message);
6984
return message;
7085
}
86+
87+
/// <summary>
88+
/// A command pattern like mutation with @oneOf input type
89+
/// </summary>
90+
/// <remarks>
91+
/// @oneOf - directive is provided by Tanka.GraphQL.Extensions.Experimental
92+
/// Spec PR: https://github.com/graphql/graphql-spec/pull/825
93+
/// </remarks>
94+
/// <param name="input"></param>
95+
/// <param name="db"></param>
96+
/// <returns></returns>
97+
public static Result? Execute([FromArguments] OneOfInput input, [FromServices] Db db)
98+
{
99+
if (input.Add is not null)
100+
{
101+
var message = new Message
102+
{
103+
Id = Guid.NewGuid().ToString(),
104+
Text = input.Add.Text
105+
};
106+
107+
db.Messages.Add(message);
108+
return new Result()
109+
{
110+
Id = message.Id
111+
};
112+
}
113+
114+
if (input.Remove is null)
115+
throw new ArgumentNullException(nameof(input.Remove), "This should not happen as the validation rule should ensure one of these are set");
116+
117+
db.Messages.RemoveAll(m => m.Id == input.Remove.Id);
118+
return new Result()
119+
{
120+
Id = input.Remove.Id
121+
};
122+
}
71123
}
72124

73125
[ObjectType]
@@ -78,13 +130,39 @@ public class Message
78130
public required string Text { get; set; }
79131
}
80132

133+
[ObjectType]
134+
public class Result
135+
{
136+
public string Id { get; set; }
137+
}
138+
81139
[InputType]
82-
public class InputMessage
140+
public partial class InputMessage
83141
{
84142
public string Text { get; set; } = string.Empty;
85143
}
86144

145+
[InputType]
146+
public partial class OneOfInput
147+
{
148+
public AddInput? Add { get; set; }
149+
150+
public RemoveInput? Remove { get; set; }
151+
}
152+
153+
[InputType]
154+
public partial class AddInput
155+
{
156+
public string Text { get; set; }
157+
}
158+
159+
[InputType]
160+
public partial class RemoveInput
161+
{
162+
public string Id { get; set; }
163+
}
164+
87165
public class Db
88166
{
89-
public ConcurrentBag<Message> Messages { get; } = new();
167+
public List<Message> Messages { get; } = new();
90168
}

samples/GraphQL.Samples.SG.Namespace/Program.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public class World
4242
}
4343

4444
[InputType]
45-
public class HelloInput
45+
public partial class HelloInput
4646
{
4747
public string Name { get; set; }
4848
}

src/GraphQL.Server.SourceGenerators/InputTypeEmitter.cs

+93-13
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
using Microsoft.CodeAnalysis;
1+
using System.Runtime.Serialization;
2+
3+
using Microsoft.CodeAnalysis;
24
using System.Text;
35
using System.Text.Json;
46

7+
using Microsoft.CodeAnalysis.CSharp;
8+
59
namespace Tanka.GraphQL.Server.SourceGenerators;
610

7-
public class InputTypeEmitter
11+
public class InputTypeEmitter(SourceProductionContext context)
812
{
9-
public const string ObjectTypeTemplate = """
13+
public const string InputObjectTypeTemplate =
14+
"""
15+
/// <auto-generated/>
16+
#nullable enable
1017
using System;
1118
using System.Threading.Tasks;
1219
using Microsoft.Extensions.Options;
@@ -31,28 +38,101 @@ public static class {{name}}InputTypeExtensions
3138
}
3239
}
3340
41+
{{parseableImplementation}}
42+
#nullable restore
43+
""";
44+
45+
public static string ParseMethodTemplate(string name, string parseMethod) =>
46+
$$"""
47+
public partial class {{name}}: IParseableInputObject
48+
{
49+
public void Parse(IReadOnlyDictionary<string, object?> argumentValue)
50+
{
51+
{{parseMethod}}
52+
}
53+
}
3454
""";
3555

36-
public SourceProductionContext Context { get; }
56+
public static string TrySetProperty(string fieldName, string name, string type) =>
57+
$$"""
58+
// {{name}} is an scalar type
59+
if (argumentValue.TryGetValue("{{fieldName}}", out var {{fieldName}}Value))
60+
{
61+
if ({{fieldName}}Value is null)
62+
{
63+
{{name}} = default;
64+
}
65+
else
66+
{
67+
{{name}} = ({{type}}){{fieldName}}Value;
68+
}
69+
}
70+
""";
3771

38-
public InputTypeEmitter(SourceProductionContext context)
39-
{
40-
Context = context;
41-
}
72+
public static string TrySetPropertyObjectValue(string fieldName, string name, string type) =>
73+
$$"""
74+
// {{name}} is an input object type
75+
if (argumentValue.TryGetValue("{{fieldName}}", out var {{fieldName}}Value))
76+
{
77+
if ({{fieldName}}Value is null)
78+
{
79+
{{name}} = default;
80+
}
81+
else
82+
{
83+
if ({{fieldName}}Value is not IReadOnlyDictionary<string, object?> dictionaryValue)
84+
throw new InvalidOperationException($"{{fieldName}} is not IReadOnlyDictionary<string, object?>");
85+
86+
{{name}} = new {{type}}();
87+
88+
if ({{name}} is not IParseableInputObject parseable)
89+
throw new InvalidOperationException($"{{name}} is not IParseableInputObject");
90+
91+
parseable.Parse(dictionaryValue);
92+
}
93+
}
94+
""";
95+
96+
public SourceProductionContext Context { get; } = context;
4297

4398
public void Emit(InputTypeDefinition definition)
4499
{
45-
var typeSDL = BuildTypeSdl(definition);
46-
100+
var typeSdl = BuildTypeSdl(definition);
47101
var builder = new StringBuilder();
48102
string ns = string.IsNullOrEmpty(definition.Namespace) ? "" : $"{definition.Namespace}";
49-
builder.AppendLine(ObjectTypeTemplate
103+
builder.AppendLine(InputObjectTypeTemplate
50104
.Replace("{{namespace}}", string.IsNullOrEmpty(ns) ? "" : $"namespace {ns};")
51105
.Replace("{{name}}", definition.TargetType)
52-
.Replace("{{typeSDL}}", typeSDL)
106+
.Replace("{{typeSDL}}", typeSdl)
107+
.Replace("{{parseableImplementation}}", BuildParseMethod(definition))
53108
);
54109

55-
Context.AddSource($"{ns}{definition.TargetType}InputType.g.cs", builder.ToString());
110+
var sourceText = CSharpSyntaxTree.ParseText(builder.ToString())
111+
.GetRoot()
112+
.NormalizeWhitespace()
113+
.ToFullString();
114+
115+
Context.AddSource($"{ns}{definition.TargetType}InputType.g.cs", sourceText);
116+
}
117+
118+
private string BuildParseMethod(InputTypeDefinition definition)
119+
{
120+
var builder = new IndentedStringBuilder();
121+
foreach (ObjectPropertyDefinition property in definition.Properties)
122+
{
123+
var typeName = property.ReturnType.Replace("?", "");
124+
var fieldName = JsonNamingPolicy.CamelCase.ConvertName(property.Name);
125+
if (property.ReturnTypeObject is not null)
126+
{
127+
builder.AppendLine(TrySetPropertyObjectValue(fieldName, property.Name, typeName));
128+
}
129+
else
130+
{
131+
builder.AppendLine(TrySetProperty(fieldName, property.Name, typeName));
132+
}
133+
}
134+
135+
return ParseMethodTemplate(definition.TargetType, builder.ToString());
56136
}
57137

58138
private string BuildTypeSdl(InputTypeDefinition definition)

src/GraphQL.Server.SourceGenerators/InputTypeParser.cs

+38-9
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,9 @@
88

99
namespace Tanka.GraphQL.Server.SourceGenerators;
1010

11-
public class InputTypeParser
11+
public class InputTypeParser(GeneratorAttributeSyntaxContext context)
1212
{
13-
public GeneratorAttributeSyntaxContext Context { get; }
14-
15-
public InputTypeParser(GeneratorAttributeSyntaxContext context)
16-
{
17-
Context = context;
18-
}
13+
public GeneratorAttributeSyntaxContext Context { get; } = context;
1914

2015
public InputTypeDefinition ParseInputTypeDefinition(ClassDeclarationSyntax classDeclaration)
2116
{
@@ -36,17 +31,18 @@ private List<ObjectPropertyDefinition> ParseMembers(ClassDeclarationSyntax clas
3631

3732
foreach (MemberDeclarationSyntax memberDeclarationSyntax in classDeclaration
3833
.Members
39-
.Where(m => CSharpExtensions.Any((SyntaxTokenList)m.Modifiers, SyntaxKind.PublicKeyword)))
34+
.Where(m => ((SyntaxTokenList)m.Modifiers).Any(SyntaxKind.PublicKeyword)))
4035
{
4136
if (memberDeclarationSyntax.IsKind(SyntaxKind.PropertyDeclaration))
4237
{
4338
var propertyDeclaration = (PropertyDeclarationSyntax)memberDeclarationSyntax;
39+
var typeSymbol = Context.SemanticModel.GetTypeInfo(propertyDeclaration.Type).Type;
4440
var propertyDefinition = new ObjectPropertyDefinition()
4541
{
4642
Name = propertyDeclaration.Identifier.Text,
4743
ReturnType = propertyDeclaration.Type.ToString(),
4844
ClosestMatchingGraphQLTypeName = GetClosestMatchingGraphQLTypeName(TypeHelper.UnwrapTaskType(propertyDeclaration.Type)),
49-
IsNullable = TypeHelper.IsTypeNullable(propertyDeclaration.Type),
45+
ReturnTypeObject = typeSymbol != null ? TryParseInputTypeDefinition(typeSymbol): null
5046
};
5147
properties.Add(propertyDefinition);
5248
}
@@ -55,6 +51,39 @@ private List<ObjectPropertyDefinition> ParseMembers(ClassDeclarationSyntax clas
5551
return properties;
5652
}
5753

54+
private InputTypeDefinition? TryParseInputTypeDefinition(ITypeSymbol namedTypeSymbol)
55+
{
56+
if (namedTypeSymbol.TypeKind != TypeKind.Class)
57+
return null;
58+
59+
if (namedTypeSymbol.SpecialType is not SpecialType.None)
60+
return null;
61+
62+
var properties = GetPublicProperties(namedTypeSymbol)
63+
.Select(property => new ObjectPropertyDefinition()
64+
{
65+
Name = property.Name,
66+
ReturnType = property.Type.ToString(),
67+
ClosestMatchingGraphQLTypeName = GetClosestMatchingGraphQLTypeName(property.Type),
68+
})
69+
.ToList();
70+
71+
return new InputTypeDefinition() { Properties = properties };
72+
73+
static IEnumerable<IPropertySymbol> GetPublicProperties(ITypeSymbol typeSymbol)
74+
{
75+
return typeSymbol.GetMembers()
76+
.Where(member => member.Kind == SymbolKind.Property)
77+
.Cast<IPropertySymbol>()
78+
.Where(property => property.DeclaredAccessibility == Accessibility.Public);
79+
}
80+
}
81+
82+
private string GetClosestMatchingGraphQLTypeName(ITypeSymbol typeSymbol)
83+
{
84+
var typeName = TypeHelper.GetGraphQLTypeName(typeSymbol);
85+
return typeName;
86+
}
5887

5988

6089
private string GetClosestMatchingGraphQLTypeName(TypeSyntax typeSyntax)

src/GraphQL.Server.SourceGenerators/ObjectPropertyDefinition.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public record ObjectPropertyDefinition
66

77
public string ReturnType { get; init; }
88

9-
public bool IsNullable { get; init; }
109
public string ClosestMatchingGraphQLTypeName { get; set; }
10+
11+
public InputTypeDefinition? ReturnTypeObject { get; set; }
1112
}

src/GraphQL.Server.SourceGenerators/ObjectTypeParser.cs

-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ private static (List<ObjectPropertyDefinition> Properties, List<ObjectMethodDefi
5454
Name = propertyDeclaration.Identifier.Text,
5555
ReturnType = propertyDeclaration.Type.ToString(),
5656
ClosestMatchingGraphQLTypeName = GetClosestMatchingGraphQLTypeName(context.SemanticModel, TypeHelper.UnwrapTaskType(propertyDeclaration.Type)),
57-
IsNullable = TypeHelper.IsTypeNullable(propertyDeclaration.Type),
5857
};
5958
properties.Add(propertyDefinition);
6059
}

src/GraphQL.Server/GraphQLHttpRequest.cs

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ public record GraphQLHttpRequest
1414
[JsonConverter(typeof(ExecutableDocumentConverter))]
1515
public ExecutableDocument Query { get; set; } = string.Empty;
1616

17+
[JsonPropertyName("initialValue")]
18+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
19+
public object? InitialValue { get; set; }
20+
1721
[JsonPropertyName("variables")]
1822
[JsonConverter(typeof(NestedDictionaryConverter))]
1923
public IReadOnlyDictionary<string, object?>? Variables { get; set; }

0 commit comments

Comments
 (0)