diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Visitors/NameVisitor.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Visitors/NameVisitor.cs index 1cc1df07a93c..102be1b2b259 100644 --- a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Visitors/NameVisitor.cs +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Visitors/NameVisitor.cs @@ -40,6 +40,16 @@ internal class NameVisitor : ScmLibraryVisitor "PrivateEndpointConnectionListResult", "PrivateLinkResourceListResult" }; + private static readonly (string Source, string Replacement)[] _acronymRenamingRules = + [ + ("Ipv4", "IPv4"), + ("Ipv6", "IPv6"), + ("IpV4", "IPv4"), + ("IpV6", "IPv6"), + ("Ip", "IP"), + ("Db", "DB"), + ("Os", "OS") + ]; protected override EnumProvider? PreVisitEnum(InputEnumType enumType, EnumProvider? type) { @@ -53,7 +63,13 @@ internal class NameVisitor : ScmLibraryVisitor var newName = $"{ManagementClientGenerator.Instance.TypeFactory.ResourceProviderName}{enumType.Name}"; type.Update(name: newName); } - return base.PreVisitEnum(enumType, type); + + type = base.PreVisitEnum(enumType, type); + if (type is not null) + { + type.Update(name: NormalizeAcronymCasing(type.Name)); + } + return type; } protected override ModelProvider? PreVisitModel(InputModelType model, ModelProvider? type) @@ -101,6 +117,8 @@ internal class NameVisitor : ScmLibraryVisitor type.Update(name: newName); } } + + type.Update(name: NormalizeAcronymCasing(type.Name)); return type; } @@ -110,7 +128,12 @@ internal class NameVisitor : ScmLibraryVisitor DoPreVisitPropertyForUrlPropertyName(property, propertyProvider); DoPreVisitPropertyForTimePropertyName(property, propertyProvider); DoPreVisitPropertyNameRenaming(property, propertyProvider); - return base.PreVisitProperty(property, propertyProvider); + propertyProvider = base.PreVisitProperty(property, propertyProvider); + if (propertyProvider is not null) + { + propertyProvider.Update(name: NormalizeAcronymCasing(propertyProvider.Name)); + } + return propertyProvider; } private void DoPreVisitPropertyForResourceTypeName(InputProperty property, PropertyProvider? propertyProvider) @@ -228,6 +251,35 @@ private bool TryTransformUrlToUri(string name, [MaybeNullWhen(false)] out string return false; } + internal static string NormalizeAcronymCasing(string name) + { + char[]? normalizedName = null; + for (int index = 0; index < name.Length - 1; index++) + { + foreach (var rule in _acronymRenamingRules) + { + if (!name.AsSpan(index).StartsWith(rule.Source, StringComparison.Ordinal)) + { + continue; + } + + int boundaryIndex = index + rule.Source.Length; + // Ensure the acronym ends at a word boundary. + if (boundaryIndex < name.Length && !char.IsUpper(name[boundaryIndex])) + { + continue; + } + + normalizedName ??= name.ToCharArray(); + rule.Replacement.CopyTo(0, normalizedName, index, rule.Replacement.Length); + index = boundaryIndex - 1; + break; + } + } + + return normalizedName is null ? name : new string(normalizedName); + } + /// /// Checks the input type (rather than the C# type) to determine if it represents a date/time, /// so the rename logic works regardless of what C# type the downstream generator maps it to diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/test/NameVisitorTests.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/test/NameVisitorTests.cs index 9488f693bbe6..bd8711e449ba 100644 --- a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/test/NameVisitorTests.cs +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/test/NameVisitorTests.cs @@ -4,6 +4,7 @@ using Azure.Generator.Management; using Azure.Generator.Management.Tests.Common; using Azure.Generator.Management.Tests.TestHelpers; +using Azure.Generator.Management.Visitors; using Microsoft.TypeSpec.Generator.Input; using NUnit.Framework; @@ -13,53 +14,112 @@ internal class NameVisitorTests { private const string TestClientName = "TestClient"; + [TestCase("IpAddress", "IPAddress")] + [TestCase("CosmosDbAccount", "CosmosDBAccount")] + [TestCase("OsProfile", "OSProfile")] + [TestCase("IpDbOsIpAddressDb", "IPDBOSIPAddressDB")] + [TestCase("IPAddressCosmosDBOSProfile", "IPAddressCosmosDBOSProfile")] + [TestCase("Oslo", "Oslo")] + [TestCase("Ipsum", "Ipsum")] + [TestCase("Osmosis", "Osmosis")] + [TestCase("osmosis", "osmosis")] + [TestCase("dbz", "dbz")] + [TestCase("Ipv4Address", "IPv4Address")] + [TestCase("Ipv6Address", "IPv6Address")] + [TestCase("Ipv4AddressIpv6", "IPv4AddressIPv6")] + [TestCase("Ipv4", "IPv4")] + [TestCase("Ipv6", "IPv6")] + [TestCase("Ipv4address", "Ipv4address")] + [TestCase("Ipv42Address", "Ipv42Address")] + [TestCase("IPV4Address", "IPV4Address")] + [TestCase("IPV6Address", "IPV6Address")] + [TestCase("IpV4Address", "IPv4Address")] + [TestCase("IpV6Address", "IPv6Address")] + public void TestNormalizeCompleteAcronymWords(string inputName, string expectedName) + { + Assert.That(NameVisitor.NormalizeAcronymCasing(inputName), Is.EqualTo(expectedName)); + } + + [TestCase("IpAddress", "IPAddress")] + [TestCase("CosmosDbAccount", "CosmosDBAccount")] + [TestCase("OsProfile", "OSProfile")] + [TestCase("IpDbOsIpAddressDb", "IPDBOSIPAddressDB")] + [TestCase("IPAddressCosmosDBOSProfile", "IPAddressCosmosDBOSProfile")] + [TestCase("Ipv4AddressIpv6", "IPv4AddressIPv6")] + public void TestNormalizeModelAcronymCasing(string inputName, string expectedName) + { + var model = InputFactory.Model(inputName); + var client = CreateClient(model); + var plugin = ManagementMockHelpers.LoadMockPlugin(inputModels: () => [model], clients: () => [client]); + + var type = plugin.Object.TypeFactory.CreateModel(model); + + Assert.That(type?.Name, Is.EqualTo(expectedName)); + } + + [TestCase("IpAddress", "IPAddress")] + [TestCase("CosmosDbAccount", "CosmosDBAccount")] + [TestCase("OsProfile", "OSProfile")] + [TestCase("IpDbOsIpAddressDb", "IPDBOSIPAddressDB")] + [TestCase("IPAddressCosmosDBOSProfile", "IPAddressCosmosDBOSProfile")] + [TestCase("Ipv4Address", "IPv4Address")] + [TestCase("Ipv6Address", "IPv6Address")] + public void TestNormalizePropertyAcronymCasing(string inputName, string expectedName) + { + var modelProperty = InputFactory.Property(inputName, InputPrimitiveType.String, serializedName: "testName", isRequired: true); + var model = InputFactory.Model("TestModel", properties: [modelProperty]); + var client = CreateClient(model); + var plugin = ManagementMockHelpers.LoadMockPlugin(inputModels: () => [model], clients: () => [client]); + + var type = plugin.Object.TypeFactory.CreateModel(model); + + Assert.That(type?.Properties[0].Name, Is.EqualTo(expectedName)); + } + + [TestCase(false, "CosmosDbOsIpKind", "CosmosDBOSIPKind")] + [TestCase(true, "IpDbOsValue", "IPDBOSValue")] + public void TestNormalizeEnumAndUnionTypeAcronymCasing(bool isExtensible, string inputName, string expectedName) + { + var inputEnum = InputFactory.StringEnum(inputName, [("IpValue", "ip")], isExtensible: isExtensible); + var client = CreateClient(inputEnum); + var plugin = ManagementMockHelpers.LoadMockPlugin(inputEnums: () => [inputEnum], clients: () => [client]); + + var type = plugin.Object.TypeFactory.CreateEnum(inputEnum); + + Assert.That(type?.Name, Is.EqualTo(expectedName)); + } + [Test] public void TestTransformUrlToUri() { - const string testModelName = "TestModelUrl"; - const string testPropertyName = "TestPropertyUrl"; + const string testModelName = "IpModelUrl"; + const string testPropertyName = "DbPropertyUrl"; var modelProperty = InputFactory.Property(testPropertyName, InputPrimitiveType.String, serializedName: "testName", isRequired: true); var model = InputFactory.Model(testModelName, properties: [modelProperty]); - var responseType = InputFactory.OperationResponse(statusCodes: [200], bodytype: model); - var testNameParameter = InputFactory.MethodParameter("testName", InputPrimitiveType.String, location: InputRequestLocation.Path); - var operation = InputFactory.Operation(name: "get", responses: [responseType], parameters: [testNameParameter], path: "/providers/a/test/{testName}", decorators: []); - - var client = InputFactory.Client( - TestClientName, - methods: [InputFactory.BasicServiceMethod("Get", operation, parameters: [testNameParameter])], - crossLanguageDefinitionId: $"Test.{TestClientName}", - decorators: []); + var client = CreateClient(model); var plugin = ManagementMockHelpers.LoadMockPlugin(inputModels: () => [model], clients: () => [client]); // PreVisitModel is called during the model creation var type = plugin.Object.TypeFactory.CreateModel(model); - Assert.That(type?.Name, Is.EqualTo(testModelName.Replace("Url", "Uri"))); - Assert.That(type?.Properties[0].Name, Is.EqualTo(testPropertyName.Replace("Url", "Uri"))); + Assert.That(type?.Name, Is.EqualTo("IPModelUri")); + Assert.That(type?.Properties[0].Name, Is.EqualTo("DBPropertyUri")); } [Test] public void TestTransformTimePropertyName() { const string testModelName = "TestModel"; - const string testPropertyName = "StartTime"; + const string testPropertyName = "OsTime"; var modelProperty = InputFactory.Property(testPropertyName, InputPrimitiveType.PlainDate, serializedName: "testName", isRequired: true); var model = InputFactory.Model(testModelName, properties: [modelProperty]); - var responseType = InputFactory.OperationResponse(statusCodes: [200], bodytype: model); - var testNameParameter = InputFactory.MethodParameter("testName", InputPrimitiveType.String, location: InputRequestLocation.Path); - var operation = InputFactory.Operation(name: "get", responses: [responseType], parameters: [testNameParameter], path: "/providers/a/test/{testName}", decorators: []); - - var client = InputFactory.Client( - TestClientName, - methods: [InputFactory.BasicServiceMethod("Get", operation, parameters: [testNameParameter])], - crossLanguageDefinitionId: $"Test.{TestClientName}", - decorators: []); + var client = CreateClient(model); var plugin = ManagementMockHelpers.LoadMockPlugin(inputModels: () => [model], clients: () => [client]); // PreVisitModel is called during the model creation var type = plugin.Object.TypeFactory.CreateModel(model); - Assert.That(type?.Properties[0].Name, Is.EqualTo(testPropertyName.Replace("Time", "On"))); + Assert.That(type?.Properties[0].Name, Is.EqualTo("OSOn")); } [Test] @@ -68,21 +128,13 @@ public void TestPrependResourceProviderNameForModel() var skuModelName = "Sku"; var modelProperty = InputFactory.Property("TestName", InputPrimitiveType.String, serializedName: "testName", isRequired: true); var model = InputFactory.Model(skuModelName, properties: [modelProperty]); - var responseType = InputFactory.OperationResponse(statusCodes: [200], bodytype: model); - var testNameParameter = InputFactory.MethodParameter("testName", InputPrimitiveType.String, location: InputRequestLocation.Path); - var operation = InputFactory.Operation(name: "get", responses: [responseType], parameters: [testNameParameter], path: "/providers/a/test/{testName}", decorators: []); - - var client = InputFactory.Client( - TestClientName, - methods: [InputFactory.BasicServiceMethod("Get", operation, parameters: [testNameParameter])], - crossLanguageDefinitionId: $"Test.{TestClientName}", - decorators: []); + var client = CreateClient(model); - var plugin = ManagementMockHelpers.LoadMockPlugin(inputModels: () => [model], clients: () => [client]); + var plugin = ManagementMockHelpers.LoadMockPlugin(inputModels: () => [model], clients: () => [client], primaryNamespace: "IpSamples"); // PreVisitModel is called during the model creation var type = plugin.Object.TypeFactory.CreateModel(model); - var resourceProviderName = ManagementClientGenerator.Instance.TypeFactory.ResourceProviderName; + const string resourceProviderName = "IPSamples"; var updatedSkuModelName = $"{resourceProviderName}{skuModelName}"; Assert.That(updatedSkuModelName, Is.EqualTo(type?.Name)); Assert.That($"{resourceProviderName}{skuModelName}", Is.EqualTo(type!.Constructors[0].Signature.Name)); @@ -90,7 +142,7 @@ public void TestPrependResourceProviderNameForModel() Assert.That(serializationProvider, Is.Not.Null); Assert.That(updatedSkuModelName, Is.EqualTo(serializationProvider!.Name)); var deserializationMethod = serializationProvider.Methods.SingleOrDefault(m => m.Signature.Name.StartsWith("Deserialize")); - Assert.That(deserializationMethod!.Signature.Name, Is.EqualTo("DeserializeSamplesSku")); + Assert.That(deserializationMethod!.Signature.Name, Is.EqualTo("DeserializeIPSamplesSku")); } [Test] @@ -98,15 +150,7 @@ public void TestPrependResourceProviderNameForEnum() { var enumName = "PrivateEndpointServiceConnectionStatus"; var stringEnum = InputFactory.StringEnum(enumName, [("a", "a"), ("b", "b")]); - var responseType = InputFactory.OperationResponse(statusCodes: [200], bodytype: stringEnum); - var testNameParameter = InputFactory.MethodParameter("testName", InputPrimitiveType.String, location: InputRequestLocation.Path); - var operation = InputFactory.Operation(name: "get", responses: [responseType], parameters: [testNameParameter], path: "/providers/a/test/{testName}", decorators: []); - - var client = InputFactory.Client( - TestClientName, - methods: [InputFactory.BasicServiceMethod("Get", operation, parameters: [testNameParameter])], - crossLanguageDefinitionId: $"Test.{TestClientName}", - decorators: []); + var client = CreateClient(stringEnum); var plugin = ManagementMockHelpers.LoadMockPlugin(inputEnums: () => [stringEnum], clients: () => [client]); @@ -124,15 +168,7 @@ public void TestTransformEtagToETag() const string testPropertyName = "Etag"; var modelProperty = InputFactory.Property(testPropertyName, InputPrimitiveType.String, serializedName: "etag", isRequired: true); var model = InputFactory.Model(testModelName, properties: [modelProperty]); - var responseType = InputFactory.OperationResponse(statusCodes: [200], bodytype: model); - var testNameParameter = InputFactory.MethodParameter("testName", InputPrimitiveType.String, location: InputRequestLocation.Path); - var operation = InputFactory.Operation(name: "get", responses: [responseType], parameters: [testNameParameter], path: "/providers/a/test/{testName}", decorators: []); - - var client = InputFactory.Client( - TestClientName, - methods: [InputFactory.BasicServiceMethod("Get", operation, parameters: [testNameParameter])], - crossLanguageDefinitionId: $"Test.{TestClientName}", - decorators: []); + var client = CreateClient(model); var plugin = ManagementMockHelpers.LoadMockPlugin(inputModels: () => [model], clients: () => [client]); @@ -151,5 +187,17 @@ public void TestPatchModelRenameRespectsResourceDerivedClientNameOverride() Assert.That(type?.Name, Is.EqualTo("OperationSpecificUpdateShape")); } + + private static InputClient CreateClient(InputType responseBodyType) + { + var response = InputFactory.OperationResponse(statusCodes: [200], bodytype: responseBodyType); + var testNameParameter = InputFactory.MethodParameter("testName", InputPrimitiveType.String, location: InputRequestLocation.Path); + var operation = InputFactory.Operation(name: "get", responses: [response], parameters: [testNameParameter], path: "/providers/a/test/{testName}", decorators: []); + return InputFactory.Client( + TestClientName, + methods: [InputFactory.BasicServiceMethod("Get", operation, parameters: [testNameParameter])], + crossLanguageDefinitionId: $"Test.{TestClientName}", + decorators: []); + } } }