diff --git a/AutoSDK.slnx b/AutoSDK.slnx index 39ffe9555d..3649d0d190 100644 --- a/AutoSDK.slnx +++ b/AutoSDK.slnx @@ -3,10 +3,10 @@ + - @@ -57,6 +57,7 @@ + diff --git a/specs/petstore.yaml b/specs/petstore.yaml index fa14a9781d..de31197cf9 100644 --- a/specs/petstore.yaml +++ b/specs/petstore.yaml @@ -73,7 +73,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Pet" + $ref: "#/components/schemas/PetStore.Pet" default: description: unexpected error content: @@ -82,7 +82,7 @@ paths: $ref: "#/components/schemas/Error" components: schemas: - Pet: + PetStore.Pet: type: object required: - id @@ -99,7 +99,7 @@ components: type: array maxItems: 100 items: - $ref: "#/components/schemas/Pet" + $ref: "#/components/schemas/PetStore.Pet" Error: type: object required: diff --git a/src/libs/AutoSDK/Extensions/StringExtensions.cs b/src/libs/AutoSDK/Extensions/StringExtensions.cs index d2b397e44a..0dd7e4d3e4 100644 --- a/src/libs/AutoSDK/Extensions/StringExtensions.cs +++ b/src/libs/AutoSDK/Extensions/StringExtensions.cs @@ -1,6 +1,4 @@ using System.Globalization; -using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Readers; namespace AutoSDK.Extensions; @@ -25,7 +23,7 @@ public static string Inject(this IEnumerable values, string emptyValue = return text; } - + /// /// Makes the first letter of the name uppercase. /// @@ -46,7 +44,7 @@ public static string ToPropertyName(this string input) #endif }; } - + /// /// Makes the first letter of the name lowercase. /// @@ -75,7 +73,7 @@ public static string ToParameterName(this string input) #pragma warning restore CA1308 // Normalize strings to uppercase }; } - + /// /// Normalizes line endings to '\n' or your endings. /// @@ -99,7 +97,7 @@ public static string NormalizeLineEndings( return newText; } - + private static readonly char[] Separator = { '\n' }; /// @@ -121,85 +119,85 @@ public static string RemoveBlankLinesWhereOnlyWhitespaces(this string text) .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 ToXmlDocumentation( this string text, int level = 4, bool returnIfSingleLine = false) { text = text ?? throw new ArgumentNullException(nameof(text)); - + var lines = text.Split(NewLine, StringSplitOptions.RemoveEmptyEntries); if (lines.Length == 0) { lines = [string.Empty]; } - + if (returnIfSingleLine && lines.Length == 1) { return $"{lines[0]}"; } - + var spaces = new string(' ', level); var value = string.Join("\n", lines .Select((line, i) => $"{spaces}/// {line}{(i != lines.Length - 1 ? "
" : string.Empty)}")); - + return value; } - + public static string ToXmlDocumentationSummary( this string text, int level = 4) { text = text ?? throw new ArgumentNullException(nameof(text)); - + var spaces = new string(' ', level); var value = ToXmlDocumentation(text, level); - + return $@"/// {value} {spaces}/// "; } - + public static string ToXmlDocumentationExample( this string text, int level = 4) { text = text ?? throw new ArgumentNullException(nameof(text)); - + if (string.IsNullOrWhiteSpace(text)) { return string.Empty; } - + var value = ToXmlDocumentation(text, level, returnIfSingleLine: true); if (!value.Contains("\n")) { return $"/// {value}"; } - + var spaces = new string(' ', level); - + return $@"/// {value} {spaces}/// "; } - + public static string ToXmlDocumentationDefault( this string text, int level = 4) @@ -210,20 +208,20 @@ public static string ToXmlDocumentationDefault( { return string.Empty; } - + var value = ToXmlDocumentation(text, level, returnIfSingleLine: true); if (!value.Contains("\n")) { return $"/// {value}"; } - + var spaces = new string(' ', level); - + return $@"/// {value} {spaces}/// "; } - + public static string ToXmlDocumentationForParam( this string text, string parameterName, @@ -231,37 +229,37 @@ public static string ToXmlDocumentationForParam( { text = text ?? throw new ArgumentNullException(nameof(text)); parameterName = parameterName ?? throw new ArgumentNullException(nameof(parameterName)); - + var spaces = new string(' ', level); var value = ToXmlDocumentation(text, level); - + return string.IsNullOrWhiteSpace(text) ? $@"/// " : $@"/// {value} {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) @@ -271,14 +269,14 @@ public static string AddIndent( { 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) @@ -287,15 +285,29 @@ public static string FixPropertyName( ? $"{propertyName}1" : propertyName; } - + public static string ToClassName( this string text) { return text .ToPropertyName() - .UseWordSeparator('_', '-', '.', ' ', '\\', '/', '[', ']'); + .UseWordSeparator('_', '-', ' ', '\\', '/', '[', ']') + .Split('.') + .Last(); } - + + public static string ToNamespace( + this string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + throw new ArgumentNullException(nameof(text)); + } + return text + .Split('.') + .Last(); + } + public static string ReplaceIfEquals( this string text, string oldValue, diff --git a/src/libs/AutoSDK/Models/AnyOfData.cs b/src/libs/AutoSDK/Models/AnyOfData.cs index 6e8ff7cf85..24f3cc29dc 100644 --- a/src/libs/AutoSDK/Models/AnyOfData.cs +++ b/src/libs/AutoSDK/Models/AnyOfData.cs @@ -1,8 +1,7 @@ -using System.Collections.Immutable; using AutoSDK.Extensions; using AutoSDK.Naming.AnyOfs; -using AutoSDK.Naming.Properties; using AutoSDK.Serialization.Json; +using System.Collections.Immutable; namespace AutoSDK.Models; @@ -20,11 +19,11 @@ Settings Settings ) { public bool IsNamed => !string.IsNullOrWhiteSpace(Name); - + public static AnyOfData FromSchemaContext(SchemaContext context) { context = context ?? throw new ArgumentNullException(nameof(context)); - + var children = context.Children .Where(x => x.Hint == (context.IsAnyOf ? Hint.AnyOf @@ -35,15 +34,14 @@ public static AnyOfData FromSchemaContext(SchemaContext context) var className = context.Id.ToClassName(); TypeData? discriminatorType = null; string? discriminatorPropertyName = null; - + if (context.Schema.Discriminator != null && context.Schema.Discriminator.Mapping.Count != 0) { discriminatorType = context.Children.FirstOrDefault(x => x.Hint == Hint.Discriminator)?.TypeData; - discriminatorPropertyName = context.Schema.Discriminator.PropertyName.ToPropertyName() - .ToCSharpName(context.Settings, context.Parent); + discriminatorPropertyName = context.Schema.Discriminator.PropertyName.ToPropertyName(); } - + var count = context.IsAnyOf ? context.Schema.AnyOf.Count : context.IsOneOf @@ -86,7 +84,7 @@ public static AnyOfData FromSchemaContext(SchemaContext context) }) .ToImmutableArray().AsEquatableArray(); } - + return new AnyOfData( SubType: context.IsAnyOf ? "AnyOf" diff --git a/src/libs/AutoSDK/Models/EndPointResponse.cs b/src/libs/AutoSDK/Models/EndPointResponse.cs index 0702ffd3cc..13acf890ea 100644 --- a/src/libs/AutoSDK/Models/EndPointResponse.cs +++ b/src/libs/AutoSDK/Models/EndPointResponse.cs @@ -1,5 +1,5 @@ -using Microsoft.OpenApi.Models; using AutoSDK.Extensions; +using Microsoft.OpenApi.Models; namespace AutoSDK.Models; @@ -20,18 +20,18 @@ TypeData Type public bool IsPattern => StatusCode.Contains("XX"); public int Min => int.TryParse(StatusCode.Replace("XX", "00"), out var code) ? code : 0; public int Max => int.TryParse(StatusCode.Replace("XX", "99"), out var code) ? code : 0; - + public static EndPointResponse Default => new( StatusCode: "200", Description: string.Empty, MimeType: string.Empty, ContentType: ContentType.String, Type: TypeData.Default); - + public static EndPointResponse FromResponse(KeyValuePair responseWithStatusCode, OperationContext operation) { operation = operation ?? throw new ArgumentNullException(nameof(operation)); - + var responses = responseWithStatusCode.Value?.ResolveIfRequired().Content? .Select(x => ( StatusCode: responseWithStatusCode.Key, @@ -46,9 +46,9 @@ public static EndPointResponse FromResponse(KeyValuePair responseContext?.TypeData, + _ => responseContext?.ResolvedReference?.TypeData, }; if (responseType?.CSharpTypeWithoutNullability == "object") { @@ -77,7 +77,7 @@ public static EndPointResponse FromResponse(KeyValuePair p.Box()).ToImmutableArray(), - Namespace: context.Settings.Namespace, + Namespace: context.TypeData.Namespace ?? throw new ArgumentNullException(context.ReferenceId), Style: context.Schema.IsEnum() ? ModelStyle.Enumeration : context.Settings.ModelStyle, Settings: context.Settings, Properties: context.IsDerivedClass diff --git a/src/libs/AutoSDK/Models/SchemaContext.cs b/src/libs/AutoSDK/Models/SchemaContext.cs index fbfd891c91..89af8eee33 100644 --- a/src/libs/AutoSDK/Models/SchemaContext.cs +++ b/src/libs/AutoSDK/Models/SchemaContext.cs @@ -1,8 +1,7 @@ -using Microsoft.OpenApi.Models; using AutoSDK.Extensions; using AutoSDK.Naming.Models; -using AutoSDK.Naming.Properties; using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; namespace AutoSDK.Models; @@ -14,36 +13,36 @@ public class SchemaContext( { public SchemaContext? Parent { get; set; } public IList Children { get; set; } = []; - + public Settings Settings { get; set; } = settings; public OpenApiSchema Schema { get; set; } = schema; public string Id { get; set; } = id; public string Type { get; set; } = type; - + public string? ReferenceId { get; set; } public bool IsReference => ReferenceId != null; public SchemaContext? ResolvedReference { get; set; } - + public IList Links { get; set; } = []; - + public Hint? Hint { get; set; } public int? Index { get; set; } /// /// Used to constrain the recursion depth. /// public int Depth { get; set; } - + public string? PropertyName { get; set; } public bool IsProperty => PropertyName != null; public PropertyData? PropertyData { get; set; } - + public string? ParameterName => Parameter?.Name; public bool IsParameter => ParameterName != null; public MethodParameter? ParameterData { get; set; } - + public string? ComponentId { get; set; } public bool IsComponent => ComponentId != null || ResolvedReference?.IsComponent == true; - + public string? OperationPath { get; set; } public OperationType? OperationType { get; set; } public OpenApiOperation? Operation { get; set; } @@ -53,35 +52,35 @@ public class SchemaContext( public string? ResponseStatusCode { get; set; } public OpenApiResponse? Response { get; set; } public bool IsOperation => OperationPath != null; - + public TypeData TypeData { get; set; } = TypeData.Default; - + public bool IsClass => Type == "class" || IsDerivedClass;// || ResolvedReference?.IsClass == true; //public ModelData? ClassData { get; set; } public ModelData? ClassData => IsClass ? //IsReference - //? ModelData.FromSchemaContext(ResolvedReference!) - //: + //? ModelData.FromSchemaContext(ResolvedReference!) + //: ModelData.FromSchemaContext(this) : null; - + public bool IsEnum => Type == "enum";// || ResolvedReference?.IsEnum == true; //public ModelData? EnumData { get; set; } public ModelData? EnumData => IsEnum ? //IsReference - //? ModelData.FromSchemaContext(ResolvedReference!) - //: + //? ModelData.FromSchemaContext(ResolvedReference!) + //: ModelData.FromSchemaContext(this) : null; public string? ClassName { get; set; } - + public AnyOfData? AnyOfData { get; set; } public string? VariantName { get; set; } - + public bool IsModel => IsClass || IsEnum || IsAnyOfLikeStructure && IsComponent; - + public bool IsAnyOf => Schema.IsAnyOf(); public bool IsOneOf => Schema.IsOneOf(); public bool IsAllOf => Schema.IsAllOf() && !IsDerivedClass; @@ -100,7 +99,7 @@ public class SchemaContext( ? Children.First(x => x.ReferenceId == allOf[1].Reference?.Id) : Children.First(x => x.ReferenceId == allOf[0].Reference?.Id) : throw new InvalidOperationException("Schema is not derived class."); - + public SchemaContext BaseClassContext => Schema.IsAllOf() && Schema.AllOf is { Count: 2 } allOf @@ -109,11 +108,11 @@ public class SchemaContext( ? Children.First(x => x.ReferenceId == allOf[0].Reference?.Id) : Children.First(x => x.ReferenceId == allOf[1].Reference?.Id) : throw new InvalidOperationException("Schema is not derived class."); - + public bool IsAnyOfLikeStructure => IsAnyOf || IsOneOf || IsAllOf; public bool IsNamedAnyOfLike => IsAnyOfLikeStructure && (IsComponent || Schema.Discriminator != null); - + public IReadOnlyList ComputedProperties { get @@ -122,7 +121,7 @@ public IReadOnlyList ComputedProperties { return []; } - + if (Schema.IsBinary() && Parent?.Children.Any(x => x.PropertyName == PropertyName + "name") != true) { @@ -150,7 +149,7 @@ PropertyData.Value with } public HashSet Tags { get; set; } = []; - + private static string ComputeType(OpenApiSchema schema, bool isComponent) { if (schema.IsEnum()) @@ -158,9 +157,9 @@ private static string ComputeType(OpenApiSchema schema, bool isComponent) return "enum"; } if (schema.Type == "object")// && - // (isComponent || - // schema.Properties.Count > 0 || - // !schema.AdditionalPropertiesAllowed)) + // (isComponent || + // schema.Properties.Count > 0 || + // !schema.AdditionalPropertiesAllowed)) { return "class"; } @@ -209,7 +208,7 @@ private static string ComputeType(OpenApiSchema schema, bool isComponent) { return "allOf"; } - + return schema.Type ?? "class"; } @@ -246,7 +245,7 @@ public static IReadOnlyList FromSchema( return [new SchemaContext( settings, schema, - id: schema.Reference.Id.ToCSharpName(settings, parent), + id: schema.Reference.Id, type: "ref") { Parent = parent, @@ -266,7 +265,7 @@ public static IReadOnlyList FromSchema( Depth = depth, }]; } - + var context = new SchemaContext( settings, schema, @@ -288,7 +287,7 @@ public static IReadOnlyList FromSchema( Response = response, Depth = depth, }; - + var children = new List(); if (schema.Items != null) { @@ -308,7 +307,7 @@ public static IReadOnlyList FromSchema( hint: Models.Hint.AdditionalProperties, depth: depth + 1)); } - + var i = 0; foreach (var property in schema.Properties) { @@ -355,7 +354,7 @@ public static IReadOnlyList FromSchema( index: i++, depth: depth + 1)); } - + if (schema.Discriminator != null) { children.AddRange(FromSchema( @@ -364,7 +363,7 @@ public static IReadOnlyList FromSchema( Type = "object", Properties = { - [schema.Discriminator.PropertyName] = new OpenApiSchema + [schema.Discriminator.PropertyName.ToPropertyName()] = new OpenApiSchema { Type = "string", Enum = schema.Discriminator.Mapping?.Keys @@ -379,20 +378,20 @@ public static IReadOnlyList FromSchema( hint: Models.Hint.Discriminator, depth: depth + 1)); } - + context.Children = children .Where(x => x.Depth == depth + 1) .ToList(); - + // We need to fix name, so it doesn't conflict with real used name() and not to be handled as name conflict. if (schema.HasAllOfTypeForMetadata(propertyName: propertyName)) { context.Id += "_AllOf1Wrapped"; } - - return [context, ..children]; + + return [context, .. children]; } - + public void ComputeData(int level = 0, int maxDepth = 20) { // Prevent infinite recursion for circular references @@ -400,13 +399,13 @@ public void ComputeData(int level = 0, int maxDepth = 20) { return; } - + ResolvedReference?.ComputeData(level + 1, maxDepth: maxDepth); foreach (var child in Children) { child.ComputeData(level + 1, maxDepth: maxDepth); } - + TypeData = IsReference ? ResolvedReference?.TypeData ?? throw new InvalidOperationException("Resolved reference must have type data.") @@ -432,7 +431,7 @@ public void ComputeData(int level = 0, int maxDepth = 20) AnyOfData = global::AutoSDK.Models.AnyOfData.FromSchemaContext(this); } } - + public bool HasAnyTag(params string[] tags) { return @@ -440,19 +439,19 @@ public bool HasAnyTag(params string[] tags) Tags.Any(tags.Contains) || Parent?.HasAnyTag(tags) == true; } - + public bool AnyParent(Func predicate) { predicate = predicate ?? throw new ArgumentNullException(nameof(predicate)); - + if (predicate(this)) { return true; } - + return Parent?.AnyParent(predicate) == true; } - + public IReadOnlyCollection WithAllChildren(int level = 0, int maxDepth = 20) { // Prevent infinite recursion for circular references @@ -460,21 +459,21 @@ public IReadOnlyCollection WithAllChildren(int level = 0, int max { return []; } - + if (IsReference) { - return [this, ..ResolvedReference!.WithAllChildren(level + 1, maxDepth: maxDepth)]; + return [this, .. ResolvedReference!.WithAllChildren(level + 1, maxDepth: maxDepth)]; } - + var result = new List { this }; foreach (var child in Children) { result.AddRange(child.WithAllChildren(level + 1, maxDepth: maxDepth)); } - + return result; } - + public void ComputeTags(HashSet? parentTags = null, int level = 0, int maxDepth = 20) { // Prevent infinite recursion for circular references @@ -482,7 +481,7 @@ public void ComputeTags(HashSet? parentTags = null, int level = 0, int m { return; } - + foreach (var tag in Operation?.Tags ?? []) { Tags.Add(tag.Name); @@ -491,7 +490,7 @@ public void ComputeTags(HashSet? parentTags = null, int level = 0, int m { Tags.Add(tag); } - + foreach (var child in Children) { child.ComputeTags(Tags, level + 1, maxDepth: maxDepth); diff --git a/src/libs/AutoSDK/Models/TypeData.cs b/src/libs/AutoSDK/Models/TypeData.cs index 94bdb33bbb..9e0a03441f 100644 --- a/src/libs/AutoSDK/Models/TypeData.cs +++ b/src/libs/AutoSDK/Models/TypeData.cs @@ -55,8 +55,24 @@ public record struct TypeData( Namespace: string.Empty, IsDeprecated: false, Settings: Settings.Default); - - public string CSharpTypeWithoutNullability => CSharpTypeRaw.TrimEnd('?'); + /// + /// This method removes the namespace from the type, + /// if the type is in the ExcludeModels list. + /// This makes it possible to use global usings to define + /// outside of the generator where this type should come from. + /// + /// + private string GetCsharpTypeValue() + { + var typeName = CSharpTypeRaw.TrimEnd('?').Replace($"global::{Namespace}.", string.Empty); + if (Settings.ExcludeModels.Contains(typeName)) + { + Namespace = string.Empty; + return typeName; + } + return CSharpTypeRaw; + } + public string CSharpTypeWithoutNullability => GetCsharpTypeValue(); public string CSharpTypeWithNullability => CSharpTypeWithoutNullability + "?"; public string ShortCSharpTypeWithoutNullability => CSharpTypeWithoutNullability.Replace($"global::{Namespace}.", string.Empty); public string ShortCSharpTypeWithNullability => ShortCSharpTypeWithoutNullability + "?"; @@ -129,7 +145,7 @@ public static TypeData FromSchemaContext(SchemaContext context) subTypes = [ context.Children .FirstOrDefault(x => x is { Hint: Hint.ArrayItem } && x.TypeData != Default) - ?.TypeData ?? + ?.ResolvedReference?.TypeData ?? Default with { IsEnum = context.Schema.Items.IsEnum(), @@ -162,6 +178,11 @@ Default with } var type = GetCSharpType(context); + if (context.Schema.Reference is not null && + context.Settings.ExcludeModels.Contains(context.Schema.Reference.Id)) + { + type = context.Schema.Reference.Id; + } return new TypeData( CSharpTypeRaw: type, @@ -233,10 +254,12 @@ public static string GetCSharpType(SchemaContext context) (_, _) when context.Schema.IsUnixTimestamp() => "global::System.DateTimeOffset", (_, _) when context.Schema.IsArray() => - $"{context.Children.FirstOrDefault(x => x.Hint == Hint.ArrayItem)?.TypeData.CSharpTypeWithoutNullability}".AsArray(), + $"{(context.Children.FirstOrDefault(x => x.Hint == Hint.ArrayItem)?.IsReference == true ? + context.Children.FirstOrDefault(x => x.Hint == Hint.ArrayItem)?.ResolvedReference?.TypeData.CSharpTypeWithoutNullability : + context.Children.FirstOrDefault(x => x.Hint == Hint.ArrayItem)?.TypeData.CSharpTypeWithoutNullability)}".AsArray(), // Fallback if `items` property is missing (openai specification) ("array", _) => "byte[]", - + (_, _) when context.IsNamedAnyOfLike => $"global::{context.Settings.Namespace}.{context.Id}", (_, _) when context.IsDerivedClass => $"global::{context.Settings.Namespace}.{context.Id}", diff --git a/src/libs/AutoSDK/Naming/Models/ModelNameGenerator.cs b/src/libs/AutoSDK/Naming/Models/ModelNameGenerator.cs index 98eaadd76e..2c1bb3e100 100644 --- a/src/libs/AutoSDK/Naming/Models/ModelNameGenerator.cs +++ b/src/libs/AutoSDK/Naming/Models/ModelNameGenerator.cs @@ -1,7 +1,7 @@ -using Microsoft.OpenApi.Models; using AutoSDK.Extensions; using AutoSDK.Models; using AutoSDK.Naming.Properties; +using Microsoft.OpenApi.Models; namespace AutoSDK.Naming.Models; @@ -19,11 +19,11 @@ public static string ComputeId( { if (propertyName != null) { - return propertyName.ToCSharpName(settings, parent); + return propertyName.ToPropertyName(); } if (componentId != null) { - return componentId.ToCSharpName(settings, parent); + return componentId; } var helper = hint switch @@ -37,7 +37,7 @@ public static string ComputeId( //_ when propertyName != null => propertyName, _ => null, }; - var id = parent?.Id + helper?.ToCSharpName(settings, parent); + var id = parent?.Id + helper?.ToClassName(); if (string.IsNullOrWhiteSpace(id)) { throw new InvalidOperationException("Id is required. Invalid info."); @@ -45,11 +45,11 @@ public static string ComputeId( return id; } - + public static string? ComputeHelperName(this SchemaContext context) { context = context ?? throw new ArgumentNullException(nameof(context)); - + return (context.Hint switch { Hint.ArrayItem => "Item", @@ -60,16 +60,16 @@ public static string ComputeId( Hint.Discriminator => "Discriminator", _ when context.PropertyName != null => context.PropertyName, _ => null, - })?.ToCSharpName(context.Settings, context.Parent); + }); } - + public static string ComputeClassName(this SchemaContext context) { context = context ?? throw new ArgumentNullException(nameof(context)); - + if (context.ComponentId != null) { - return context.ComponentId.ToCSharpName(context.Settings, context.Parent).ToClassName(); + return context.ComponentId.ToClassName(); } // NamingConvention.InnerClasses => Parents.IsEmpty ? Name : $"_{Name}", @@ -80,7 +80,7 @@ public static string ComputeClassName(this SchemaContext context) } var className = id.ToClassName(); - + // Special case for anyOf/oneOf/allOf with a single non-basic type if (context.Hint is Hint.AnyOf or Hint.OneOf or Hint.AllOf && context.Parent?.Children @@ -89,7 +89,7 @@ public static string ComputeClassName(this SchemaContext context) { className = $"{context.Parent?.ComputeClassName()}"; } - + // Special case for anyOf/oneOf/allOf with a single Enum type without reference else if (context.Hint is Hint.AnyOf or Hint.OneOf or Hint.AllOf && context.Parent?.Children @@ -98,27 +98,27 @@ public static string ComputeClassName(this SchemaContext context) var variantName = ComputeHelperName(context)!; className = className.Substring(0, className.Length - variantName.Length) + "Enum"; } - + // Special case for array items with pluralized property name if (context.Hint is Hint.ArrayItem && className.EndsWith("sItem", StringComparison.Ordinal)) { className = className.Substring(0, className.Length - 5); } - + return className; } - + public static string ComputeId(SchemaContext context) { context = context ?? throw new ArgumentNullException(nameof(context)); - + context.ClassName = CSharpPropertyNameGenerator.SanitizeName(context.ComputeClassName(), context.Settings.ClsCompliantEnumPrefix); context.Id = context.ClassName; - + return context.Id; } - + public static void ResolveCollisions(IReadOnlyCollection contexts) { while (true) @@ -133,7 +133,7 @@ public static void ResolveCollisions(IReadOnlyCollection contexts { break; } - + foreach (var group in contextsWithCollision) { var i = 2; @@ -144,7 +144,7 @@ public static void ResolveCollisions(IReadOnlyCollection contexts } } } - + public static void ResolveCollisions(IReadOnlyCollection contexts) { while (true) @@ -157,7 +157,7 @@ public static void ResolveCollisions(IReadOnlyCollection conte { break; } - + foreach (var group in schemasWithCollision) { var i = 2; diff --git a/src/libs/AutoSDK/Naming/Properties/PropertyNameGenerator.cs b/src/libs/AutoSDK/Naming/Properties/PropertyNameGenerator.cs index a36007a806..6a68154916 100644 --- a/src/libs/AutoSDK/Naming/Properties/PropertyNameGenerator.cs +++ b/src/libs/AutoSDK/Naming/Properties/PropertyNameGenerator.cs @@ -12,7 +12,7 @@ public static string ComputePropertyName( var propertyName = context.PropertyName ?? throw new InvalidOperationException("Property name or parameter name is required."); var name = propertyName.ToPropertyName(); - + name = HandleWordSeparators(name); if (context.Parent != null) @@ -24,7 +24,7 @@ public static string ComputePropertyName( return name; } - + internal static string SanitizeName(string? name, string clsCompliantEnumPrefix, bool skipHandlingWordSeparators = false) { static bool InvalidFirstChar(char ch) @@ -37,7 +37,7 @@ static bool InvalidSubsequentChar(char ch) or >= 'a' and <= 'z' or >= '0' and <= '9' ); - + if (name is null || name.Length == 0) { return ""; @@ -54,7 +54,7 @@ static bool InvalidSubsequentChar(char ch) ? "_" : clsCompliantEnumPrefix; } - + if (InvalidFirstChar(name[0])) { name = (string.IsNullOrWhiteSpace(clsCompliantEnumPrefix) @@ -69,7 +69,7 @@ static bool InvalidSubsequentChar(char ch) Span buf = stackalloc char[name.Length]; name.AsSpan().CopyTo(buf); - + for (var i = 1; i < buf.Length; i++) { if (InvalidSubsequentChar(buf[i])) @@ -86,20 +86,33 @@ internal static string HandleWordSeparators(string name) { return name .ReplacePlusAndMinusOnStart() - .UseWordSeparator('_', '+', '-', '.', '/', '(', '[', ']', ')'); + .UseWordSeparator('_', '+', '-', '/', '(', '[', ']', ')'); } - internal static string ToCSharpName(this string text, Settings settings, SchemaContext? parent) + /// + /// Parses the text and returns a C# namespace and a name. + /// The namespace is onyl the part of the namespace that is maybe used inside the openapi spec. + /// It doesn't include the namespace that is defined in the settings. + /// + /// + /// + /// + /// + internal static (string, string) ToCSharpName(this string text, Settings settings, SchemaContext? parent) { - var name = text.ToPropertyName(); - - name = HandleWordSeparators(name); - - if (parent != null) - { - name = name.FixPropertyName(parent.Id); - } - - return SanitizeName(name, settings.ClsCompliantEnumPrefix, true); + //var name = text.ToPropertyName(); + + //var name = HandleWordSeparators(text); + + //if (parent != null) + //{ + //name = name.FixPropertyName(parent.Id); + //} + var name = text; + var splittedName = name.Split('.'); + return ( + SanitizeName(string.Join(".", splittedName), string.Empty, true), + SanitizeName(splittedName.Last(), settings.ClsCompliantEnumPrefix, true) + ); } } \ No newline at end of file diff --git a/src/libs/AutoSDK/Sources/Data.cs b/src/libs/AutoSDK/Sources/Data.cs index 9c073c24a9..a8c755a4c6 100644 --- a/src/libs/AutoSDK/Sources/Data.cs +++ b/src/libs/AutoSDK/Sources/Data.cs @@ -1,12 +1,12 @@ -using System.Collections.Immutable; -using System.Diagnostics; -using Microsoft.OpenApi.Models; -using AutoSDK.Extensions; +using AutoSDK.Extensions; using AutoSDK.Helpers; using AutoSDK.Models; using AutoSDK.Naming.Clients; using AutoSDK.Naming.Models; using AutoSDK.Serialization.Json; +using Microsoft.OpenApi.Models; +using System.Collections.Immutable; +using System.Diagnostics; namespace AutoSDK.Generation; @@ -18,15 +18,15 @@ public static Models.Data Prepare( { var totalTime = Stopwatch.StartNew(); var traversalTreeTime = Stopwatch.StartNew(); - + var (text, settings) = tuple; var openApiDocument = text.GetOpenApiDocument(settings, cancellationToken); var schemas = openApiDocument.GetSchemas(settings); - + traversalTreeTime.Stop(); - + var namingTime = Stopwatch.StartNew(); foreach (var schema in schemas.Where(x => x.IsModel)) @@ -35,26 +35,26 @@ public static Models.Data Prepare( } ModelNameGenerator.ResolveCollisions(schemas); - + namingTime.Stop(); - + var resolveReferencesTime = Stopwatch.StartNew(); - + var componentSchemas = schemas .Where(x => x.IsComponent) .ToDictionary(x => x.ComponentId!, x => x); - + foreach (var context in schemas.Where(x => x.IsReference)) { context.ResolvedReference = componentSchemas[context.ReferenceId!]; context.Id = context.ResolvedReference.Id; context.TypeData = context.ResolvedReference.TypeData; - + context.ResolvedReference.Links.Add(context); } - + resolveReferencesTime.Stop(); - + var filteringTime = Stopwatch.StartNew(); var includedOperationIds = new HashSet(settings.IncludeOperationIds); @@ -67,7 +67,7 @@ public static Models.Data Prepare( { excludedOperationIds.UnionWith(openApiDocument.FindAllOperationIdsForTag(tag)); } - + // Find all tags used in operations besides the ones defined in the document var allTags = openApiDocument.Tags!; foreach (var operation in openApiDocument.Paths! @@ -83,7 +83,7 @@ public static Models.Data Prepare( } } } - + if (settings.GroupByTags && allTags.Count < 2) { settings = settings with @@ -103,49 +103,58 @@ public static Models.Data Prepare( { context.ComputeTags(maxDepth: maxDepth); } - + var includedModels = new HashSet(settings.IncludeModels); var excludedModels = new HashSet(settings.ExcludeModels); - + var isFilteringRequired = settings.IncludeTags.Length > 0 || settings.ExcludeTags.Length > 0 || includedModels.Count > 0 || excludedModels.Count > 0 || !settings.GenerateModels; + var filteredSchemas = isFilteringRequired ? schemas .Where(x => - (settings.GenerateModels || - settings.GenerateSdk || - (x.Operation?.OperationId != null && includedOperationIds.Contains(x.Operation.OperationId))) && - (settings.IncludeTags.Length == 0 || - x.HasAnyTag(settings.IncludeTags.ToArray())) && - !x.HasAnyTag(settings.ExcludeTags.ToArray()) && - (!x.IsComponent && includedModels.Count == 0 || + settings.GenerateModels || + settings.GenerateSdk || + (x.Operation?.OperationId != null && includedOperationIds.Contains(x.Operation.OperationId)) + ) + .Where(x => + settings.IncludeTags.Length == 0 || + x.HasAnyTag(settings.IncludeTags.ToArray()) + ) + .Where(x => + settings.ExcludeTags.Length == 0 || + !x.HasAnyTag(settings.ExcludeTags.ToArray()) + ) + .SelectMany(x => x.WithAllChildren()) + .Where(x => + !x.IsComponent && includedModels.Count == 0 || (includedModels.Count == 0 || includedModels.Contains(x.ComponentId!)) && - !excludedModels.Contains(x.ComponentId!))) - .SelectMany(x => x.WithAllChildren()) + !excludedModels.Contains(x.ComponentId!) + ) .Distinct() .ToArray() : schemas; filteredSchemas = filteredSchemas .Where(x => !x.HasAllOfTypeForMetadata()) .ToArray(); - + filteringTime.Stop(); - + var computeDataTime = Stopwatch.StartNew(); foreach (var schema in filteredSchemas) { schema.ComputeData(); } - + computeDataTime.Stop(); - + var computeDataClassesTime = Stopwatch.StartNew(); - + var classes = filteredSchemas .Where(x => x is { IsReference: false, IsAnyOfLikeStructure: false }) .Select(x => x.ClassData) @@ -168,7 +177,7 @@ public static Models.Data Prepare( var operations = openApiDocument.GetOperations(settings, filteredSchemas); ModelNameGenerator.ResolveCollisions(operations); - + var filteredOperations = settings.GenerateSdk || settings.GenerateMethods ? operations .Where(operation => @@ -182,7 +191,7 @@ public static Models.Data Prepare( { return true; } - + return (includedOperationIds.Count == 0 || includedOperationIds.Contains(operation.MethodName) || (operation.Operation.OperationId != null && includedOperationIds.Contains(operation.Operation.OperationId))) && @@ -191,7 +200,7 @@ public static Models.Data Prepare( }) .ToArray() : []; - + var methods = filteredOperations .Select(EndPoint.FromSchema) .ToImmutableArray(); @@ -223,8 +232,7 @@ public static Models.Data Prepare( x.Settings.JsonSerializerType == JsonSerializerType.SystemTextJson && x.AnyOfData.HasValue && string.IsNullOrWhiteSpace(x.AnyOfData.Value.Name)) - .Select(x => $"global::{settings.Namespace}.JsonConverters.{x.AnyOfData?.SubType}JsonConverter<{ - string.Join(", ", x.Children + .Select(x => $"global::{settings.Namespace}.JsonConverters.{x.AnyOfData?.SubType}JsonConverter<{string.Join(", ", x.Children .Where(y => y.Hint is Hint.AnyOf or Hint.OneOf or Hint.AllOf) .Select(y => y.TypeData.CSharpTypeWithNullabilityForValueTypes))}>")) // Unix Timestamp converter @@ -232,7 +240,7 @@ public static Models.Data Prepare( $"global::{settings.Namespace}.JsonConverters.UnixTimestampJsonConverter", ]) .ToImmutableArray(); - + var includedTags = allTags .Where(x => (settings.IncludeTags.Length == 0 || @@ -277,7 +285,7 @@ .. includedTags.Select(tag => PropertyData.Default with Converters: []))) .ToArray(); } - + var types = settings.GenerateJsonSerializerContextTypes ? filteredSchemas @@ -289,7 +297,7 @@ .. includedTags.Select(tag => PropertyData.Default with .Select(x => x.First()) .ToImmutableArray() : []; - + classes = classes .Select(x => x with { @@ -302,7 +310,7 @@ .. includedTags.Select(tag => PropertyData.Default with SchemaContext = default!, }) .ToImmutableArray(); - + return new Models.Data( Classes: classes, Enums: enums, diff --git a/src/tests/AutoSDK.GeneratorTests/AutoSDK.GeneratorTests.csproj b/src/tests/AutoSDK.GeneratorTests/AutoSDK.GeneratorTests.csproj new file mode 100644 index 0000000000..692c8a528c --- /dev/null +++ b/src/tests/AutoSDK.GeneratorTests/AutoSDK.GeneratorTests.csproj @@ -0,0 +1,33 @@ + + + + net9.0 + latest + enable + enable + + true + AllMicrosoft + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/tests/AutoSDK.GeneratorTests/GenerationTests.cs b/src/tests/AutoSDK.GeneratorTests/GenerationTests.cs new file mode 100644 index 0000000000..f46ebf2da3 --- /dev/null +++ b/src/tests/AutoSDK.GeneratorTests/GenerationTests.cs @@ -0,0 +1,74 @@ +using AutoSDK.GeneratorTests.Helpers; +using AutoSDK.Models; +using AutoSDK.SourceGenerators; +using FluentAssertions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; + +namespace AutoSDK.GeneratorTests; + +[TestClass] +public partial class GenerationTests +{ + [TestMethod] + public async Task ExcludeModelsAsync() + { + var compilation = CSharpCompilation.Create( + "compilation", + [ + SyntaxFactory.ParseSyntaxTree("[assembly: System.CLSCompliantAttribute(true)]"), + ], + await H.Generators.Tests.Extensions.LatestReferenceAssemblies.Net90.ResolveAsync(null, CancellationToken.None), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var openApiSpec = new CustomAdditionalText( + "petstore.yaml", + new H.Resource("petstore.yaml").AsString()); + openApiSpec.Options.Add("OpenApiSpecification", "true"); + var additionalFiles = new[] { openApiSpec }.ToImmutableArray(); + var optionsProvider = new AdditionalTextOptionsProvider(new() + { + [nameof(Settings.ExcludeModels)] = "PetStore.Pet;", + [nameof(Settings.Namespace)] = "TestNamespace", + [nameof(Settings.ClassName)] = "PetClient", + }); + + var driver = CSharpGeneratorDriver.Create( + new IIncrementalGenerator[] { new SdkGenerator() } + .Select(GeneratorExtensions.AsSourceGenerator) + .ToArray()) + .AddAdditionalTexts(additionalFiles); + driver = driver.WithUpdatedAnalyzerConfigOptions(optionsProvider); + + driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); + var generatedClasses = outputCompilation.SyntaxTrees + .SelectMany(x => x.GetRoot().DescendantNodes()) + .OfType() + .SelectMany(x => x.Members.OfType()) + .ToArray(); + generatedClasses.Where(c => c.Identifier.Text == "Pet").Should().BeEmpty(); + + // If a model is excluded, it should be refered with global usings, because their is no easy way to find the model. + generatedClasses.Select(c => c.Members.OfType()) + .SelectMany(x => x) + .Where(m => + { + var symbole = outputCompilation.GetSemanticModel(m.SyntaxTree).GetSymbolInfo(m.ReturnType).Symbol as INamedTypeSymbol; + if (symbole is null) + { + return false; + } + var listSymbole = symbole.TypeArguments.FirstOrDefault() as INamedTypeSymbol; + var petType = listSymbole?.TypeArguments.FirstOrDefault(); + if (petType is null) + { + return false; + } + return petType.ContainingNamespace.Name.StartsWith("TestNamespace") && + petType.Name == "Pet"; + }) + .Should().BeEmpty(); + } +} diff --git a/src/tests/AutoSDK.GeneratorTests/Helpers/AdditionalTextOptionsProvider.cs b/src/tests/AutoSDK.GeneratorTests/Helpers/AdditionalTextOptionsProvider.cs new file mode 100644 index 0000000000..f39c92c73c --- /dev/null +++ b/src/tests/AutoSDK.GeneratorTests/Helpers/AdditionalTextOptionsProvider.cs @@ -0,0 +1,45 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace AutoSDK.GeneratorTests.Helpers; + +public class AdditionalTextOptionsProvider : AnalyzerConfigOptionsProvider +{ + private readonly Dictionary globalOptions = []; + + public AdditionalTextOptionsProvider(Dictionary? globalOptions = null) + { + if (globalOptions is not null && globalOptions.Count != 0) + { + this.globalOptions = globalOptions. + ToDictionary( + k => $"build_property.AutoSDK_{k.Key}", + v => v.Value + ); + } + } + public override AnalyzerConfigOptions GlobalOptions => new DictionaryAnalyzerConfigOptions(globalOptions); + + // + // Summary: + // Gets options for a given file. + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) + { + if (textFile is not CustomAdditionalText customTextFile) + { + throw new ArgumentException("Invalid type", nameof(textFile)); + } + return new DictionaryAnalyzerConfigOptions( + customTextFile.Options + .ToDictionary( + k => $"build_metadata.AdditionalFiles.AutoSDK_{k.Key}", + v => v.Value + ) + ); + } + + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) + { + return new DictionaryAnalyzerConfigOptions([]); + } +} diff --git a/src/tests/AutoSDK.GeneratorTests/Helpers/CSharpIncrementalSourceGeneratorVerifier.cs b/src/tests/AutoSDK.GeneratorTests/Helpers/CSharpIncrementalSourceGeneratorVerifier.cs new file mode 100644 index 0000000000..5ecf6a43a6 --- /dev/null +++ b/src/tests/AutoSDK.GeneratorTests/Helpers/CSharpIncrementalSourceGeneratorVerifier.cs @@ -0,0 +1,41 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using System.Collections.Immutable; + +namespace AutoSDK.GeneratorTests.Helpers; + +internal static class CSharpIncrementalSourceGeneratorVerifier + where TSourceGenerator : IIncrementalGenerator, new() +{ + public class Test : CSharpSourceGeneratorTest + { + public Test() + { + } + + protected override CompilationOptions CreateCompilationOptions() + { + var compilationOptions = base.CreateCompilationOptions(); + return compilationOptions.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions.SetItems(GetNullableWarningsFromCompiler())); + } + + public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.Default; + + private static ImmutableDictionary GetNullableWarningsFromCompiler() + { + string[] args = { "/warnaserror:nullable" }; + var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); + var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; + + return nullableWarnings; + } + + protected override ParseOptions CreateParseOptions() + { + return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion); + } + } +} diff --git a/src/tests/AutoSDK.GeneratorTests/Helpers/CustomAdditionalText.cs b/src/tests/AutoSDK.GeneratorTests/Helpers/CustomAdditionalText.cs new file mode 100644 index 0000000000..f5ef81aa7b --- /dev/null +++ b/src/tests/AutoSDK.GeneratorTests/Helpers/CustomAdditionalText.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace AutoSDK.GeneratorTests.Helpers; + +public class CustomAdditionalText(string path, string text) : AdditionalText +{ + public string Text { get; } = text; + + public override string Path { get; } = path; + + public override SourceText GetText(CancellationToken cancellationToken = default) + { + return SourceText.From(Text); + } + + public Dictionary Options { get; } = new Dictionary(); +} diff --git a/src/tests/AutoSDK.GeneratorTests/Helpers/DictionaryAnalyzerConfigOptions.cs b/src/tests/AutoSDK.GeneratorTests/Helpers/DictionaryAnalyzerConfigOptions.cs new file mode 100644 index 0000000000..6bb37f48f5 --- /dev/null +++ b/src/tests/AutoSDK.GeneratorTests/Helpers/DictionaryAnalyzerConfigOptions.cs @@ -0,0 +1,34 @@ +using Microsoft.CodeAnalysis.Diagnostics; + +namespace AutoSDK.GeneratorTests.Helpers; + +/// +/// +/// +public sealed class DictionaryAnalyzerConfigOptions : AnalyzerConfigOptions +{ + private Dictionary Properties { get; } + + /// + /// + /// + /// + public DictionaryAnalyzerConfigOptions(Dictionary properties) + { + Properties = properties; + } + + /// + /// + /// + /// + /// + /// + public override bool TryGetValue(string key, out string value) + { + var result = Properties.TryGetValue(key, out var newValue); + value = newValue ?? string.Empty; + + return result; + } +} diff --git a/src/tests/AutoSDK.GeneratorTests/MSTestSettings.cs b/src/tests/AutoSDK.GeneratorTests/MSTestSettings.cs new file mode 100644 index 0000000000..aaf278c844 --- /dev/null +++ b/src/tests/AutoSDK.GeneratorTests/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/src/tests/AutoSDK.UnitTests/DataTests.cs b/src/tests/AutoSDK.UnitTests/DataTests.cs index 844b1f3c89..ece630628e 100644 --- a/src/tests/AutoSDK.UnitTests/DataTests.cs +++ b/src/tests/AutoSDK.UnitTests/DataTests.cs @@ -1,5 +1,6 @@ using AutoSDK.Generation; using AutoSDK.Naming.Methods; +using System.Collections.Immutable; namespace AutoSDK.UnitTests; @@ -62,4 +63,26 @@ DefaultSettings with })), resourceName: Path.GetFileNameWithoutExtension(resourceName)); } + + [TestMethod] + public void ExcludeModels() + { + var processedSchema = Data.Prepare( + ( + new H.Resource("petstore.yaml").AsString(), + DefaultSettings with + { + GenerateJsonSerializerContextTypes = true, + MethodNamingConvention = MethodNamingConvention.OperationIdWithDots, + IgnoreOpenApiErrors = true, + ExcludeModels = new[] { "Pet" }.ToImmutableArray(), + + } + )); + + var result = processedSchema.Classes + .Where(x => x.ClassName.Contains("Pet")) + .ToArray(); + Assert.IsEmpty(result); + } } \ No newline at end of file