Skip to content

Commit

Permalink
Merge pull request #24 from rjrudman/master
Browse files Browse the repository at this point in the history
Allow code to be used as a nuget package
  • Loading branch information
lukemurray authored Nov 15, 2023
2 parents 0c775e7 + 47aa9bc commit 51eeed0
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 188 deletions.
15 changes: 15 additions & 0 deletions DotNetGraphQLQueryGen.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetGraphQLQueryGen.Tests
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-gqlgen", "src\dotnet-gqlgen\dotnet-gqlgen.csproj", "{8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-gqlgen-console", "src\dotnet-gqlgen-console\dotnet-gqlgen-console.csproj", "{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -48,10 +50,23 @@ Global
{8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|x64.Build.0 = Release|Any CPU
{8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|x86.ActiveCfg = Release|Any CPU
{8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|x86.Build.0 = Release|Any CPU
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Debug|x64.ActiveCfg = Debug|Any CPU
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Debug|x64.Build.0 = Debug|Any CPU
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Debug|x86.ActiveCfg = Debug|Any CPU
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Debug|x86.Build.0 = Debug|Any CPU
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Release|Any CPU.Build.0 = Release|Any CPU
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Release|x64.ActiveCfg = Release|Any CPU
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Release|x64.Build.0 = Release|Any CPU
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Release|x86.ActiveCfg = Release|Any CPU
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0112D762-344C-4545-9377-86D255E5AEAB} = {E65B3177-BF31-4B95-A594-06CF381474C7}
{FFEBA61F-81B5-44C3-8178-1723A7BDB8AC} = {0112D762-344C-4545-9377-86D255E5AEAB}
{8F67E2C5-6152-4DDD-BF6F-BDF8836F549C} = {E65B3177-BF31-4B95-A594-06CF381474C7}
{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A} = {E65B3177-BF31-4B95-A594-06CF381474C7}
EndGlobalSection
EndGlobal
59 changes: 59 additions & 0 deletions src/dotnet-gqlgen-console/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;

namespace dotnet_gqlgen
{
public class Program
{
[Argument(0, Description = "Path to the GraphQL schema file or a GraphQL introspection endpoint")]
[Required]
public string Source { get; }

[Option(LongName = "header", ShortName = "h", Description = "Headers to pass to GraphQL introspection endpoint. Use \"Authorization=Bearer eyJraWQ,X-API-Key=abc,...\"")]
public string HeaderValues { get; }

[Option(LongName = "namespace", ShortName = "n", Description = "Namespace to generate code under")]
public string Namespace { get; } = "Generated";

[Option(LongName = "client_class_name", ShortName = "c", Description = "Name for the client class")]
public string ClientClassName { get; } = "GraphQLClient";

[Option(LongName = "scalar_mapping", ShortName = "m", Description = "Map of custom schema scalar types to dotnet types. Use \"GqlType=DotNetClassName,ID=Guid,...\"")]
public string ScalarMapping { get; }

[Option(LongName = "output", ShortName = "o", Description = "Output directory")]
public string OutputDir { get; } = "output";

[Option(LongName = "usings", ShortName = "u", Description = "Extra using statements to add to generated code.")]
public string Usings { get; } = "";

[Option(LongName = "no_generated_timestamp", ShortName = "nt", Description = "Don't add 'Generated on abc from xyz' in generated files")]
public bool NoGeneratedTimestamp { get; }

public static Task<int> Main(string[] args) => CommandLineApplication.ExecuteAsync<Program>(args);

private async Task OnExecute()
{
try
{
await Generator.Generate(new()
{
Source = Source,
HeaderValues = HeaderValues,
Namespace = Namespace,
ClientClassName = ClientClassName,
ScalarMapping = ScalarMapping,
OutputDir = OutputDir,
Usings = Usings,
NoGeneratedTimestamp = NoGeneratedTimestamp
});
}
catch (Exception e)
{
Console.WriteLine("Error: " + e);
}
}
}
}
19 changes: 19 additions & 0 deletions src/dotnet-gqlgen-console/dotnet-gqlgen-console.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>dotnet_gqlgen</RootNamespace>
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.3" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="0.2.4" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\dotnet-gqlgen\dotnet-gqlgen.csproj" />
</ItemGroup>

</Project>
175 changes: 175 additions & 0 deletions src/dotnet-gqlgen/Generator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using RazorLight;

namespace dotnet_gqlgen
{
public class GeneratorOptions
{
public string Source { get; set; }
public string HeaderValues { get; set; }
public string Namespace { get; set; } = "Generated";
public string ClientClassName { get; set; } = "GraphQLClient";
public string ScalarMapping { get; set; }
public string OutputDir { get; set; } = "output";
public string Usings { get; set; } = "";
public bool NoGeneratedTimestamp { get; set; }
}

public static class Generator
{
public static async Task Generate(GeneratorOptions options)
{
if (string.IsNullOrWhiteSpace(options.Source)) throw new ArgumentException($"{nameof(options.Source)} is required");

var dotnetToGqlTypeMappings = new Dictionary<string, string>
{
{ "string", "String" },
{ "String", "String" },
{ "int", "Int!" },
{ "Int32", "Int!" },
{ "double", "Float!" },
{ "bool", "Boolean!" },
};

Uri uriResult;
bool isGraphQlEndpoint = Uri.TryCreate(options.Source, UriKind.Absolute, out uriResult)
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);

string schemaText = null;
bool isIntroSpectionFile = false;

if (isGraphQlEndpoint)
{
Console.WriteLine($"Loading from {options.Source}...");
using (var httpClient = new HttpClient())
{
foreach (var header in SplitMultiValueArgument(options.HeaderValues))
{
httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
}

Dictionary<string, string> request = new Dictionary<string, string>();
request["query"] = IntroSpectionQuery.Query;
request["operationName"] = "IntrospectionQuery";

var response = httpClient
.PostAsync(options.Source,
new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json")).GetAwaiter().GetResult();

schemaText = await response.Content.ReadAsStringAsync();
isIntroSpectionFile = true;
}
}
else
{
Console.WriteLine($"Loading {options.Source}...");
schemaText = await File.ReadAllTextAsync(options.Source);
isIntroSpectionFile = Path.GetExtension(options.Source).Equals(".json", StringComparison.OrdinalIgnoreCase);
}

var mappings = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(options.ScalarMapping))
{
SplitMultiValueArgument(options.ScalarMapping).ToList().ForEach(i =>
{
dotnetToGqlTypeMappings[i.Value] = i.Key;
mappings[i.Key] = i.Value;
});
}

// parse into AST
var typeInfo = !isIntroSpectionFile ? SchemaCompiler.Compile(schemaText, mappings) : IntrospectionCompiler.Compile(schemaText, mappings);

Console.WriteLine($"Generating types in namespace {options.Namespace}, outputting to {options.ClientClassName}.cs");

var rootType = typeof(Generator);

// pass the schema to the template
var engine = new RazorLightEngineBuilder()
.UseEmbeddedResourcesProject(rootType)
.UseMemoryCachingProvider()
.Build();

var allTypes = typeInfo.Types.Concat(typeInfo.Inputs).ToDictionary(k => k.Key, v => v.Value);

string resultTypes = await engine.CompileRenderAsync("resultTypes.cshtml", new
{
Namespace = options.Namespace,
SchemaFile = options.Source,
Types = allTypes,
Enums = typeInfo.Enums,
Mutation = typeInfo.Mutation,
CmdArgs = $"-n {options.Namespace} -c {options.ClientClassName} -m {options.ScalarMapping} -u {options.Usings.Replace("\n", "\\n")}",
Usings = options.Usings,
options.NoGeneratedTimestamp
});
Directory.CreateDirectory(options.OutputDir);
await File.WriteAllTextAsync($"{options.OutputDir}/GeneratedResultTypes.cs", resultTypes);

string queryTypes = await engine.CompileRenderAsync("queryTypes.cshtml", new
{
Namespace = options.Namespace,
SchemaFile = options.Source,
Types = allTypes,
Mutation = typeInfo.Mutation,
CmdArgs = $"-n {options.Namespace} -c {options.ClientClassName} -m {options.ScalarMapping} -u {options.Usings.Replace("\n", "\\n")}",
Usings = options.Usings,
options.NoGeneratedTimestamp
});
Directory.CreateDirectory(options.OutputDir);
await File.WriteAllTextAsync($"{options.OutputDir}/GeneratedQueryTypes.cs", queryTypes);

resultTypes = await engine.CompileRenderAsync("client.cshtml", new
{
Namespace = options.Namespace,
SchemaFile = options.Source,
Query = typeInfo.Query,
Mutation = typeInfo.Mutation,
ClientClassName = options.ClientClassName,
Mappings = dotnetToGqlTypeMappings,
CmdArgs = $"-n {options.Namespace} -c {options.ClientClassName} -m {options.ScalarMapping}",
options.NoGeneratedTimestamp
});
await File.WriteAllTextAsync($"{options.OutputDir}/{options.ClientClassName}.cs", resultTypes);

await WriteResourceToFile(rootType, "BaseGraphQLClient.cs", $"{options.OutputDir}/BaseGraphQLClient.cs");
await WriteResourceToFile(rootType, "GqlFieldNameAttribute.cs", $"{options.OutputDir}/GqlFieldNameAttribute.cs");

Console.WriteLine($"Done.");
}

private static async Task WriteResourceToFile(Type rootType, string resourceName, string outputLocation)
{
var assembly = rootType.GetTypeInfo().Assembly;
await using var fileStream = File.Open(outputLocation, FileMode.Create);
await using var resourceStream = assembly.GetManifestResourceStream($"{rootType.Namespace}.{resourceName}")!;
await resourceStream.CopyToAsync(fileStream);
}

/// <summary>
/// Splits an argument value like "value1=v1;value2=v2" into a dictionary.
/// </summary>
/// <remarks>Very simple splitter. Eg can't handle semi-colon's or equal signs in values</remarks>
private static Dictionary<string, string> SplitMultiValueArgument(string arg)
{
if (string.IsNullOrEmpty(arg))
{
return new Dictionary<string, string>();
}

return arg
.Split(';')
.Select(h => h.Split('='))
.Where(hs => hs.Length >= 2)
.ToDictionary(key => key[0], value => value[1]);
}
}
}
Loading

0 comments on commit 51eeed0

Please sign in to comment.