diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Shared/StringExtensions.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Shared/StringExtensions.cs new file mode 100644 index 00000000000..b9b17932ecf --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Shared/StringExtensions.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis.CSharp; + +namespace Microsoft.Generator.CSharp +{ + internal static class StringExtensions + { + private static bool IsWordSeparator(char c) => !SyntaxFacts.IsIdentifierPartCharacter(c) || c == '_'; + private static readonly Regex HumanizedCamelCaseRegex = new Regex(@"([A-Z])", RegexOptions.Compiled); + + [return: NotNullIfNotNull("name")] + public static string ToCleanName(this string name, bool isCamelCase = true) + { + if (string.IsNullOrEmpty(name)) + { + return name; + } + StringBuilder nameBuilder = new StringBuilder(); + + int i = 0; + + if (char.IsDigit(name[0])) + { + nameBuilder.Append("_"); + } + else + { + while (!SyntaxFacts.IsIdentifierStartCharacter(name[i])) + { + i++; + } + } + + bool upperCase = false; + int firstWordLength = 1; + for (; i < name.Length; i++) + { + var c = name[i]; + if (IsWordSeparator(c)) + { + upperCase = true; + continue; + } + + if (nameBuilder.Length == 0 && isCamelCase) + { + c = char.ToUpper(c); + upperCase = false; + } + else if (nameBuilder.Length < firstWordLength && !isCamelCase) + { + c = char.ToLower(c); + upperCase = false; + // grow the first word length when this letter follows by two other upper case letters + // this happens in OSProfile, where OS is the first word + if (i + 2 < name.Length && char.IsUpper(name[i + 1]) && (char.IsUpper(name[i + 2]) || IsWordSeparator(name[i + 2]))) + firstWordLength++; + // grow the first word length when this letter follows by another upper case letter and an end of the string + // this happens when the string only has one word, like OS, DNS + if (i + 2 == name.Length && char.IsUpper(name[i + 1])) + firstWordLength++; + } + + if (upperCase) + { + c = char.ToUpper(c); + upperCase = false; + } + + nameBuilder.Append(c); + } + + return nameBuilder.ToString(); + } + + [return: NotNullIfNotNull(nameof(name))] + public static string ToVariableName(this string name) => ToCleanName(name, isCamelCase: false); + + public static GetPathPartsEnumerator GetFormattableStringFormatParts(string? format) => new GetPathPartsEnumerator(format); + + public static GetPathPartsEnumerator GetFormattableStringFormatParts(ReadOnlySpan format) => new GetPathPartsEnumerator(format); + + public ref struct GetPathPartsEnumerator + { + private ReadOnlySpan _path; + public Part Current { get; private set; } + + public GetPathPartsEnumerator(ReadOnlySpan format) + { + _path = format; + Current = default; + } + + public readonly GetPathPartsEnumerator GetEnumerator() => this; + + public bool MoveNext() + { + var span = _path; + if (span.Length == 0) + { + return false; + } + + var separatorIndex = span.IndexOfAny('{', '}'); + + if (separatorIndex == -1) + { + Current = new Part(span, true); + _path = ReadOnlySpan.Empty; + return true; + } + + var separator = span[separatorIndex]; + // Handle {{ and }} escape sequences + if (separatorIndex + 1 < span.Length && span[separatorIndex + 1] == separator) + { + Current = new Part(span.Slice(0, separatorIndex + 1), true); + _path = span.Slice(separatorIndex + 2); + return true; + } + + var isLiteral = separator == '{'; + + // Skip empty literals + if (isLiteral && separatorIndex == 0 && span.Length > 1) + { + separatorIndex = span.IndexOf('}'); + if (separatorIndex == -1) + { + Current = new Part(span.Slice(1), true); + _path = ReadOnlySpan.Empty; + return true; + } + + Current = new Part(span.Slice(1, separatorIndex - 1), false); + } + else + { + Current = new Part(span.Slice(0, separatorIndex), isLiteral); + } + + _path = span.Slice(separatorIndex + 1); + return true; + } + + public readonly ref struct Part + { + public Part(ReadOnlySpan span, bool isLiteral) + { + Span = span; + IsLiteral = isLiteral; + } + + public ReadOnlySpan Span { get; } + public bool IsLiteral { get; } + + public void Deconstruct(out ReadOnlySpan span, out bool isLiteral) + { + span = Span; + isLiteral = IsLiteral; + } + + public void Deconstruct(out ReadOnlySpan span, out bool isLiteral, out int argumentIndex) + { + span = Span; + isLiteral = IsLiteral; + + if (IsLiteral) + { + argumentIndex = -1; + } + else + { + var formatSeparatorIndex = span.IndexOf(':'); + var indexSpan = formatSeparatorIndex == -1 ? span : span.Slice(0, formatSeparatorIndex); + argumentIndex = int.Parse(indexSpan); + } + } + } + } + + /// + /// Determines if the given name is a C# keyword. + /// + /// The string name of the keyword. + /// true if the string is a csharp keyword. + public static bool IsCSharpKeyword(string? name) + { + if (name == null) + { + return false; + } + + SyntaxKind kind = SyntaxFacts.GetKeywordKind(name); + if (kind == SyntaxKind.None) + { + kind = SyntaxFacts.GetContextualKeywordKind(name); + } + + return SyntaxFacts.IsKeywordKind(kind); + } + + public static string ToApiVersionMemberName(this string version) + { + var sb = new StringBuilder("V"); + int startIndex = version.StartsWith("v", StringComparison.InvariantCultureIgnoreCase) ? 1 : 0; + + for (int i = startIndex; i < version.Length; i++) + { + char c = version[i]; + if (c == '-' || c == '.') + { + sb.Append('_'); + } + else + { + sb.Append(c); + } + } + + return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(sb.ToString()); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Expressions/FormattableStringExpression.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Expressions/FormattableStringExpression.cs index 09b70d88ef2..5b16c354b04 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Expressions/FormattableStringExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Expressions/FormattableStringExpression.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Input; namespace Microsoft.TypeSpec.Generator.Expressions { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/OutputLibrary.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/OutputLibrary.cs index 13b27c7c858..1f6f884db97 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/OutputLibrary.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/OutputLibrary.cs @@ -51,7 +51,7 @@ private static TypeProvider[] BuildModels() foreach (var inputModel in input.Models) { var outputModel = CodeModelPlugin.Instance.TypeFactory.CreateModel(inputModel); - if (outputModel != null) + if (outputModel != null && outputModel is not SystemObjectProvider) { models.Add(outputModel); var unknownVariant = inputModel.DiscriminatedSubtypes.Values.FirstOrDefault(m => m.IsUnknownDiscriminatorModel); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 97be4ad13a0..c43419da962 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -57,7 +57,7 @@ private IReadOnlyList BuildDerivedModels() // add discriminated subtypes foreach (var subtype in _inputModel.DiscriminatedSubtypes) { - var model = CodeModelPlugin.Instance.TypeFactory.CreateModel(subtype.Value); + var model = CodeModelPlugin.Instance.TypeFactory.CreateModel(subtype.Value) as ModelProvider; if (model != null) { derivedModels.Add(model); @@ -67,7 +67,7 @@ private IReadOnlyList BuildDerivedModels() // add derived models foreach (var derivedModel in _inputModel.DerivedModels) { - var model = CodeModelPlugin.Instance.TypeFactory.CreateModel(derivedModel); + var model = CodeModelPlugin.Instance.TypeFactory.CreateModel(derivedModel) as ModelProvider; if (model != null) { derivedModels.Add(model); @@ -76,7 +76,23 @@ private IReadOnlyList BuildDerivedModels() return [.. derivedModels]; } - internal override TypeProvider? BaseTypeProvider => BaseModelProvider; + + internal override TypeProvider? BaseTypeProvider + { + get + { + if (_baseTypeProvider?.Value is ModelProvider modelProvider) + { + return modelProvider; + } + else if (_baseTypeProvider?.Value is SystemObjectProvider systemObjectProvider) + { + return systemObjectProvider; + } + + return null; + } + } public ModelProvider? BaseModelProvider => _baseModelProvider ??= (_baseTypeProvider?.Value is ModelProvider baseModelProvider ? baseModelProvider : null); @@ -93,7 +109,7 @@ protected override string BuildNamespace() => string.IsNullOrEmpty(_inputModel.N protected override CSharpType? GetBaseType() { - return BaseModelProvider?.Type; + return BaseTypeProvider?.Type; } protected override TypeProvider[] BuildSerializationProviders() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs index 294a3683028..27a721fdb8a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs @@ -52,6 +52,13 @@ protected PropertyProvider() internal static bool TryCreate(InputModelProperty inputProperty, TypeProvider enclosingType, [NotNullWhen(true)] out PropertyProvider? property) { + var inputPropertyType = inputProperty.Type as InputModelType; + if (inputPropertyType != null && CodeModelPlugin.Instance.TypeFactory.TryGetPropertyTypeReplacement(inputPropertyType, out var replacement)) + { + property = new PropertyProvider(inputProperty, replacement.Type, enclosingType); + return true; + } + var type = CodeModelPlugin.Instance.TypeFactory.CreateCSharpType(inputProperty.Type); if (type == null) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectProvider.cs new file mode 100644 index 00000000000..c59f18919c9 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectProvider.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Statements; + +namespace Microsoft.Generator.CSharp.Providers +{ + public class SystemObjectProvider : TypeProvider + { + private readonly Type _type; + private const string InitializationCtorAttributeName = "InitializationConstructorAttribute"; + private const string SerializationCtorAttributeName = "SerializationConstructorAttribute"; + + public SystemObjectProvider(Type type) : base() + { + _type = type; + } + + protected override string BuildName() => _type.Name; + + protected override string BuildRelativeFilePath() => throw new InvalidOperationException("This type should not be writing in generation"); + + protected override ConstructorProvider[] BuildConstructors() + { + var initializationCtor = GetCtor(_type, InitializationCtorAttributeName); + var serializationCtor = GetCtor(_type, SerializationCtorAttributeName); + + return [BuildConstructor(initializationCtor), BuildConstructor(serializationCtor)]; + } + + private ConstructorProvider BuildConstructor(ConstructorInfo ctor) + { + var parameters = new List(); + foreach (var param in ctor.GetParameters()) + { + var parameter = new ParameterProvider(param.Name!, $"The {param.Name}", param.ParameterType); + parameters.Add(parameter); + } + + // we should only add initializers when there is a corresponding parameter + List arguments = new List(); + foreach (var property in Properties) + { + var parameter = parameters.FirstOrDefault(p => p.Name == property.Name.ToVariableName()); + if (parameter is not null) + { + arguments.Add(parameter); + } + } + + var modifiers = ctor.IsFamily ? MethodSignatureModifiers.Protected : MethodSignatureModifiers.Public; + var signature = new ConstructorSignature(Type, null, modifiers, parameters, Initializer: new ConstructorInitializer(false, arguments)); + + return new ConstructorProvider(signature, MethodBodyStatement.Empty, this); + } + + private static ConstructorInfo GetCtor(Type type, string attributeType) + { + if (TryGetCtor(type, attributeType, out var ctor)) + return ctor; + + throw new InvalidOperationException($"{attributeType} ctor was not found for {type.Name}"); + } + + private static bool TryGetCtor(Type type, string attributeType, [MaybeNullWhen(false)] out ConstructorInfo result) + { + foreach (var ctor in type.GetConstructors(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.CreateInstance)) + { + if (ctor.GetCustomAttributes().FirstOrDefault(a => a.GetType().Name == attributeType) != null) + { + result = ctor; + return true; + } + } + + result = null; + return false; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/StringExtensions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/StringExtensions.cs index 4cdf8c091b8..fce99311339 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/StringExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/StringExtensions.cs @@ -8,9 +8,13 @@ using System.Text.RegularExpressions; using Microsoft.CodeAnalysis.CSharp; +<<<<<<<< HEAD:packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/Utilities/StringExtensions.cs +namespace Microsoft.Generator.CSharp.Input +======== namespace Microsoft.TypeSpec.Generator +>>>>>>>> origin/main:packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/StringExtensions.cs { - internal static class StringExtensions + public static class StringExtensions { private static bool IsWordSeparator(char c) => !SyntaxFacts.IsIdentifierPartCharacter(c) || c == '_'; private static readonly Regex HumanizedCamelCaseRegex = new Regex(@"([A-Z])", RegexOptions.Compiled); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs index c94e8dac02f..16b58eb2b4f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Primitives; @@ -43,6 +44,20 @@ protected internal TypeFactory() public CSharpType? CreateCSharpType(InputType inputType) { + var inputModelType = inputType as InputModelType; + + if (inputModelType is not null) + { + if (TryGetTypeReplacement(inputModelType, out var typeReplacement)) + { + return typeReplacement.Type; + } + else if (TryGetPropertyTypeReplacement(inputModelType, out var propertyReplacement)) + { + return propertyReplacement.Type; + } + } + if (TypeCache.TryGetValue(inputType, out var type)) { return type; @@ -141,8 +156,13 @@ protected internal TypeFactory() /// /// The to convert. /// An instance of . - public ModelProvider? CreateModel(InputModelType model) + public TypeProvider? CreateModel(InputModelType model) { + if (TryGetTypeReplacement(model, out var replacement)) + { + return replacement; + } + if (CSharpToModelProvider.TryGetValue(model, out var modelProvider)) return modelProvider; @@ -157,6 +177,19 @@ protected internal TypeFactory() return modelProvider; } + + public virtual bool TryGetTypeReplacement(InputModelType inputModelType, [NotNullWhen(true)] out SystemObjectProvider? replacement) + { + replacement = null; + return false; + } + + public virtual bool TryGetPropertyTypeReplacement(InputModelType inputModelType, [NotNullWhen(true)] out SystemObjectProvider? replacement) + { + replacement = null; + return false; + } + protected virtual ModelProvider? CreateModelCore(InputModelType model) => new ModelProvider(model); /// diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/TestTypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/TestTypeFactory.cs index b2ee341ec31..1e20dcfdb7a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/TestTypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/TestTypeFactory.cs @@ -5,5 +5,15 @@ namespace Microsoft.TypeSpec.Generator.Tests { public class TestTypeFactory : TypeFactory { + // TODO: create some custom types to replace the existing models + public override bool TryGetPropertyTypeReplacement(InputModelType inputModelType, [NotNullWhen(true)] out SystemObjectProvider? replacement) + { + return base.TryGetPropertyTypeReplacement(inputModelType, out replacement); + } + + public override bool TryGetTypeReplacement(InputModelType inputModelType, [NotNullWhen(true)] out SystemObjectProvider? replacement) + { + return base.TryGetTypeReplacement(inputModelType, out replacement); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/StringExtensionsTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/StringExtensionsTests.cs index 8ed62ac48c7..ca165dcfcdc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/StringExtensionsTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/StringExtensionsTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Input; using NUnit.Framework; namespace Microsoft.TypeSpec.Generator.Tests.Utilities