Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ internal class NameVisitor : ScmLibraryVisitor
"PrivateEndpointConnectionListResult",
"PrivateLinkResourceListResult"
};
private static readonly (string Source, string Replacement)[] _acronymRenamingRules =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if this should instead go in the base emitter - https://github.com/microsoft/typespec/tree/main/packages/http-client-csharp

These don't seem to be specific to mgmt.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest we put these in unbranded. This is a dotnet rule not an azure rule.

@weikanglim Wei Lim (weikanglim) Aug 12, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds perfectly reasonable. 😃

TBH, part of me wasn't entirely sure where we wanted to enforce "naming conventions," since that can also be an organizational concern.

I was also slightly confused by the presence of linters enforcing the same rules. I had the impression we were thinking of linting public TypeSpec rather than changing its generation rules.

Are we ultimately thinking of removing the linter rules in favor of improvements in the generator? I suspect otherwise the rules could still fire even when the resulting naming is actually valid.

All-in-all, I'm in favor of making the change wherever it makes the most sense, and I personally much prefer having this in base TypeSpec. I mostly tossed this PR out based on my limited understanding of where we wanted this behavior to live. 🗡️

[
("Ipv4", "IPv4"),
("Ipv6", "IPv6"),
("IpV4", "IPv4"),
("IpV6", "IPv6"),
("Ip", "IP"),
("Db", "DB"),
("Os", "OS")
];

protected override EnumProvider? PreVisitEnum(InputEnumType enumType, EnumProvider? type)
{
Expand All @@ -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)
Expand Down Expand Up @@ -101,6 +117,8 @@ internal class NameVisitor : ScmLibraryVisitor
type.Update(name: newName);
}
}

type.Update(name: NormalizeAcronymCasing(type.Name));
return type;
}

Expand All @@ -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)
Expand Down Expand Up @@ -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);
}

/// <summary>
/// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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]
Expand All @@ -68,45 +128,29 @@ 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));
var serializationProvider = type?.SerializationProviders.SingleOrDefault();
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]
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]);

Expand All @@ -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]);

Expand All @@ -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: []);
}
}
}
Loading