private static async Task GenerateClientSourceCode(ProgramOptions options, List <FileInfo> generatedFiles) { GraphQlSchema schema; if (String.IsNullOrWhiteSpace(options.ServiceUrl)) { schema = GraphQlGenerator.DeserializeGraphQlSchema(await File.ReadAllTextAsync(options.SchemaFileName)); } else { 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); } var generatorConfiguration = new GraphQlGeneratorConfiguration { CSharpVersion = options.CSharpVersion, ClassPrefix = options.ClassPrefix, ClassSuffix = options.ClassSuffix, GeneratePartialClasses = options.PartialClasses, MemberAccessibility = options.MemberAccessibility, IdTypeMapping = options.IdTypeMapping, FloatTypeMapping = options.FloatTypeMapping, JsonPropertyGeneration = options.JsonPropertyAttribute }; if (!KeyValueParameterParser.TryGetCustomClassMapping(options.ClassMapping, out var customMapping, out var customMappingParsingErrorMessage)) { throw new InvalidOperationException(customMappingParsingErrorMessage); } foreach (var kvp in customMapping) { generatorConfiguration.CustomClassNameMapping.Add(kvp); } var generator = new GraphQlGenerator(generatorConfiguration); if (options.OutputType == OutputType.SingleFile) { await File.WriteAllTextAsync(options.OutputPath, generator.GenerateFullClientCSharpFile(schema, options.Namespace)); generatedFiles.Add(new FileInfo(options.OutputPath)); } else { var multipleFileGenerationContext = new MultipleFileGenerationContext(schema, options.OutputPath, options.Namespace); generator.Generate(multipleFileGenerationContext); generatedFiles.AddRange(multipleFileGenerationContext.Files); } }
private static RootCommand SetupGenerateCommand() { var serviceUrlOption = new Option <string>(new[] { "--serviceUrl", "-u" }, "GraphQL service URL used for retrieving schema metadata"); var schemaFileOption = new Option <string>(new[] { "--schemaFileName", "-s" }, "Path to schema metadata file in JSON format"); var classMappingOption = new Option <string[]>( "--classMapping", "Format: {GraphQlTypeName}:{C#ClassName}; allows to define custom class names for specific GraphQL types; common reason for this is to avoid property of the same name as its parent class"); classMappingOption.AddValidator(option => option.ErrorMessage = KeyValueParameterParser.TryGetCustomClassMapping(option.Tokens.Select(t => t.Value), out _, out var errorMessage) ? null : errorMessage); var headerOption = new Option <string[]>("--header", "Format: {Header}:{Value}; allows to enter custom headers required to fetch GraphQL metadata"); headerOption.AddValidator(option => option.ErrorMessage = KeyValueParameterParser.TryGetCustomHeaders(option.Tokens.Select(t => t.Value), out _, out var errorMessage) ? null : errorMessage); var command = new RootCommand { new Option <string>(new[] { "--outputPath", "-o" }, "Output path; include file name for single file output type; folder name for one class per file output type") { IsRequired = true }, new Option <string>(new[] { "--namespace", "-n" }, "Root namespace all classes and other members are generated to") { IsRequired = true }, serviceUrlOption, schemaFileOption, new Option <string>("--httpMethod", () => "POST", "GraphQL schema metadata retrieval HTTP method"), headerOption, new Option <string>("--classPrefix", "Class prefix; value \"Test\" extends class name to \"TestTypeName\""), new Option <string>("--classSuffix", "Class suffix, for instance for version control; value \"V2\" extends class name to \"TypeNameV2\""), new Option <CSharpVersion>("--csharpVersion", () => CSharpVersion.Compatible, "C# version compatibility"), new Option <MemberAccessibility>("--memberAccessibility", () => MemberAccessibility.Public, "Class and interface access level"), new Option <OutputType>("--outputType", () => OutputType.SingleFile, "Specifies generated classes organization"), new Option <bool>("--partialClasses", () => false, "Mark classes as \"partial\""), classMappingOption, new Option <IdTypeMapping>("--idTypeMapping", () => IdTypeMapping.Guid, "Specifies the .NET type generated for GraphQL ID data type"), new Option <FloatTypeMapping>("--floatTypeMapping", () => FloatTypeMapping.Decimal, "Specifies the .NET type generated for GraphQL Float data type"), new Option <JsonPropertyGenerationOption>("--jsonPropertyAttribute", () => JsonPropertyGenerationOption.CaseInsensitive, "Specifies the condition for using \"JsonPropertyAttribute\""), new Option <EnumValueNamingOption>("--enumValueNaming", "Use \"Original\" to avoid pretty C# name conversion for maximum deserialization compatibility") }; command.TreatUnmatchedTokensAsErrors = true; command.Name = "GraphQlClientGenerator.Console"; command.Description = "A tool for generating strongly typed GraphQL query builders and data classes"; command.Handler = CommandHandler.Create <IConsole, ProgramOptions>(GraphQlCSharpFileHelper.GenerateGraphQlClientSourceCode); command.AddValidator(option => option.ErrorMessage = option.FindResultFor(serviceUrlOption) is not null && option.FindResultFor(schemaFileOption) is not null ? "\"serviceUrl\" and \"schemaFileName\" parameters are mutually exclusive. " : null); command.AddValidator(option => option.ErrorMessage = option.FindResultFor(serviceUrlOption) is null && option.FindResultFor(schemaFileOption) is null ? "Either \"serviceUrl\" or \"schemaFileName\" parameter must be specified. " : null); return(command); }
public void Execute(GeneratorExecutionContext context) { if (context.Compilation is not CSharpCompilation compilation) { context.ReportDiagnostic( Diagnostic.Create( DescriptorParameterError, Location.None, "incompatible language: " + context.Compilation.Language)); return; } try { context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + "ServiceUrl", out var serviceUrl); var isServiceUrlMissing = String.IsNullOrWhiteSpace(serviceUrl); var graphQlSchemaFiles = context.AdditionalFiles.Where(f => Path.GetFileName(f.Path).EndsWith(".gql.schema.json", StringComparison.OrdinalIgnoreCase)).ToList(); var indexRegexScalarFieldTypeMappingProviderConfigurationFile = graphQlSchemaFiles.FindIndex(f => String.Equals(Path.GetFileName(f.Path), FileNameRegexScalarFieldTypeMappingProviderConfiguration, StringComparison.OrdinalIgnoreCase)); ICollection <RegexScalarFieldTypeMappingRule> regexScalarFieldTypeMappingProviderRules = null; if (indexRegexScalarFieldTypeMappingProviderConfigurationFile != -1) { regexScalarFieldTypeMappingProviderRules = JsonConvert.DeserializeObject <ICollection <RegexScalarFieldTypeMappingRule> >( graphQlSchemaFiles[indexRegexScalarFieldTypeMappingProviderConfigurationFile].GetText().ToString()); graphQlSchemaFiles.RemoveAt(indexRegexScalarFieldTypeMappingProviderConfigurationFile); } var isSchemaFileSpecified = graphQlSchemaFiles.Any(); if (isServiceUrlMissing && !isSchemaFileSpecified) { context.ReportDiagnostic( Diagnostic.Create( DescriptorInfo, Location.None, "Neither \"GraphQlClientGenerator_ServiceUrl\" parameter nor GraphQL JSON schema additional file specified; terminating. ")); return; } if (!isServiceUrlMissing && isSchemaFileSpecified) { context.ReportDiagnostic( Diagnostic.Create( DescriptorParameterError, Location.None, "\"GraphQlClientGenerator_ServiceUrl\" parameter and GraphQL JSON schema additional file are mutually exclusive. ")); return; } context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + "Namespace", out var @namespace); if (String.IsNullOrWhiteSpace(@namespace)) { var root = (CompilationUnitSyntax)compilation.SyntaxTrees.FirstOrDefault()?.GetRoot(); var namespaceIdentifier = (IdentifierNameSyntax)root?.Members.OfType <NamespaceDeclarationSyntax>().FirstOrDefault()?.Name; if (namespaceIdentifier == null) { context.ReportDiagnostic( Diagnostic.Create( DescriptorParameterError, Location.None, "\"GraphQlClientGenerator_Namespace\" required")); return; } @namespace = namespaceIdentifier.Identifier.ValueText; context.ReportDiagnostic( Diagnostic.Create( DescriptorInfo, Location.None, $"\"GraphQlClientGenerator_Namespace\" not specified; using \"{@namespace}\"")); } var configuration = new GraphQlGeneratorConfiguration { TreatUnknownObjectAsScalar = true }; context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + "ClassPrefix", out var classPrefix); configuration.ClassPrefix = classPrefix; context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + "ClassSuffix", out var classSuffix); configuration.ClassSuffix = classSuffix; if (compilation.LanguageVersion >= LanguageVersion.CSharp6) { configuration.CSharpVersion = compilation.Options.NullableContextOptions == NullableContextOptions.Disable ? CSharpVersion.Newest : CSharpVersion.NewestWithNullableReferences; } var currentParameterName = "IncludeDeprecatedFields"; context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + currentParameterName, out var includeDeprecatedFieldsRaw); configuration.IncludeDeprecatedFields = !String.IsNullOrWhiteSpace(includeDeprecatedFieldsRaw) && Convert.ToBoolean(includeDeprecatedFieldsRaw); currentParameterName = "HttpMethod"; if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + currentParameterName, out var httpMethod)) { httpMethod = "POST"; } currentParameterName = "CommentGeneration"; context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + currentParameterName, out var commentGenerationRaw); configuration.CommentGeneration = String.IsNullOrWhiteSpace(commentGenerationRaw) ? CommentGenerationOption.CodeSummary : (CommentGenerationOption)Enum.Parse(typeof(CommentGenerationOption), commentGenerationRaw, true); currentParameterName = "FloatTypeMapping"; context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + currentParameterName, out var floatTypeMappingRaw); configuration.FloatTypeMapping = String.IsNullOrWhiteSpace(floatTypeMappingRaw) ? FloatTypeMapping.Decimal : (FloatTypeMapping)Enum.Parse(typeof(FloatTypeMapping), floatTypeMappingRaw, true); currentParameterName = "BooleanTypeMapping"; context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + currentParameterName, out var booleanTypeMappingRaw); configuration.BooleanTypeMapping = String.IsNullOrWhiteSpace(booleanTypeMappingRaw) ? BooleanTypeMapping.Boolean : (BooleanTypeMapping)Enum.Parse(typeof(BooleanTypeMapping), booleanTypeMappingRaw, true); currentParameterName = "IdTypeMapping"; context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + currentParameterName, out var idTypeMappingRaw); configuration.IdTypeMapping = String.IsNullOrWhiteSpace(idTypeMappingRaw) ? IdTypeMapping.Guid : (IdTypeMapping)Enum.Parse(typeof(IdTypeMapping), idTypeMappingRaw, true); currentParameterName = "JsonPropertyGeneration"; context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + currentParameterName, out var jsonPropertyGenerationRaw); configuration.JsonPropertyGeneration = String.IsNullOrWhiteSpace(jsonPropertyGenerationRaw) ? JsonPropertyGenerationOption.CaseInsensitive : (JsonPropertyGenerationOption)Enum.Parse(typeof(JsonPropertyGenerationOption), jsonPropertyGenerationRaw, true); currentParameterName = "EnumValueNaming"; context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + currentParameterName, out var enumValueNamingRaw); configuration.EnumValueNaming = String.IsNullOrWhiteSpace(enumValueNamingRaw) ? EnumValueNamingOption.CSharp : (EnumValueNamingOption)Enum.Parse(typeof(EnumValueNamingOption), enumValueNamingRaw, true); currentParameterName = "CustomClassMapping"; context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + currentParameterName, out var customClassMappingRaw); if (!KeyValueParameterParser.TryGetCustomClassMapping( customClassMappingRaw?.Split(new[] { '|', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries), out var customMapping, out var customMappingParsingErrorMessage)) { context.ReportDiagnostic(Diagnostic.Create(DescriptorParameterError, Location.None, customMappingParsingErrorMessage)); return; } foreach (var kvp in customMapping) { configuration.CustomClassNameMapping.Add(kvp); } currentParameterName = "Headers"; context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + currentParameterName, out var headersRaw); if (!KeyValueParameterParser.TryGetCustomHeaders( headersRaw?.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries), out var headers, out var headerParsingErrorMessage)) { context.ReportDiagnostic(Diagnostic.Create(DescriptorParameterError, Location.None, headerParsingErrorMessage)); return; } currentParameterName = "ScalarFieldTypeMappingProvider"; if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKeyPrefix + currentParameterName, out var scalarFieldTypeMappingProviderName)) { if (regexScalarFieldTypeMappingProviderRules != null) { context.ReportDiagnostic(Diagnostic.Create(DescriptorParameterError, Location.None, "\"GraphQlClientGenerator_ScalarFieldTypeMappingProvider\" and RegexScalarFieldTypeMappingProviderConfiguration are mutually exclusive")); return; } if (String.IsNullOrWhiteSpace(scalarFieldTypeMappingProviderName)) { context.ReportDiagnostic(Diagnostic.Create(DescriptorParameterError, Location.None, "\"GraphQlClientGenerator_ScalarFieldTypeMappingProvider\" value missing")); return; } var scalarFieldTypeMappingProviderType = Type.GetType(scalarFieldTypeMappingProviderName); if (scalarFieldTypeMappingProviderType == null) { context.ReportDiagnostic(Diagnostic.Create(DescriptorParameterError, Location.None, $"ScalarFieldTypeMappingProvider \"{scalarFieldTypeMappingProviderName}\" not found")); return; } var scalarFieldTypeMappingProvider = (IScalarFieldTypeMappingProvider)Activator.CreateInstance(scalarFieldTypeMappingProviderType); configuration.ScalarFieldTypeMappingProvider = scalarFieldTypeMappingProvider; } else if (regexScalarFieldTypeMappingProviderRules?.Count > 0) { configuration.ScalarFieldTypeMappingProvider = new RegexScalarFieldTypeMappingProvider(regexScalarFieldTypeMappingProviderRules); } var graphQlSchemas = new List <(string TargetFileName, GraphQlSchema Schema)>(); if (isSchemaFileSpecified) { foreach (var schemaFile in graphQlSchemaFiles) { var targetFileName = Path.GetFileNameWithoutExtension(schemaFile.Path) + ".cs"; graphQlSchemas.Add((targetFileName, GraphQlGenerator.DeserializeGraphQlSchema(schemaFile.GetText().ToString()))); } } else { graphQlSchemas.Add((FileNameGraphQlClientSource, GraphQlGenerator.RetrieveSchema(new HttpMethod(httpMethod), serviceUrl, headers).GetAwaiter().GetResult())); context.ReportDiagnostic( Diagnostic.Create( DescriptorInfo, Location.None, "GraphQl schema fetched successfully from " + serviceUrl)); } var generator = new GraphQlGenerator(configuration); foreach (var(targetFileName, schema) in graphQlSchemas) { var builder = new StringBuilder(); using (var writer = new StringWriter(builder)) generator.WriteFullClientCSharpFile(schema, @namespace, writer); context.AddSource(targetFileName, SourceText.From(builder.ToString(), Encoding.UTF8)); } context.ReportDiagnostic( Diagnostic.Create( DescriptorInfo, Location.None, "GraphQlClientGenerator task completed successfully. ")); } catch (Exception exception) { context.ReportDiagnostic(Diagnostic.Create(DescriptorGenerationError, Location.None, exception.Message)); } }
public static async Task <IReadOnlyCollection <FileInfo> > GenerateClientSourceCode(ProgramOptions options) { var isServiceUrlMissing = String.IsNullOrWhiteSpace(options.ServiceUrl); if (isServiceUrlMissing && String.IsNullOrWhiteSpace(options.SchemaFileName)) { System.Console.WriteLine("ERROR: Either \"serviceUrl\" or \"schemaFileName\" parameter must be specified. "); Environment.Exit(4); } if (!isServiceUrlMissing && !String.IsNullOrWhiteSpace(options.SchemaFileName)) { System.Console.WriteLine("ERROR: \"serviceUrl\" and \"schemaFileName\" parameters are mutually exclusive. "); Environment.Exit(5); } GraphQlSchema schema; if (isServiceUrlMissing) { schema = GraphQlGenerator.DeserializeGraphQlSchema(await File.ReadAllTextAsync(options.SchemaFileName)); } else { if (!KeyValueParameterParser.TryGetCustomHeaders(options.Headers, out var headers, out var headerParsingErrorMessage)) { System.Console.WriteLine("ERROR: " + headerParsingErrorMessage); Environment.Exit(3); } schema = await GraphQlGenerator.RetrieveSchema(options.ServiceUrl, headers); } var generatorConfiguration = new GraphQlGeneratorConfiguration { CSharpVersion = options.CSharpVersion, ClassPrefix = options.ClassPrefix, ClassSuffix = options.ClassSuffix, GeneratePartialClasses = options.PartialClasses, MemberAccessibility = options.MemberAccessibility, IdTypeMapping = options.IdTypeMapping, FloatTypeMapping = options.FloatTypeMapping, JsonPropertyGeneration = options.JsonPropertyAttribute }; if (!KeyValueParameterParser.TryGetCustomClassMapping(options.ClassMapping, out var customMapping, out var customMappingParsingErrorMessage)) { System.Console.WriteLine("ERROR: " + customMappingParsingErrorMessage); Environment.Exit(3); } foreach (var kvp in customMapping) { generatorConfiguration.CustomClassNameMapping.Add(kvp); } var generator = new GraphQlGenerator(generatorConfiguration); if (options.OutputType == OutputType.SingleFile) { await File.WriteAllTextAsync(options.OutputPath, generator.GenerateFullClientCSharpFile(schema, options.Namespace)); return(new[] { new FileInfo(options.OutputPath) }); } var multipleFileGenerationContext = new MultipleFileGenerationContext(schema, options.OutputPath, options.Namespace); generator.Generate(multipleFileGenerationContext); return(multipleFileGenerationContext.Files); }