Skip to content
Draft
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 @@ -177,7 +177,7 @@ await GenerateAndAssertFiles(
clients: [client],
customFiles: [],
expectedFiles: [],
publicModelNames: ["MetadataOnlyResponse"]);
publicModelNames: ["MetadataOnlyResult"]);
}

[Test]
Expand Down Expand Up @@ -227,13 +227,13 @@ await GenerateAndAssertFiles(
clients: [client],
customFiles: [],
expectedFiles: [
Path.Combine("src", "Generated", "Models", "MetadataOnlyResponse.cs"),
Path.Combine("src", "Generated", "Models", "MetadataOnlyResponse.Serialization.cs")
Path.Combine("src", "Generated", "Models", "MetadataOnlyResult.cs"),
Path.Combine("src", "Generated", "Models", "MetadataOnlyResult.Serialization.cs")
],
internalModelNames: ["MetadataOnlyResponse"],
internalModelNames: ["MetadataOnlyResult"],
configureGenerator: () =>
{
var provider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.Single(provider => provider.Name == "MetadataOnlyResponse");
var provider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.Single(provider => provider.Name == "MetadataOnlyResult");
CodeModelGenerator.Instance.AddTypeToKeep(provider);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,35 @@ protected TypeProvider() : this(null)
}

private protected virtual TypeProvider? BuildCustomCodeView(string? generatedTypeName = null, string? generatedTypeNamespace = null)
=> CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCurrentCompilation(
generatedTypeNamespace ?? BuildNamespace(),
generatedTypeName ?? BuildName(),
{
var typeNamespace = generatedTypeNamespace ?? BuildNamespace();
var typeName = generatedTypeName ?? BuildName();
var customCodeView = CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCurrentCompilation(
typeNamespace,
typeName,
_declaringTypeName.Value);
if (customCodeView is not null || this is not ModelProvider || _inputType is null || _inputType.IsExactName)
{
return customCodeView;
}

var originalName = _inputType.Name.ToIdentifierName();
if (!originalName.EndsWith("Response", StringComparison.Ordinal) ||
originalName == typeName ||
typeName != NormalizeTypeName(originalName))
{
return null;
}

return CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCurrentCompilation(
typeNamespace,
originalName,
_declaringTypeName.Value) ??
CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCurrentCompilation(
typeNamespace,
originalName.NormalizeCSharpAcronyms(),
_declaringTypeName.Value);
}

private protected virtual TypeProvider? BuildLastContractView(string? generatedTypeName = null, string? generatedTypeNamespace = null)
{
Expand All @@ -64,7 +89,7 @@ protected TypeProvider() : this(null)
}

var originalName = _inputType.Name.ToIdentifierName();
var normalizedOriginalName = originalName.NormalizeCSharpAcronyms();
var normalizedOriginalName = NormalizeTypeName(originalName);
if (normalizedOriginalName == originalName || typeName != normalizedOriginalName)
{
return null;
Expand All @@ -73,6 +98,10 @@ protected TypeProvider() : this(null)
return CodeModelGenerator.Instance.SourceInputModel.FindForTypeInLastContract(
typeNamespace,
originalName,
_declaringTypeName.Value) ??
CodeModelGenerator.Instance.SourceInputModel.FindForTypeInLastContract(
typeNamespace,
originalName.NormalizeCSharpAcronyms(),
_declaringTypeName.Value);
}

Expand Down Expand Up @@ -748,7 +777,7 @@ protected string NormalizeTypeNameForNewContract(string name)
return name;
}

var normalizedName = name.NormalizeCSharpAcronyms();
var normalizedName = NormalizeTypeName(name);
if (normalizedName == name)
{
return name;
Expand All @@ -761,6 +790,52 @@ protected string NormalizeTypeNameForNewContract(string name)
return lastContractType is null ? normalizedName : name;
}

private string NormalizeTypeName(string name)
{
var normalizedName = name.NormalizeCSharpAcronyms();
const string responseSuffix = "Response";
if (this is not ModelProvider || !normalizedName.EndsWith(responseSuffix, StringComparison.Ordinal))
{
return normalizedName;
}

var typeNamespace = BuildNamespace();
var sourceInputModel = CodeModelGenerator.Instance.SourceInputModel;
if (sourceInputModel.FindForTypeInCurrentCompilation(typeNamespace, normalizedName, _declaringTypeName.Value) is not null ||
sourceInputModel.FindForTypeInLastContract(typeNamespace, normalizedName, _declaringTypeName.Value) is not null)
{
return normalizedName;
}

var resultName = $"{normalizedName[..^responseSuffix.Length]}Result";
var inputNamespace = CodeModelGenerator.Instance.InputLibrary.InputNamespace;
// Model and enum files share a flat output directory, even across namespaces.
return inputNamespace.Models.Any(model => HasConflictingName(model, model.Namespace)) ||
inputNamespace.Enums.Any(@enum => HasConflictingName(@enum, @enum.Namespace))
? normalizedName
: resultName;

bool HasConflictingName(InputType inputType, string inputTypeNamespace)
{
var otherNamespace = string.IsNullOrEmpty(inputTypeNamespace)
? CodeModelGenerator.Instance.TypeFactory.PrimaryNamespace
: CodeModelGenerator.Instance.TypeFactory.GetCleanNameSpace(inputTypeNamespace);
var otherName = inputType.IsExactName ? inputType.Name : inputType.Name.ToIdentifierName();
var customType = sourceInputModel.FindForTypeInCurrentCompilation(otherNamespace, otherName);
if (customType is null && !inputType.IsExactName)
{
var normalizedOtherName = otherName.NormalizeCSharpAcronyms();
customType = sourceInputModel.FindForTypeInCurrentCompilation(otherNamespace, normalizedOtherName);
if (sourceInputModel.FindForTypeInLastContract(otherNamespace, otherName) is null)
{
otherName = normalizedOtherName;
}
}

return inputType != _inputType && (otherName == resultName || customType?.Name == resultName);
}
}

/// <summary>
/// Resets only the cached methods so they are rebuilt on next access.
/// Use this instead of <see cref="Reset"/> when you need to force a method
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ public void BuildEnumType_ValidateStringBasedFixedEnum()
[TestCase(false, "IPV4AddressIPV6", false, "IPV4AddressIPV6")]
[TestCase(true, "OsloIpsumOsmosisDbz", false, "OsloIpsumOsmosisDbz")]
[TestCase(false, "IpKind", true, "IpKind")]
[TestCase(false, "WidgetResponse", false, "WidgetResponse")]
[TestCase(true, "IpResponse", false, "IPResponse")]
public void BuildEnumType_NormalizesTypeAcronymCasing(
bool isExtensible,
string inputName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,148 @@ public void TestBuildName_NormalizesAcronymCasing(string inputName, bool isExact
Assert.AreEqual(expectedName, modelProvider.Name);
}

[TestCase("WidgetResponse", false, "WidgetResult")]
[TestCase("widgetResponse", false, "WidgetResult")]
[TestCase("widget_response", false, "WidgetResult")]
[TestCase("Response", false, "Result")]
[TestCase("WidgetResponseResponse", false, "WidgetResponseResult")]
[TestCase("IpResponse", false, "IPResult")]
[TestCase("WidgetResult", false, "WidgetResult")]
[TestCase("ResponseWidget", false, "ResponseWidget")]
[TestCase("WidgetResponseDetails", false, "WidgetResponseDetails")]
[TestCase("WidgetResponses", false, "WidgetResponses")]
[TestCase("Widgetresponse", false, "Widgetresponse")]
[TestCase("WidgetRESPONSE", false, "WidgetRESPONSE")]
[TestCase("WidgetResponse", true, "WidgetResponse")]
[TestCase("widget_response", true, "widget_response")]
public void TestBuildName_NormalizesResponseSuffix(string inputName, bool isExactName, string expectedName)
{
var inputModel = InputFactory.Model(inputName, isExactName: isExactName);

var modelProvider = new ModelProvider(inputModel);

Assert.AreEqual(expectedName, modelProvider.Name);
Assert.AreEqual(Path.Combine("src", "Generated", "Models", $"{expectedName}.cs"), modelProvider.RelativeFilePath);
}

[Test]
public void TestBuildName_ResponseSuffixHandlesPropertyNameCollision()
{
var inputModel = InputFactory.Model(
"WidgetResponse",
properties:
[
InputFactory.Property("widgetResult", InputPrimitiveType.String),
InputFactory.Property("otherResponse", InputPrimitiveType.String)
]);

var modelProvider = new ModelProvider(inputModel);

Assert.AreEqual("WidgetResult", modelProvider.Name);
Assert.AreEqual("WidgetResultProperty", modelProvider.Properties[0].Name);
Assert.AreEqual("OtherResponse", modelProvider.Properties[1].Name);
Assert.AreEqual("widgetResult", modelProvider.Properties[0].WireInfo!.SerializedName);
}

[TestCase(false, "Sample.Models", "WidgetResponse")]
[TestCase(true, "Sample.Models", "WidgetResponse")]
[TestCase(false, "Other.Models", "WidgetResponse")]
[TestCase(true, "Other.Models", "WidgetResponse")]
public void TestBuildName_ResponseSuffixAvoidsTypeNameCollision(bool isEnum, string otherNamespace, string expectedName)
{
var inputModel = InputFactory.Model("WidgetResponse");
var otherModel = InputFactory.Model("WidgetResult", @namespace: otherNamespace);
var otherEnum = InputFactory.StringEnum("WidgetResult", [("Value", "value")], clientNamespace: otherNamespace);
MockHelpers.LoadMockGenerator(
inputModelTypes: isEnum ? [inputModel] : [inputModel, otherModel],
inputEnumTypes: isEnum ? [otherEnum] : []);

var modelProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel)!;

Assert.AreEqual(expectedName, modelProvider.Name);
}

[Test]
public async Task TestBuildName_ResponseSuffixAvoidsCustomizedTypeNameCollision()
{
var inputModel = InputFactory.Model("WidgetResponse");
var otherModel = InputFactory.Model("Other");
await MockHelpers.LoadMockGeneratorAsync(
inputModelTypes: [inputModel, otherModel],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var modelProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel)!;
var otherProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(otherModel)!;

Assert.AreEqual("WidgetResponse", modelProvider.Name);
Assert.AreEqual("WidgetResult", otherProvider.Name);
Assert.IsNull(modelProvider.CustomCodeView);
}

[Test]
public async Task TestBuildName_ResponseSuffixAvoidsCustomizationAliasCollision()
{
var inputModel = InputFactory.Model("WidgetResponse");
var otherModel = InputFactory.Model("WidgetResult");
await MockHelpers.LoadMockGeneratorAsync(
inputModelTypes: [inputModel, otherModel],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var modelProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel)!;
var otherProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(otherModel)!;

Assert.AreEqual("WidgetResponse", modelProvider.Name);
Assert.AreEqual("CustomizedResult", otherProvider.Name);
Assert.IsNull(modelProvider.CustomCodeView);
}

[TestCase("WidgetResponse", "WidgetResult", "WidgetResponse", false, false)]
[TestCase("WidgetResponse", "WidgetResult", "WidgetResponse", false, true)]
[TestCase("WidgetResponse", "WidgetResult", "WidgetResponse", true, false)]
[TestCase("WidgetResponse", "WidgetResult", "WidgetResponse", true, true)]
[TestCase("IpResponse", "IPResult", "IPResponse", false, false)]
[TestCase("IpResponse", "IPResult", "IPResponse", false, true)]
[TestCase("IpResponse", "IPResult", "IPResponse", true, false)]
[TestCase("IpResponse", "IPResult", "IPResponse", true, true)]
public async Task TestBuildName_ResponseSuffixPreservesExistingName(
string inputName,
string generatedName,
string existingName,
bool isLastContract,
bool updateNamespace)
{
var inputModel = InputFactory.Model(
inputName,
@namespace: updateNamespace ? "Sample" : "Sample.Models");
await MockHelpers.LoadMockGeneratorAsync(
inputModelTypes: [inputModel],
compilation: isLastContract ? null : async () => await Helpers.GetCompilationFromDirectoryAsync(),
lastContractCompilation: isLastContract ? async () => await Helpers.GetCompilationFromDirectoryAsync() : null);

var modelProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel)!;
if (updateNamespace)
{
Assert.AreEqual(generatedName, modelProvider.Name);
modelProvider.Update(@namespace: "Sample.Models");
}

Assert.AreEqual(existingName, modelProvider.Name);
Assert.IsNotNull(isLastContract ? modelProvider.LastContractView : modelProvider.CustomCodeView);
}

[Test]
public async Task TestBuildName_ResponseSuffixPreservesCustomName()
{
await MockHelpers.LoadMockGeneratorAsync(
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());
var inputModel = InputFactory.Model("WidgetResponse");

var modelProvider = new ModelProvider(inputModel);

Assert.AreEqual("CustomizedResponse", modelProvider.Name);
Assert.IsNotNull(modelProvider.CustomCodeView);
}

[Test]
public async Task TestBuildName_BackCompatTakesPrecedenceOverAcronymNormalization()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.TypeSpec.Generator.Customizations;

namespace Sample.Models
{
[CodeGenType("WidgetResult")]
public partial class CustomizedResult
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.TypeSpec.Generator.Customizations;

namespace Sample.Models
{
[CodeGenType("Other")]
public partial class WidgetResult
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.TypeSpec.Generator.Customizations;

namespace Sample.Models
{
[CodeGenType("WidgetResponse")]
public partial class CustomizedResponse
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Sample.Models
{
public partial class IPResponse
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Sample.Models
{
public partial class WidgetResponse
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public override async IAsyncEnumerable<ClientResult> GetRawPagesAsync()
ClientResult result = await GetNextResponseAsync(message).ConfigureAwait(false);
yield return result;

nextToken = ((ListWithContinuationTokenResponse)result).NextToken;
nextToken = ((ListWithContinuationTokenResult)result).NextToken;
if (string.IsNullOrEmpty(nextToken))
{
yield break;
Expand All @@ -55,7 +55,7 @@ public override async IAsyncEnumerable<ClientResult> GetRawPagesAsync()
/// <returns> The continuation token for the specified page. </returns>
public override ContinuationToken GetContinuationToken(ClientResult page)
{
string nextPage = ((ListWithContinuationTokenResponse)page).NextToken;
string nextPage = ((ListWithContinuationTokenResult)page).NextToken;
if (!string.IsNullOrEmpty(nextPage))
{
return ContinuationToken.FromBytes(BinaryData.FromString(nextPage));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public override async IAsyncEnumerable<ClientResult> GetRawPagesAsync()
ClientResult result = await GetNextResponseAsync(message).ConfigureAwait(false);
yield return result;

nextToken = ((ListWithContinuationTokenResponse)result).NextToken;
nextToken = ((ListWithContinuationTokenResult)result).NextToken;
if (string.IsNullOrEmpty(nextToken))
{
yield break;
Expand All @@ -55,7 +55,7 @@ public override async IAsyncEnumerable<ClientResult> GetRawPagesAsync()
/// <returns> The continuation token for the specified page. </returns>
public override ContinuationToken GetContinuationToken(ClientResult page)
{
string nextPage = ((ListWithContinuationTokenResponse)page).NextToken;
string nextPage = ((ListWithContinuationTokenResult)page).NextToken;
if (!string.IsNullOrEmpty(nextPage))
{
return ContinuationToken.FromBytes(BinaryData.FromString(nextPage));
Expand All @@ -71,7 +71,7 @@ public override ContinuationToken GetContinuationToken(ClientResult page)
/// <returns> The values from the specified page. </returns>
protected override async IAsyncEnumerable<Thing> GetValuesFromPageAsync(ClientResult page)
{
foreach (Thing item in ((ListWithContinuationTokenResponse)page).Things)
foreach (Thing item in ((ListWithContinuationTokenResult)page).Things)
{
yield return item;
await Task.Yield();
Expand Down
Loading