From 76c06e6e2265bc875d0619cfe96e28002fba1d3d Mon Sep 17 00:00:00 2001 From: HavenDV Date: Sat, 27 Jan 2024 20:08:39 +0400 Subject: [PATCH] refactor: Added OpenApiGenerator.Core library. --- OpenApiGenerator.sln | 7 + .../Extensions/OpenApiExtensions.cs} | 122 +++--------- .../Extensions/StringExtensions.cs | 175 ++++++++++++++++++ .../Generation}/Sources.EndPoints.cs | 8 +- .../Generation}/Sources.Models.cs | 48 ++--- .../Generators/ClientGeneratorMethods.cs | 40 ++++ .../Generators/ModelGeneratorMethods.cs | 54 ++++++ .../Models/EndPoint.cs | 4 +- .../Models/FileWithName.cs | 23 +++ .../Models/ModelData.cs} | 48 ++--- .../Models/ModelStyle.cs | 2 +- .../Models/NamingConvention.cs | 2 +- .../Models/PropertyData.cs} | 6 +- .../OpenApiGenerator.Core/Models/Settings.cs | 15 ++ .../OpenApiGenerator.Core.csproj | 22 +++ .../Generators/ClientGenerator.cs | 33 +--- .../Generators/ModelGenerator.cs | 44 ++--- .../InitializationContextExtensions.cs | 14 -- .../OpenApiGenerator/OpenApiGenerator.csproj | 18 +- .../OpenApiGenerator/OpenApiGenerator.props | 2 - .../Settings.cs => OptionsExtensions.cs} | 44 ++--- .../OpenApiGenerator.IntegrationTests.csproj | 5 + .../OpenApiGenerator.SnapshotTests/Tests.cs | 12 -- 23 files changed, 474 insertions(+), 274 deletions(-) rename src/libs/{OpenApiGenerator/Extensions.cs => OpenApiGenerator.Core/Extensions/OpenApiExtensions.cs} (68%) create mode 100644 src/libs/OpenApiGenerator.Core/Extensions/StringExtensions.cs rename src/libs/{OpenApiGenerator/Sources => OpenApiGenerator.Core/Generation}/Sources.EndPoints.cs (71%) rename src/libs/{OpenApiGenerator/Sources => OpenApiGenerator.Core/Generation}/Sources.Models.cs (61%) create mode 100644 src/libs/OpenApiGenerator.Core/Generators/ClientGeneratorMethods.cs create mode 100644 src/libs/OpenApiGenerator.Core/Generators/ModelGeneratorMethods.cs rename src/libs/{OpenApiGenerator => OpenApiGenerator.Core}/Models/EndPoint.cs (76%) create mode 100644 src/libs/OpenApiGenerator.Core/Models/FileWithName.cs rename src/libs/{OpenApiGenerator/Models/Model.cs => OpenApiGenerator.Core/Models/ModelData.cs} (82%) rename src/libs/{OpenApiGenerator => OpenApiGenerator.Core}/Models/ModelStyle.cs (69%) rename src/libs/{OpenApiGenerator => OpenApiGenerator.Core}/Models/NamingConvention.cs (62%) rename src/libs/{OpenApiGenerator/Models/Property.cs => OpenApiGenerator.Core/Models/PropertyData.cs} (68%) create mode 100644 src/libs/OpenApiGenerator.Core/Models/Settings.cs create mode 100644 src/libs/OpenApiGenerator.Core/OpenApiGenerator.Core.csproj delete mode 100644 src/libs/OpenApiGenerator/InitializationContextExtensions.cs rename src/libs/OpenApiGenerator/{Models/Settings.cs => OptionsExtensions.cs} (60%) diff --git a/OpenApiGenerator.sln b/OpenApiGenerator.sln index 78d739a934..8ef96073e6 100644 --- a/OpenApiGenerator.sln +++ b/OpenApiGenerator.sln @@ -28,6 +28,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiGenerator.Integratio EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiGenerator.Helpers", "src\libs\OpenApiGenerator.Helpers\OpenApiGenerator.Helpers.csproj", "{CF6126AF-6934-46EC-A0F2-B75C673EA8D8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiGenerator.Core", "src\libs\OpenApiGenerator.Core\OpenApiGenerator.Core.csproj", "{3A3F5EE3-0076-4822-B3BA-2955EC802E25}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -50,6 +52,10 @@ Global {CF6126AF-6934-46EC-A0F2-B75C673EA8D8}.Debug|Any CPU.Build.0 = Debug|Any CPU {CF6126AF-6934-46EC-A0F2-B75C673EA8D8}.Release|Any CPU.ActiveCfg = Release|Any CPU {CF6126AF-6934-46EC-A0F2-B75C673EA8D8}.Release|Any CPU.Build.0 = Release|Any CPU + {3A3F5EE3-0076-4822-B3BA-2955EC802E25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A3F5EE3-0076-4822-B3BA-2955EC802E25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A3F5EE3-0076-4822-B3BA-2955EC802E25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A3F5EE3-0076-4822-B3BA-2955EC802E25}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -59,6 +65,7 @@ Global {440B9C75-CA33-47F3-9EDF-A7CD6886155C} = {9CAA231D-7BE1-46C9-ACD6-EB2E569CEBEA} {0EC09367-E7C5-4847-A9F3-DBC18A1C8395} = {9CAA231D-7BE1-46C9-ACD6-EB2E569CEBEA} {CF6126AF-6934-46EC-A0F2-B75C673EA8D8} = {6E5BF389-3D3F-4D74-9DD0-3B199CB529C5} + {3A3F5EE3-0076-4822-B3BA-2955EC802E25} = {6E5BF389-3D3F-4D74-9DD0-3B199CB529C5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1493AEE4-9211-46E9-BFE6-8F629EAC5693} diff --git a/src/libs/OpenApiGenerator/Extensions.cs b/src/libs/OpenApiGenerator.Core/Extensions/OpenApiExtensions.cs similarity index 68% rename from src/libs/OpenApiGenerator/Extensions.cs rename to src/libs/OpenApiGenerator.Core/Extensions/OpenApiExtensions.cs index 97dbc05e69..80f7b1943b 100644 --- a/src/libs/OpenApiGenerator/Extensions.cs +++ b/src/libs/OpenApiGenerator.Core/Extensions/OpenApiExtensions.cs @@ -1,19 +1,17 @@ using System.Globalization; -using H.Generators.Extensions; -using Microsoft.CodeAnalysis; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers; -using OpenApiGenerator.Models; -namespace OpenApiGenerator; +using OpenApiGenerator.Core.Models; -internal static class Extensions +namespace OpenApiGenerator.Core.Extensions; + +public static class OpenApiExtensions { public static OpenApiDocument GetOpenApiDocument( - this AdditionalText text, + this string yaml, CancellationToken cancellationToken = default) { - var yaml = text.GetText(cancellationToken)?.ToString() ?? string.Empty; var openApiDocument = new OpenApiStringReader().Read(yaml, out _); openApiDocument.Components ??= new OpenApiComponents(); @@ -25,13 +23,13 @@ public static OpenApiDocument GetOpenApiDocument( public static string GetCSharpType( this KeyValuePair schema, Settings settings, - params Model[] parents) + params ModelData[] parents) { - var model = Model.FromSchema(schema, settings, parents); + var model = ModelData.FromSchema(schema, settings, parents); var (type, reference) = (schema.Value.Type, schema.Value.Format) switch { ("object", _) or (null, _) when schema.Value.Reference != null => - ($"{Model.FromKey(schema.Value.Reference.Id, settings).ClassName}", true), + ($"{ModelData.FromKey(schema.Value.Reference.Id, settings).ClassName}", true), ("object", _) when schema.Value.Reference == null => ($"{model.ExternalClassName}", true), @@ -68,30 +66,17 @@ public static string GetCSharpType( : type; } - public static string AsArray(this string type) - { - return $"global::System.Collections.Generic.IList<{type}>"; - } - - public static string? WithPostfix(this string? type, string postfix) - { - if (type == null) - { - return null; - } - - return type + postfix; - } - public static string? GetDefaultValue(this OpenApiSchema schema) { + schema = schema ?? throw new ArgumentNullException(nameof(schema)); + return schema.Default?.GetString(); } - private readonly static string[] NewLine = { "\n" }; - public static string GetSummary(this OpenApiSchema schema) { + schema = schema ?? throw new ArgumentNullException(nameof(schema)); + var summary = schema.Description ?? string.Empty; if (schema.Default != null) { @@ -122,56 +107,6 @@ public static string GetSummary(this OpenApiSchema schema) }; } - public static string ToXmlDocumentationSummary( - this string text, - int level = 4) - { - var lines = text.Split(NewLine, StringSplitOptions.RemoveEmptyEntries); - if (lines.Length == 0) - { - lines = new[] { string.Empty }; - } - - var spaces = new string(' ', level); - - return $@"/// -{string.Join("\n", lines - .Select(line => $"{spaces}/// {line}"))} -{spaces}/// "; - } - - public static string UseWordSeparator( - this string propertyName, - params char[] separator) - { - if (!separator.Any(propertyName.Contains)) - { - return propertyName; - } - - return string.Join( - string.Empty, - propertyName - .Split(separator) - .Select(word => word.ToPropertyName())); - } - - public static string AddIndent( - this string text, - int level) - { - if (level < 1) - { - return text; - } - - var lines = text.Split(NewLine, StringSplitOptions.None); - var spaces = new string(' ', level * 4); - - return string.Join("\n", lines - .Select(line => string.IsNullOrEmpty(line) ? line : $"{spaces}{line}")); - } - public static bool IsObjectWithoutReference( this OpenApiSchema schema) { @@ -181,6 +116,8 @@ public static bool IsObjectWithoutReference( public static bool IsEnum( this OpenApiSchema schema) { + schema = schema ?? throw new ArgumentNullException(nameof(schema)); + return schema.Type == "string" && schema.Enum.Any(); } @@ -199,7 +136,7 @@ public static KeyValuePair WithKey( } - public static Property ToEnumValue( + public static PropertyData ToEnumValue( this IOpenApiAny any) { var id = any.GetString() ?? string.Empty; @@ -208,29 +145,22 @@ public static Property ToEnumValue( .UseWordSeparator('_', '-') .Replace(".", string.Empty); - return Property.Default with + return PropertyData.Default with { Id = id, Name = name, }; } - public static string FixPropertyName( - this string propertyName, - string className) - { - return propertyName == className - ? $"{propertyName}1" - : propertyName; - } - - public static Property ToProperty( + public static PropertyData ToProperty( this KeyValuePair schema, HashSet requiredProperties, Settings settings, - params Model[] parents) + params ModelData[] parents) { - return new Property( + requiredProperties = requiredProperties ?? throw new ArgumentNullException(nameof(requiredProperties)); + + return new PropertyData( Id: schema.Key, Name: schema.Key.ToPropertyName() .FixPropertyName(parents.Last().ClassName) @@ -241,12 +171,12 @@ public static Property ToProperty( Summary: schema.Value.GetSummary()); } - public static IEnumerable WithAdditionalModels( - this Model model) + public static IEnumerable WithAdditionalModels( + this ModelData modelData) { - return new []{ model } - .Concat(model.AdditionalModels.SelectMany(WithAdditionalModels)) - .Concat(model.Enumerations.SelectMany(WithAdditionalModels)); + return new []{ modelData } + .Concat(modelData.AdditionalModels.SelectMany(WithAdditionalModels)) + .Concat(modelData.Enumerations.SelectMany(WithAdditionalModels)); } public static IEnumerable GetReferences( diff --git a/src/libs/OpenApiGenerator.Core/Extensions/StringExtensions.cs b/src/libs/OpenApiGenerator.Core/Extensions/StringExtensions.cs new file mode 100644 index 0000000000..19b40a6d4f --- /dev/null +++ b/src/libs/OpenApiGenerator.Core/Extensions/StringExtensions.cs @@ -0,0 +1,175 @@ +using System.Globalization; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; + +namespace OpenApiGenerator.Core.Extensions; + +public static class StringExtensions +{ + /// + /// Concatenates strings and cleans up line breaks at the beginning and end of the resulting string.
+ /// Returns " " if collection is empty(to use with ). + ///
+ /// + /// + public static string Inject(this IEnumerable values) + { + var text = string.Concat(values) + .TrimStart('\r', '\n') + .TrimEnd('\r', '\n'); + if (string.IsNullOrWhiteSpace(text)) + { + return " "; + } + + return text; + } + + /// + /// Makes the first letter of the name uppercase. + /// + /// + /// + /// + /// + public static string ToPropertyName(this string input) + { + return input switch + { + null => throw new ArgumentNullException(nameof(input)), + "" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)), +#if NET6_0_OR_GREATER + _ => string.Concat(input[0].ToString().ToUpper(CultureInfo.InvariantCulture), input.AsSpan(1)), +#else + _ => input[0].ToString().ToUpper(CultureInfo.InvariantCulture) + input.Substring(1), +#endif + }; + } + + /// + /// Normalizes line endings to '\n' or your endings. + /// + /// + /// '\n' by default + /// + /// + public static string NormalizeLineEndings( + this string text, + string? newLine = null) + { + text = text ?? throw new ArgumentNullException(nameof(text)); + + var newText = text + .Replace("\r\n", "\n") + .Replace("\r", "\n"); + if (newLine != null) + { + newText = newText.Replace("\n", newLine); + } + + return newText; + } + + private static readonly char[] Separator = { '\n' }; + + /// + /// Removes blank lines where there are only spaces. + /// Used to preserve formatting in code where lines of code may be missing based on conditions. + /// Just return a string with spaces to remove it. + /// + /// + /// + /// + public static string RemoveBlankLinesWhereOnlyWhitespaces(this string text) + { + text = text ?? throw new ArgumentNullException(nameof(text)); + + return string.Join( + separator: "\n", + values: text + .NormalizeLineEndings() + .Split(Separator, StringSplitOptions.None) + .Where(static line => line.Length == 0 || !line.All(char.IsWhiteSpace))); + } + + public static string AsArray(this string type) + { + return $"global::System.Collections.Generic.IList<{type}>"; + } + + public static string? WithPostfix(this string? type, string postfix) + { + if (type == null) + { + return null; + } + + return type + postfix; + } + + public static string ToXmlDocumentationSummary( + this string text, + int level = 4) + { + text = text ?? throw new ArgumentNullException(nameof(text)); + + var lines = text.Split(NewLine, StringSplitOptions.RemoveEmptyEntries); + if (lines.Length == 0) + { + lines = new[] { string.Empty }; + } + + var spaces = new string(' ', level); + + return $@"/// +{string.Join("\n", lines + .Select(line => $"{spaces}/// {line}"))} +{spaces}/// "; + } + + public static string UseWordSeparator( + this string propertyName, + params char[] separator) + { + propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName)); + + if (!separator.Any(propertyName.Contains)) + { + return propertyName; + } + + return string.Join( + string.Empty, + propertyName + .Split(separator) + .Select(word => word.ToPropertyName())); + } + + private readonly static string[] NewLine = { "\n" }; + + public static string AddIndent( + this string text, + int level) + { + text = text ?? throw new ArgumentNullException(nameof(text)); + if (level < 1) + { + return text; + } + + var lines = text.Split(NewLine, StringSplitOptions.None); + var spaces = new string(' ', level * 4); + + return string.Join("\n", lines + .Select(line => string.IsNullOrEmpty(line) ? line : $"{spaces}{line}")); + } + + public static string FixPropertyName( + this string propertyName, + string className) + { + return propertyName == className + ? $"{propertyName}1" + : propertyName; + } +} \ No newline at end of file diff --git a/src/libs/OpenApiGenerator/Sources/Sources.EndPoints.cs b/src/libs/OpenApiGenerator.Core/Generation/Sources.EndPoints.cs similarity index 71% rename from src/libs/OpenApiGenerator/Sources/Sources.EndPoints.cs rename to src/libs/OpenApiGenerator.Core/Generation/Sources.EndPoints.cs index e892a563ad..019b91dc6f 100644 --- a/src/libs/OpenApiGenerator/Sources/Sources.EndPoints.cs +++ b/src/libs/OpenApiGenerator.Core/Generation/Sources.EndPoints.cs @@ -1,9 +1,9 @@ -using H.Generators.Extensions; -using OpenApiGenerator.Models; +using OpenApiGenerator.Core.Extensions; +using OpenApiGenerator.Core.Models; -namespace OpenApiGenerator; +namespace OpenApiGenerator.Core.Generation; -internal static partial class Sources +public static partial class Sources { public static string GenerateEndPoint( EndPoint endPoint, diff --git a/src/libs/OpenApiGenerator/Sources/Sources.Models.cs b/src/libs/OpenApiGenerator.Core/Generation/Sources.Models.cs similarity index 61% rename from src/libs/OpenApiGenerator/Sources/Sources.Models.cs rename to src/libs/OpenApiGenerator.Core/Generation/Sources.Models.cs index 0a626f1f8a..606e36f149 100644 --- a/src/libs/OpenApiGenerator/Sources/Sources.Models.cs +++ b/src/libs/OpenApiGenerator.Core/Generation/Sources.Models.cs @@ -1,56 +1,56 @@ -using H.Generators.Extensions; -using OpenApiGenerator.Models; +using OpenApiGenerator.Core.Extensions; +using OpenApiGenerator.Core.Models; -namespace OpenApiGenerator; +namespace OpenApiGenerator.Core.Generation; -internal static partial class Sources +public static partial class Sources { public static string GenerateModel( - Model model, + ModelData modelData, CancellationToken cancellationToken = default) { return $@" #nullable enable -namespace {model.Namespace} +namespace {modelData.Namespace} {{ -{GenerateModel(model, level: 0, cancellationToken: cancellationToken)} +{GenerateModel(modelData, level: 0, cancellationToken: cancellationToken)} }}".RemoveBlankLinesWhereOnlyWhitespaces(); } private static string GenerateModel( - Model model, + ModelData modelData, int level, CancellationToken cancellationToken = default) { - if (model.NamingConvention == NamingConvention.ConcatNames || - level == model.Parents.AsSpan().Length) + if (modelData.NamingConvention == NamingConvention.ConcatNames || + level == modelData.Parents.AsSpan().Length) { - return model.Style switch + return modelData.Style switch { - ModelStyle.Class => GenerateClassModel(model, cancellationToken), - ModelStyle.Enumeration => GenerateEnumerationModel(model, cancellationToken), - _ => throw new NotSupportedException($"Model style {model.Style} is not supported."), + ModelStyle.Class => GenerateClassModel(modelData, cancellationToken), + ModelStyle.Enumeration => GenerateEnumerationModel(modelData, cancellationToken), + _ => throw new NotSupportedException($"Model style {modelData.Style} is not supported."), }; } return $@" -public sealed partial class {model.Parents[level].ClassName} +public sealed partial class {modelData.Parents[level].ClassName} {{ -{GenerateModel(model, level + 1, cancellationToken: cancellationToken)} +{GenerateModel(modelData, level + 1, cancellationToken: cancellationToken)} }}".RemoveBlankLinesWhereOnlyWhitespaces().AddIndent(level: 1); } public static string GenerateEnumerationModel( - Model model, + ModelData modelData, CancellationToken cancellationToken = default) { return $@" - {model.Summary.ToXmlDocumentationSummary(level: 4)} + {modelData.Summary.ToXmlDocumentationSummary(level: 4)} [global::System.Runtime.Serialization.DataContract] - public enum {model.ClassName} + public enum {modelData.ClassName} {{ -{model.Properties.Select(property => @$" +{modelData.Properties.Select(property => @$" {property.Summary.ToXmlDocumentationSummary(level: 8)} [global::System.Runtime.Serialization.EnumMember(Value=""{property.Id}"")] {property.Name}, @@ -59,14 +59,14 @@ public enum {model.ClassName} } public static string GenerateClassModel( - Model model, + ModelData modelData, CancellationToken cancellationToken = default) { return $@" - {model.Summary.ToXmlDocumentationSummary(level: 4)} - public sealed partial class {model.ClassName} + {modelData.Summary.ToXmlDocumentationSummary(level: 4)} + public sealed partial class {modelData.ClassName} {{ -{model.Properties.Select(property => @$" +{modelData.Properties.Select(property => @$" {property.Summary.ToXmlDocumentationSummary(level: 8)} [global::System.Text.Json.Serialization.JsonPropertyName(""{property.Id}"")] public{(property.IsRequired ? " required" : "")} {property.Type} {property.Name} {{ get; set; }}{(property.IsRequired || property.DefaultValue == null ? string.Empty : $" = {property.DefaultValue};")} diff --git a/src/libs/OpenApiGenerator.Core/Generators/ClientGeneratorMethods.cs b/src/libs/OpenApiGenerator.Core/Generators/ClientGeneratorMethods.cs new file mode 100644 index 0000000000..931b4dc771 --- /dev/null +++ b/src/libs/OpenApiGenerator.Core/Generators/ClientGeneratorMethods.cs @@ -0,0 +1,40 @@ +using System.Collections.Immutable; +using OpenApiGenerator.Core.Extensions; +using OpenApiGenerator.Core.Generation; +using OpenApiGenerator.Core.Models; + +namespace OpenApiGenerator.Core.Generators; + +public static class ClientGeneratorMethods +{ + public static ImmutableArray PrepareData( + (string yaml, Settings settings) tuple, + CancellationToken cancellationToken = default) + { + var (text, settings) = tuple; + var openApiDocument = text.GetOpenApiDocument(cancellationToken); + + var includedOperationIds = new HashSet(settings.IncludeOperationIds); + + return openApiDocument.Paths.SelectMany(path => + path.Value.Operations + .Where(x => + //includedOperationIds.Count == 0 || + includedOperationIds.Contains(x.Value.OperationId) || + includedOperationIds.Contains(x.Value.OperationId.ToPropertyName())) + .Select(operation => new EndPoint( + Id: operation.Value.OperationId, + Namespace: settings.Namespace, + ClassName: settings.ClassName))) + .ToImmutableArray(); + } + + public static FileWithName GetSourceCode( + EndPoint endPoint, + CancellationToken cancellationToken = default) + { + return new FileWithName( + Name: $"{endPoint.FileNameWithoutExtension}.g.cs", + Text: Sources.GenerateEndPoint(endPoint, cancellationToken: cancellationToken)); + } +} diff --git a/src/libs/OpenApiGenerator.Core/Generators/ModelGeneratorMethods.cs b/src/libs/OpenApiGenerator.Core/Generators/ModelGeneratorMethods.cs new file mode 100644 index 0000000000..ad326630dc --- /dev/null +++ b/src/libs/OpenApiGenerator.Core/Generators/ModelGeneratorMethods.cs @@ -0,0 +1,54 @@ +using System.Collections.Immutable; +using OpenApiGenerator.Core.Extensions; +using OpenApiGenerator.Core.Generation; +using OpenApiGenerator.Core.Models; + +namespace OpenApiGenerator.Core.Generators; + +public static class ModelGeneratorMethods +{ + #region Methods + + public static ImmutableArray PrepareData( + (string text, Settings settings) tuple, + CancellationToken cancellationToken = default) + { + var (text, settings) = tuple; + + var openApiDocument = text.GetOpenApiDocument(cancellationToken); + + var includedModels = new HashSet(settings.IncludeModels); + var referencesOfIncludedModels = includedModels.Count == 0 + ? [] + : new HashSet(openApiDocument.Components.Schemas + .Where(schema => + includedModels.Count == 0 || + includedModels.Contains(schema.Key)) + .SelectMany(schema => schema.GetReferences()) + .Select(reference => reference.Id)); + + return openApiDocument.Components.Schemas + .Where(schema => + includedModels.Count == 0 || + includedModels.Contains(schema.Key) || + referencesOfIncludedModels.Contains(schema.Key)) + .Select(schema => ModelData.FromSchema(schema, settings)) + .SelectMany(model => model.WithAdditionalModels()) + .Select(model => model with + { + Schema = default, + }) + .ToImmutableArray(); + } + + public static FileWithName GetSourceCode( + ModelData modelData, + CancellationToken cancellationToken = default) + { + return new FileWithName( + Name: $"{modelData.FileNameWithoutExtension}.g.cs", + Text: Sources.GenerateModel(modelData, cancellationToken: cancellationToken)); + } + + #endregion +} diff --git a/src/libs/OpenApiGenerator/Models/EndPoint.cs b/src/libs/OpenApiGenerator.Core/Models/EndPoint.cs similarity index 76% rename from src/libs/OpenApiGenerator/Models/EndPoint.cs rename to src/libs/OpenApiGenerator.Core/Models/EndPoint.cs index b682b2701a..b3cfe500c0 100644 --- a/src/libs/OpenApiGenerator/Models/EndPoint.cs +++ b/src/libs/OpenApiGenerator.Core/Models/EndPoint.cs @@ -1,6 +1,6 @@ -using H.Generators.Extensions; +using OpenApiGenerator.Core.Extensions; -namespace OpenApiGenerator.Models; +namespace OpenApiGenerator.Core.Models; public readonly record struct EndPoint( string Id, diff --git a/src/libs/OpenApiGenerator.Core/Models/FileWithName.cs b/src/libs/OpenApiGenerator.Core/Models/FileWithName.cs new file mode 100644 index 0000000000..2feb1319f8 --- /dev/null +++ b/src/libs/OpenApiGenerator.Core/Models/FileWithName.cs @@ -0,0 +1,23 @@ +namespace OpenApiGenerator.Core.Models; + +/// +/// +/// +/// +/// +public readonly record struct FileWithName( + string Name, + string Text) +{ + /// + /// + /// + public static FileWithName Empty => new( + Name: string.Empty, + Text: string.Empty); + + /// + /// + /// + public bool IsEmpty => string.IsNullOrEmpty(Name) || string.IsNullOrEmpty(Text); +} diff --git a/src/libs/OpenApiGenerator/Models/Model.cs b/src/libs/OpenApiGenerator.Core/Models/ModelData.cs similarity index 82% rename from src/libs/OpenApiGenerator/Models/Model.cs rename to src/libs/OpenApiGenerator.Core/Models/ModelData.cs index 80e9c34dba..38b75cd90d 100644 --- a/src/libs/OpenApiGenerator/Models/Model.cs +++ b/src/libs/OpenApiGenerator.Core/Models/ModelData.cs @@ -1,29 +1,29 @@ using System.Collections.Immutable; -using H.Generators.Extensions; using Microsoft.OpenApi.Models; +using OpenApiGenerator.Core.Extensions; -namespace OpenApiGenerator.Models; +namespace OpenApiGenerator.Core.Models; -public readonly record struct Model( +public readonly record struct ModelData( KeyValuePair Schema, string Id, bool AddTypeName, - EquatableArray Parents, + ImmutableArray Parents, string Namespace, NamingConvention NamingConvention, ModelStyle Style, - EquatableArray Properties, + ImmutableArray Properties, string Summary, - EquatableArray AdditionalModels, - EquatableArray Enumerations + ImmutableArray AdditionalModels, + ImmutableArray Enumerations ) { - public static IList WithModelName( + public static IList WithModelName( IList schemas, string key, Settings settings, bool addTypeName, - params Model[] parents) + params ModelData[] parents) { return schemas .Select(x => FromSchema(x.WithKey(key), settings, parents) with @@ -33,12 +33,12 @@ public static IList WithModelName( .ToArray(); } - public static Model FromKey( + public static ModelData FromKey( string key, Settings settings, - params Model[] parents) + params ModelData[] parents) { - return new Model( + return new ModelData( Schema: default, Id: key, AddTypeName: false, @@ -46,19 +46,19 @@ public static Model FromKey( Namespace: settings.Namespace, NamingConvention: settings.NamingConvention, Style: settings.ModelStyle, - Properties: ImmutableArray.Empty, + Properties: ImmutableArray.Empty, Summary: string.Empty, - AdditionalModels: ImmutableArray.Empty, - Enumerations: ImmutableArray.Empty + AdditionalModels: ImmutableArray.Empty, + Enumerations: ImmutableArray.Empty ); } - public static Model FromSchema( + public static ModelData FromSchema( KeyValuePair schema, Settings settings, - params Model[] parents) + params ModelData[] parents) { - var model = new Model( + var model = new ModelData( Schema: schema, Id: schema.Key, AddTypeName: false, @@ -66,10 +66,10 @@ public static Model FromSchema( Namespace: settings.Namespace, NamingConvention: settings.NamingConvention, Style: settings.ModelStyle, - Properties: ImmutableArray.Empty, + Properties: ImmutableArray.Empty, Summary: schema.Value.GetSummary(), - AdditionalModels: ImmutableArray.Empty, - Enumerations: ImmutableArray.Empty + AdditionalModels: ImmutableArray.Empty, + Enumerations: ImmutableArray.Empty ); var requiredProperties = new HashSet(schema.Value.Required); @@ -87,7 +87,7 @@ public static Model FromSchema( .Select(x => FromSchema(x, settings, innerParents)) .Concat(schema.Value.Properties.SelectMany(x => x.Value.Items != null && x.Value.Items.IsObjectWithoutReference() ? new []{ FromSchema(x.Value.Items.WithKey(x.Key), settings, innerParents) } - : Array.Empty())) + : Array.Empty())) .Where(static x => x.Schema.Value.IsObjectWithoutReference()) .Concat(schema.Value.Properties .SelectMany(x => WithModelName(x.Value.AnyOf, x.Key, settings, addTypeName: true, innerParents)) @@ -134,14 +134,14 @@ public static Model FromSchema( { NamingConvention.InnerClasses => Parents.IsEmpty ? Name : $"_{Name}", NamingConvention.ConcatNames => Parents.IsEmpty ? Name : $"{Parents.Last().ClassName}{Name}", - _ => throw new ArgumentOutOfRangeException() + _ => string.Empty, }; public string ExternalClassName => NamingConvention switch { NamingConvention.InnerClasses => string.Join(".", Parents.Select(x => x.ClassName).Append(ClassName)), NamingConvention.ConcatNames => ClassName, - _ => throw new ArgumentOutOfRangeException() + _ => string.Empty, }; public string FileNameWithoutExtension => $"{Namespace}.Models.{ExternalClassName}"; diff --git a/src/libs/OpenApiGenerator/Models/ModelStyle.cs b/src/libs/OpenApiGenerator.Core/Models/ModelStyle.cs similarity index 69% rename from src/libs/OpenApiGenerator/Models/ModelStyle.cs rename to src/libs/OpenApiGenerator.Core/Models/ModelStyle.cs index 6cb2b4d4a8..1824829276 100644 --- a/src/libs/OpenApiGenerator/Models/ModelStyle.cs +++ b/src/libs/OpenApiGenerator.Core/Models/ModelStyle.cs @@ -1,4 +1,4 @@ -namespace OpenApiGenerator.Models; +namespace OpenApiGenerator.Core.Models; public enum ModelStyle { diff --git a/src/libs/OpenApiGenerator/Models/NamingConvention.cs b/src/libs/OpenApiGenerator.Core/Models/NamingConvention.cs similarity index 62% rename from src/libs/OpenApiGenerator/Models/NamingConvention.cs rename to src/libs/OpenApiGenerator.Core/Models/NamingConvention.cs index 2ccaa27060..b29edf3851 100644 --- a/src/libs/OpenApiGenerator/Models/NamingConvention.cs +++ b/src/libs/OpenApiGenerator.Core/Models/NamingConvention.cs @@ -1,4 +1,4 @@ -namespace OpenApiGenerator.Models; +namespace OpenApiGenerator.Core.Models; public enum NamingConvention { diff --git a/src/libs/OpenApiGenerator/Models/Property.cs b/src/libs/OpenApiGenerator.Core/Models/PropertyData.cs similarity index 68% rename from src/libs/OpenApiGenerator/Models/Property.cs rename to src/libs/OpenApiGenerator.Core/Models/PropertyData.cs index 386d0a9879..10ee076141 100644 --- a/src/libs/OpenApiGenerator/Models/Property.cs +++ b/src/libs/OpenApiGenerator.Core/Models/PropertyData.cs @@ -1,6 +1,6 @@ -namespace OpenApiGenerator.Models; +namespace OpenApiGenerator.Core.Models; -public readonly record struct Property( +public readonly record struct PropertyData( string Id, string Name, string Type, @@ -8,7 +8,7 @@ public readonly record struct Property( string? DefaultValue, string Summary) { - public static Property Default => new( + public static PropertyData Default => new( Id: string.Empty, Name: string.Empty, Type: string.Empty, diff --git a/src/libs/OpenApiGenerator.Core/Models/Settings.cs b/src/libs/OpenApiGenerator.Core/Models/Settings.cs new file mode 100644 index 0000000000..5577e68231 --- /dev/null +++ b/src/libs/OpenApiGenerator.Core/Models/Settings.cs @@ -0,0 +1,15 @@ +using System.Collections.Immutable; + +namespace OpenApiGenerator.Core.Models; + +public readonly record struct Settings( + string TargetFramework, + string Namespace, + string ClassName, + NamingConvention NamingConvention, + + ImmutableArray IncludeOperationIds, + + bool GenerateModels, + ModelStyle ModelStyle, + ImmutableArray IncludeModels); \ No newline at end of file diff --git a/src/libs/OpenApiGenerator.Core/OpenApiGenerator.Core.csproj b/src/libs/OpenApiGenerator.Core/OpenApiGenerator.Core.csproj new file mode 100644 index 0000000000..1bf9f8343f --- /dev/null +++ b/src/libs/OpenApiGenerator.Core/OpenApiGenerator.Core.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + $(NoWarn);CA1031 + + + + Allows you to partially (for example, only models) or completely generate a native (without dependencies) C# client sdk according to the OpenAPI specification. Inspired by NSwag. Uses IncrementalGenerators for efficient generation and caching. + openapi;sdk;generator;source generator;dotnet;netstandard;netframework;native;nswag;incremental + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + \ No newline at end of file diff --git a/src/libs/OpenApiGenerator/Generators/ClientGenerator.cs b/src/libs/OpenApiGenerator/Generators/ClientGenerator.cs index 31f8b49a4c..74afdf61b8 100644 --- a/src/libs/OpenApiGenerator/Generators/ClientGenerator.cs +++ b/src/libs/OpenApiGenerator/Generators/ClientGenerator.cs @@ -1,7 +1,9 @@ using System.Collections.Immutable; using H.Generators.Extensions; using Microsoft.CodeAnalysis; -using OpenApiGenerator.Models; +using OpenApiGenerator.Core.Generators; +using OpenApiGenerator.Core.Models; +using FileWithName = H.Generators.Extensions.FileWithName; namespace OpenApiGenerator.Generators; @@ -29,40 +31,25 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .AddSource(context); } - private static EquatableArray PrepareData( + private static ImmutableArray PrepareData( (AdditionalText text, Settings settings) tuple, CancellationToken cancellationToken = default) { var (text, settings) = tuple; - if (settings.UseNSwag) - { - return ImmutableArray.Empty; - } + var yaml = text.GetText(cancellationToken)?.ToString() ?? string.Empty; - var openApiDocument = text.GetOpenApiDocument(cancellationToken); - - var includedOperationIds = new HashSet(settings.IncludeOperationIds); - - return openApiDocument.Paths.SelectMany(path => - path.Value.Operations - .Where(x => - //includedOperationIds.Count == 0 || - includedOperationIds.Contains(x.Value.OperationId) || - includedOperationIds.Contains(x.Value.OperationId.ToPropertyName())) - .Select(operation => new EndPoint( - Id: operation.Value.OperationId, - Namespace: settings.Namespace, - ClassName: settings.ClassName))) - .ToImmutableArray(); + return ClientGeneratorMethods.PrepareData((yaml, settings), cancellationToken); } private static FileWithName GetSourceCode( EndPoint endPoint, CancellationToken cancellationToken = default) { + var fileWithName = ClientGeneratorMethods.GetSourceCode(endPoint, cancellationToken); + return new FileWithName( - Name: $"{endPoint.FileNameWithoutExtension}.g.cs", - Text: Sources.GenerateEndPoint(endPoint, cancellationToken: cancellationToken)); + Name: fileWithName.Name, + Text: fileWithName.Text); } #endregion diff --git a/src/libs/OpenApiGenerator/Generators/ModelGenerator.cs b/src/libs/OpenApiGenerator/Generators/ModelGenerator.cs index e6ef8bfebd..b13d6f4932 100644 --- a/src/libs/OpenApiGenerator/Generators/ModelGenerator.cs +++ b/src/libs/OpenApiGenerator/Generators/ModelGenerator.cs @@ -1,7 +1,9 @@ using System.Collections.Immutable; using H.Generators.Extensions; using Microsoft.CodeAnalysis; -using OpenApiGenerator.Models; +using OpenApiGenerator.Core.Generators; +using OpenApiGenerator.Core.Models; +using FileWithName = H.Generators.Extensions.FileWithName; namespace OpenApiGenerator.Generators; @@ -29,49 +31,25 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .AddSource(context); } - private static EquatableArray PrepareData( + private static ImmutableArray PrepareData( (AdditionalText text, Settings settings) tuple, CancellationToken cancellationToken = default) { var (text, settings) = tuple; - if (settings.UseNSwag) - { - return ImmutableArray.Empty; - } + var yaml = text.GetText(cancellationToken)?.ToString() ?? string.Empty; - var openApiDocument = text.GetOpenApiDocument(cancellationToken); - - var includedModels = new HashSet(settings.IncludeModels); - var referencesOfIncludedModels = includedModels.Count == 0 - ? [] - : new HashSet(openApiDocument.Components.Schemas - .Where(schema => - includedModels.Count == 0 || - includedModels.Contains(schema.Key)) - .SelectMany(schema => schema.GetReferences()) - .Select(reference => reference.Id)); - - return openApiDocument.Components.Schemas - .Where(schema => - includedModels.Count == 0 || - includedModels.Contains(schema.Key) || - referencesOfIncludedModels.Contains(schema.Key)) - .Select(schema => Model.FromSchema(schema, settings)) - .SelectMany(model => model.WithAdditionalModels()) - .Select(model => model with - { - Schema = default, - }) - .ToImmutableArray(); + return ModelGeneratorMethods.PrepareData((yaml, settings), cancellationToken); } private static FileWithName GetSourceCode( - Model model, + ModelData model, CancellationToken cancellationToken = default) { + var fileWithName = ModelGeneratorMethods.GetSourceCode(model, cancellationToken); + return new FileWithName( - Name: $"{model.FileNameWithoutExtension}.g.cs", - Text: Sources.GenerateModel(model, cancellationToken: cancellationToken)); + Name: fileWithName.Name, + Text: fileWithName.Text); } #endregion diff --git a/src/libs/OpenApiGenerator/InitializationContextExtensions.cs b/src/libs/OpenApiGenerator/InitializationContextExtensions.cs deleted file mode 100644 index e915fe3506..0000000000 --- a/src/libs/OpenApiGenerator/InitializationContextExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.CodeAnalysis; -using OpenApiGenerator.Models; - -namespace OpenApiGenerator; - -internal static class InitializationContextExtensions -{ - public static IncrementalValueProvider DetectSettings( - this IncrementalGeneratorInitializationContext context) - { - return context.AnalyzerConfigOptionsProvider - .Select((options, _) => Settings.FromOptions(options, prefix: "OpenApiGenerator")); - } -} \ No newline at end of file diff --git a/src/libs/OpenApiGenerator/OpenApiGenerator.csproj b/src/libs/OpenApiGenerator/OpenApiGenerator.csproj index bb09c320a2..10dc5304dc 100644 --- a/src/libs/OpenApiGenerator/OpenApiGenerator.csproj +++ b/src/libs/OpenApiGenerator/OpenApiGenerator.csproj @@ -18,31 +18,27 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - - - + + diff --git a/src/libs/OpenApiGenerator/OpenApiGenerator.props b/src/libs/OpenApiGenerator/OpenApiGenerator.props index fe92f3c339..90694b9686 100644 --- a/src/libs/OpenApiGenerator/OpenApiGenerator.props +++ b/src/libs/OpenApiGenerator/OpenApiGenerator.props @@ -20,8 +20,6 @@ - - diff --git a/src/libs/OpenApiGenerator/Models/Settings.cs b/src/libs/OpenApiGenerator/OptionsExtensions.cs similarity index 60% rename from src/libs/OpenApiGenerator/Models/Settings.cs rename to src/libs/OpenApiGenerator/OptionsExtensions.cs index e914ec10d0..5bc8d1f306 100644 --- a/src/libs/OpenApiGenerator/Models/Settings.cs +++ b/src/libs/OpenApiGenerator/OptionsExtensions.cs @@ -1,61 +1,57 @@ using System.Collections.Immutable; using H.Generators.Extensions; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using OpenApiGenerator.Core.Models; +using OpenApiGenerator.Core.Extensions; -namespace OpenApiGenerator.Models; +namespace OpenApiGenerator; -public readonly record struct Settings( - string TargetFramework, - string Namespace, - string ClassName, - NamingConvention NamingConvention, - bool UseNSwag, - - EquatableArray IncludeOperationIds, - - bool GenerateModels, - ModelStyle ModelStyle, - EquatableArray IncludeModels) +public static class OptionsExtensions { - public static Settings FromOptions( - AnalyzerConfigOptionsProvider options, + public static Settings GetSettings( + this AnalyzerConfigOptionsProvider options, string prefix) { return new Settings( TargetFramework: options.GetGlobalOption("TargetFramework", prefix) ?? options.GetGlobalOption("TargetFramework") ?? "netstandard2.0", - Namespace: options.GetGlobalOption(nameof(Namespace), prefix) ?? + Namespace: options.GetGlobalOption(nameof(Settings.Namespace), prefix) ?? options.GetGlobalOption("PackageId") ?? options.GetGlobalOption("AssemblyName") ?? prefix, - ClassName: options.GetGlobalOption(nameof(ClassName), prefix) ?? + ClassName: options.GetGlobalOption(nameof(Settings.ClassName), prefix) ?? options.GetGlobalOption("PackageId")?.WithPostfix("Api") ?? options.GetGlobalOption("AssemblyName")?.WithPostfix("Api") ?? "Api", NamingConvention: Enum.TryParse( - options.GetGlobalOption(nameof(NamingConvention), prefix) ?? + options.GetGlobalOption(nameof(Settings.NamingConvention), prefix) ?? $"{default(NamingConvention):G}", ignoreCase: true, out var namingConvention) ? namingConvention : default, - UseNSwag: bool.TryParse( - options.GetGlobalOption(nameof(UseNSwag), prefix), - out var useNSwag) && useNSwag, - IncludeOperationIds: (options.GetGlobalOption(nameof(IncludeOperationIds), prefix)?.Split(';') ?? + IncludeOperationIds: (options.GetGlobalOption(nameof(Settings.IncludeOperationIds), prefix)?.Split(';') ?? Array.Empty()).ToImmutableArray(), GenerateModels: bool.TryParse( - options.GetGlobalOption(nameof(GenerateModels), prefix), + options.GetGlobalOption(nameof(Settings.GenerateModels), prefix), out var generateModels) && generateModels, ModelStyle: Enum.TryParse( options.GetGlobalOption(nameof(ModelStyle), prefix) ?? $"{default(ModelStyle):G}", ignoreCase: true, out var modelStyle) ? modelStyle : default, - IncludeModels: (options.GetGlobalOption(nameof(IncludeModels), prefix)?.Split(';') ?? + IncludeModels: (options.GetGlobalOption(nameof(Settings.IncludeModels), prefix)?.Split(';') ?? Array.Empty()).ToImmutableArray() ); } + + public static IncrementalValueProvider DetectSettings( + this IncrementalGeneratorInitializationContext context) + { + return context.AnalyzerConfigOptionsProvider + .Select((options, _) => options.GetSettings(prefix: "OpenApiGenerator")); + } } \ No newline at end of file diff --git a/src/tests/OpenApiGenerator.IntegrationTests/OpenApiGenerator.IntegrationTests.csproj b/src/tests/OpenApiGenerator.IntegrationTests/OpenApiGenerator.IntegrationTests.csproj index 65872e6064..31aefba70c 100644 --- a/src/tests/OpenApiGenerator.IntegrationTests/OpenApiGenerator.IntegrationTests.csproj +++ b/src/tests/OpenApiGenerator.IntegrationTests/OpenApiGenerator.IntegrationTests.csproj @@ -32,9 +32,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/tests/OpenApiGenerator.SnapshotTests/Tests.cs b/src/tests/OpenApiGenerator.SnapshotTests/Tests.cs index da5c0735c6..f5e96513f0 100644 --- a/src/tests/OpenApiGenerator.SnapshotTests/Tests.cs +++ b/src/tests/OpenApiGenerator.SnapshotTests/Tests.cs @@ -47,18 +47,6 @@ public Task Empty() return CheckSourceAsync( Array.Empty()); } - // - // [TestMethod] - // public Task YamlWithLocalFileUseNSwag() - // { - // return CheckSourceAsync(new AdditionalText[] - // { - // new CustomAdditionalText("openapi.yaml", H.Resources.ipinfo_yaml.AsString()), - // }, new Dictionary - // { - // ["build_property.OpenApiGenerator_UseNSwag"] = "true", - // }); - // } [TestMethod] public Task OpenAi()