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);
            }
        }
Ejemplo n.º 2
0
    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);
    }
Ejemplo n.º 3
0
    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));
        }
    }
Ejemplo n.º 4
0
        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);
        }