Skip to content

Commit 6f05fc0

Browse files
committed
Source generator
1 parent 84b514d commit 6f05fc0

File tree

7 files changed

+268
-61
lines changed

7 files changed

+268
-61
lines changed

.editorconfig

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ root = true
44
[*.cs]
55
indent_style = space
66
indent_size = 4
7+
max_line_length = 220
8+
trim_trailing_whitespace = true

src/GraphQlClientGenerator.Console/GraphQlCSharpFileHelper.cs

+20-42
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,19 @@ public static async Task<IReadOnlyCollection<FileInfo>> GenerateClientSourceCode
2222
Environment.Exit(5);
2323
}
2424

25-
var schema =
26-
isServiceUrlMissing
27-
? GraphQlGenerator.DeserializeGraphQlSchema(await File.ReadAllTextAsync(options.SchemaFileName))
28-
: await GraphQlGenerator.RetrieveSchema(options.ServiceUrl, GetCustomHeaders(options.Headers));
25+
GraphQlSchema schema;
26+
if (isServiceUrlMissing)
27+
schema = GraphQlGenerator.DeserializeGraphQlSchema(await File.ReadAllTextAsync(options.SchemaFileName));
28+
else
29+
{
30+
if (!KeyValueParameterParser.TryGetCustomHeaders(options.Headers, out var headers, out var headerParsingErrorMessage))
31+
{
32+
System.Console.WriteLine("ERROR: " + headerParsingErrorMessage);
33+
Environment.Exit(3);
34+
}
35+
36+
schema = await GraphQlGenerator.RetrieveSchema(options.ServiceUrl, headers);
37+
}
2938

3039
var generatorConfiguration =
3140
new GraphQlGeneratorConfiguration
@@ -40,7 +49,13 @@ public static async Task<IReadOnlyCollection<FileInfo>> GenerateClientSourceCode
4049
JsonPropertyGeneration = options.JsonPropertyAttribute
4150
};
4251

43-
foreach (var kvp in GetCustomClassMapping(options.ClassMapping))
52+
if (!KeyValueParameterParser.TryGetCustomClassMapping(options.ClassMapping, out var customMapping, out var customMappingParsingErrorMessage))
53+
{
54+
System.Console.WriteLine("ERROR: " + customMappingParsingErrorMessage);
55+
Environment.Exit(3);
56+
}
57+
58+
foreach (var kvp in customMapping)
4459
generatorConfiguration.CustomClassNameMapping.Add(kvp);
4560

4661
var generator = new GraphQlGenerator(generatorConfiguration);
@@ -55,42 +70,5 @@ public static async Task<IReadOnlyCollection<FileInfo>> GenerateClientSourceCode
5570
generator.Generate(multipleFileGenerationContext);
5671
return multipleFileGenerationContext.Files;
5772
}
58-
59-
private static IEnumerable<KeyValuePair<string, string>> GetCustomClassMapping(IEnumerable<string> sourceParameters)
60-
{
61-
foreach (var parameter in sourceParameters)
62-
{
63-
var parts = parameter.Split(':');
64-
if (parts.Length != 2)
65-
{
66-
System.Console.WriteLine("ERROR: \"classMapping\" value must have format {GraphQlTypeName}:{C#ClassName}. ");
67-
Environment.Exit(3);
68-
}
69-
70-
var cSharpClassName = parts[1];
71-
if (!CSharpHelper.IsValidIdentifier(cSharpClassName))
72-
{
73-
System.Console.WriteLine($"ERROR: \"{cSharpClassName}\" is not valid C# class name. ");
74-
Environment.Exit(3);
75-
}
76-
77-
yield return new KeyValuePair<string, string>(parts[0], cSharpClassName);
78-
}
79-
}
80-
81-
private static IEnumerable<KeyValuePair<string, string>> GetCustomHeaders(IEnumerable<string> sourceParameters)
82-
{
83-
foreach (var parameter in sourceParameters)
84-
{
85-
var parts = parameter.Split(':', 2);
86-
if (parts.Length != 2)
87-
{
88-
System.Console.WriteLine("ERROR: \"headers\" value must have format {Header}:{Value};. ");
89-
Environment.Exit(3);
90-
}
91-
92-
yield return new KeyValuePair<string, string>(parts[0], parts[1]);
93-
}
94-
}
9573
}
9674
}

src/GraphQlClientGenerator/GraphQlClientGenerator.csproj

+5-4
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
<Description>A simple strongly typed C# GraphQL client generator library</Description>
66
<GenerateDocumentationFile>true</GenerateDocumentationFile>
77
<NoWarn>$(NoWarn);1591</NoWarn>
8-
<TargetFrameworks>netstandard1.3;net452</TargetFrameworks>
9-
<LangVersion>latest</LangVersion>
8+
<TargetFrameworks>netstandard2.0</TargetFrameworks>
109
<IncludeSymbols>true</IncludeSymbols>
1110
<PublishRepositoryUrl>true</PublishRepositoryUrl>
12-
<LangVersion>latest</LangVersion>
11+
<LangVersion>preview</LangVersion>
1312
<Copyright>Copyright 2017-2020</Copyright>
1413
<Authors>Husqvik</Authors>
1514
<Company>Tibber</Company>
@@ -23,11 +22,13 @@
2322
<RepositoryType>git</RepositoryType>
2423
<PackageReleaseNotes></PackageReleaseNotes>
2524
<PackageTags>GraphQL Client Generator</PackageTags>
26-
<PackageReleaseNotes>Support for class prefix</PackageReleaseNotes>
25+
<PackageReleaseNotes>Source generator</PackageReleaseNotes>
2726
</PropertyGroup>
2827

2928
<ItemGroup>
3029
<PackageReference Include="System.Net.Http" Version="4.*" />
30+
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="3.*" />
31+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.*" />
3132
<PackageReference Include="Newtonsoft.Json" Version="12.*" />
3233
</ItemGroup>
3334

src/GraphQlClientGenerator/GraphQlGenerator.cs

+19-14
Original file line numberDiff line numberDiff line change
@@ -106,24 +106,29 @@ public static GraphQlSchema DeserializeGraphQlSchema(string content)
106106
}
107107

108108
public string GenerateFullClientCSharpFile(GraphQlSchema schema, string @namespace)
109+
{
110+
var builder = new StringBuilder();
111+
using var writer = new StringWriter(builder);
112+
WriteFullClientCSharpFile(schema, @namespace, writer);
113+
return builder.ToString();
114+
}
115+
116+
public void WriteFullClientCSharpFile(GraphQlSchema schema, string @namespace, TextWriter writer)
109117
{
110118
if (String.IsNullOrWhiteSpace(@namespace))
111119
throw new ArgumentException("namespace required", nameof(@namespace));
112-
113-
var builder = new StringBuilder();
114-
builder.AppendLine(AutoGeneratedPrefix);
115-
builder.AppendLine();
116-
builder.AppendLine(RequiredNamespaces);
117-
builder.Append("namespace ");
118-
builder.AppendLine(@namespace);
119-
builder.AppendLine("{");
120-
121-
using (var writer = new StringWriter(builder))
122-
Generate(new SingleFileGenerationContext(schema, writer, indentationSize: 4));
123-
124-
builder.AppendLine("}");
125120

126-
return builder.ToString();
121+
writer.WriteLine(AutoGeneratedPrefix);
122+
writer.WriteLine();
123+
writer.WriteLine(RequiredNamespaces);
124+
writer.Write("namespace ");
125+
writer.WriteLine(@namespace);
126+
writer.WriteLine("{");
127+
128+
Generate(new SingleFileGenerationContext(schema, writer, indentationSize: 4));
129+
130+
writer.WriteLine("}");
131+
writer.Flush();
127132
}
128133

129134
public string Generate(GraphQlSchema schema)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
4+
namespace GraphQlClientGenerator
5+
{
6+
public static class KeyValueParameterParser
7+
{
8+
public static bool TryGetCustomClassMapping(IEnumerable<string> sourceParameters, out ICollection<KeyValuePair<string, string>> customMapping, out string errorMessage)
9+
{
10+
customMapping = new List<KeyValuePair<string, string>>();
11+
12+
foreach (var parameter in sourceParameters ?? Enumerable.Empty<string>())
13+
{
14+
var parts = parameter.Split(':');
15+
if (parts.Length != 2)
16+
{
17+
errorMessage = "\"classMapping\" value must have format {GraphQlTypeName}:{C#ClassName}. ";
18+
return false;
19+
}
20+
21+
var cSharpClassName = parts[1];
22+
if (!CSharpHelper.IsValidIdentifier(cSharpClassName))
23+
{
24+
errorMessage = $"\"{cSharpClassName}\" is not valid C# class name. ";
25+
return false;
26+
}
27+
28+
customMapping.Add(new KeyValuePair<string, string>(parts[0], cSharpClassName));
29+
}
30+
31+
errorMessage = null;
32+
return true;
33+
}
34+
35+
public static bool TryGetCustomHeaders(IEnumerable<string> sourceParameters, out ICollection<KeyValuePair<string, string>> headers, out string errorMessage)
36+
{
37+
headers = new List<KeyValuePair<string, string>>();
38+
39+
foreach (var parameter in sourceParameters ?? Enumerable.Empty<string>())
40+
{
41+
var parts = parameter.Split(new[] { ':' }, 2);
42+
if (parts.Length != 2)
43+
{
44+
errorMessage = "\"headers\" value must have format {Header}:{Value}. ";
45+
return false;
46+
}
47+
48+
headers.Add(new KeyValuePair<string, string>(parts[0], parts[1]));
49+
}
50+
51+
errorMessage = null;
52+
return true;
53+
}
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
using System;
2+
using System.IO;
3+
using System.Text;
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp;
6+
using Microsoft.CodeAnalysis.Text;
7+
8+
namespace GraphQlClientGenerator
9+
{
10+
[Generator]
11+
public class GraphQlSourceGenerator : ISourceGenerator
12+
{
13+
private static readonly DiagnosticDescriptor DescriptorError =
14+
new DiagnosticDescriptor(
15+
"GRAPHQLGEN1000",
16+
"GraphQlClientGenerator error",
17+
"GraphQlClientGenerator error: {0}",
18+
"GraphQlClientGenerator",
19+
DiagnosticSeverity.Error,
20+
true);
21+
22+
public void Initialize(GeneratorInitializationContext context)
23+
{
24+
}
25+
26+
public void Execute(GeneratorExecutionContext context)
27+
{
28+
var compilation = context.Compilation as CSharpCompilation;
29+
if (compilation == null)
30+
{
31+
context.ReportDiagnostic(
32+
Diagnostic.Create(
33+
DescriptorError,
34+
Location.None,
35+
DiagnosticSeverity.Error,
36+
"incompatible language: " + context.Compilation.Language));
37+
38+
return;
39+
}
40+
41+
try
42+
{
43+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_ServiceUrl", out var serviceUrl);
44+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_SchemaFileName", out var schemaFileName);
45+
var isServiceUrlMissing = String.IsNullOrWhiteSpace(serviceUrl);
46+
if (isServiceUrlMissing && String.IsNullOrWhiteSpace(schemaFileName))
47+
{
48+
context.ReportDiagnostic(
49+
Diagnostic.Create(
50+
DescriptorError,
51+
Location.None,
52+
DiagnosticSeverity.Error,
53+
"Either \"GraphQlClientGenerator_ServiceUrl\" or \"GraphQlClientGenerator_SchemaFileName\" parameter must be specified. "));
54+
55+
return;
56+
}
57+
58+
if (!isServiceUrlMissing && !String.IsNullOrWhiteSpace(schemaFileName))
59+
{
60+
context.ReportDiagnostic(
61+
Diagnostic.Create(
62+
DescriptorError,
63+
Location.None,
64+
DiagnosticSeverity.Error,
65+
"\"GraphQlClientGenerator_ServiceUrl\" and \"GraphQlClientGenerator_SchemaFileName\" parameters are mutually exclusive. "));
66+
67+
return;
68+
}
69+
70+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_Namespace", out var @namespace);
71+
if (String.IsNullOrWhiteSpace(@namespace))
72+
{
73+
context.ReportDiagnostic(
74+
Diagnostic.Create(
75+
DescriptorError,
76+
Location.None,
77+
DiagnosticSeverity.Error,
78+
"\"GraphQlClientGenerator_Namespace\" invalid"));
79+
80+
return;
81+
}
82+
83+
var configuration = new GraphQlGeneratorConfiguration { TreatUnknownObjectAsScalar = true };
84+
85+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_ClassPrefix", out var classPrefix);
86+
configuration.ClassPrefix = classPrefix;
87+
88+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_ClassSuffix", out var classSuffix);
89+
configuration.ClassSuffix = classSuffix;
90+
91+
if (compilation.LanguageVersion >= LanguageVersion.CSharp6)
92+
configuration.CSharpVersion = compilation.Options.NullableContextOptions == NullableContextOptions.Disable ? CSharpVersion.Newest : CSharpVersion.NewestWithNullableReferences;
93+
94+
var currentParameterName = "GeneratePartialClasses";
95+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_" + currentParameterName, out var generatePartialClassesRaw);
96+
configuration.GeneratePartialClasses = !String.IsNullOrWhiteSpace(generatePartialClassesRaw) && Convert.ToBoolean(generatePartialClassesRaw);
97+
98+
currentParameterName = "IncludeDeprecatedFields";
99+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_" + currentParameterName, out var includeDeprecatedFieldsRaw);
100+
configuration.IncludeDeprecatedFields = !String.IsNullOrWhiteSpace(includeDeprecatedFieldsRaw) && Convert.ToBoolean(includeDeprecatedFieldsRaw);
101+
102+
currentParameterName = "CommentGeneration";
103+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_" + currentParameterName, out var commentGenerationRaw);
104+
configuration.CommentGeneration =
105+
String.IsNullOrWhiteSpace(commentGenerationRaw)
106+
? CommentGenerationOption.CodeSummary
107+
: (CommentGenerationOption)Enum.Parse(typeof(CommentGenerationOption), commentGenerationRaw, true);
108+
109+
currentParameterName = "FloatTypeMapping";
110+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_" + currentParameterName, out var floatTypeMappingRaw);
111+
configuration.FloatTypeMapping = String.IsNullOrWhiteSpace(floatTypeMappingRaw) ? FloatTypeMapping.Decimal : (FloatTypeMapping)Enum.Parse(typeof(FloatTypeMapping), floatTypeMappingRaw, true);
112+
113+
currentParameterName = "BooleanTypeMapping";
114+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_" + currentParameterName, out var booleanTypeMappingRaw);
115+
configuration.BooleanTypeMapping = String.IsNullOrWhiteSpace(booleanTypeMappingRaw) ? BooleanTypeMapping.Boolean : (BooleanTypeMapping)Enum.Parse(typeof(BooleanTypeMapping), booleanTypeMappingRaw, true);
116+
117+
currentParameterName = "IdTypeMapping";
118+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_" + currentParameterName, out var idTypeMappingRaw);
119+
configuration.IdTypeMapping = String.IsNullOrWhiteSpace(idTypeMappingRaw) ? IdTypeMapping.Guid : (IdTypeMapping)Enum.Parse(typeof(IdTypeMapping), idTypeMappingRaw, true);
120+
121+
currentParameterName = "JsonPropertyGeneration";
122+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_" + currentParameterName, out var jsonPropertyGenerationRaw);
123+
configuration.JsonPropertyGeneration =
124+
String.IsNullOrWhiteSpace(jsonPropertyGenerationRaw)
125+
? JsonPropertyGenerationOption.CaseInsensitive
126+
: (JsonPropertyGenerationOption)Enum.Parse(typeof(JsonPropertyGenerationOption), jsonPropertyGenerationRaw, true);
127+
128+
currentParameterName = "CustomClassMapping";
129+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_" + currentParameterName, out var customClassMappingRaw);
130+
if (!KeyValueParameterParser.TryGetCustomClassMapping(
131+
customClassMappingRaw?.Split(new[] { '|', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries),
132+
out var customMapping,
133+
out var customMappingParsingErrorMessage))
134+
{
135+
context.ReportDiagnostic(Diagnostic.Create(DescriptorError, Location.None, DiagnosticSeverity.Error, customMappingParsingErrorMessage));
136+
return;
137+
}
138+
139+
foreach (var kvp in customMapping)
140+
configuration.CustomClassNameMapping.Add(kvp);
141+
142+
currentParameterName = "Headers";
143+
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQlClientGenerator_" + currentParameterName, out var headersRaw);
144+
if (!KeyValueParameterParser.TryGetCustomHeaders(
145+
headersRaw?.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries),
146+
out var headers,
147+
out var headerParsingErrorMessage))
148+
{
149+
context.ReportDiagnostic(Diagnostic.Create(DescriptorError, Location.None, DiagnosticSeverity.Error, headerParsingErrorMessage));
150+
return;
151+
}
152+
153+
var schema = GraphQlGenerator.RetrieveSchema(serviceUrl, headers).GetAwaiter().GetResult();
154+
var generator = new GraphQlGenerator(configuration);
155+
var builder = new StringBuilder();
156+
using (var writer = new StringWriter(builder))
157+
generator.WriteFullClientCSharpFile(schema, @namespace, writer);
158+
159+
context.AddSource("GraphQlClient.cs", SourceText.From(builder.ToString(), Encoding.UTF8));
160+
}
161+
catch (Exception exception)
162+
{
163+
context.ReportDiagnostic(Diagnostic.Create(DescriptorError, Location.None, DiagnosticSeverity.Error, exception.Message));
164+
}
165+
}
166+
}
167+
}

test/GraphQlClientGenerator.Test/GraphQlClientGenerator.Test.csproj

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
<ItemGroup>
1010
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.*" />
11-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.*" />
1211
<PackageReference Include="Shouldly" Version="4.*" />
1312
<PackageReference Include="xunit" Version="2.*" />
1413
<PackageReference Include="xunit.runner.visualstudio" Version="2.*">

0 commit comments

Comments
 (0)