Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Ruby: a model sharing its name with a sibling namespace had the disambiguation suffix applied once per reference to it rather than once, so generation failed with `The element to rename was not found available_phone_number_countryModelModelModelModel`. The reference pass was removed: `CodeType.Name` already delegates to the type definition, so references follow the rename on their own. Un-suppresses the Twilio integration and idempotency tests. [kiota-abstractions-ruby#66](https://github.com/microsoft/kiota-abstractions-ruby/issues/66)

## [1.35.0] - 2026-09-01

### Added
Expand Down
12 changes: 0 additions & 12 deletions it/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,6 @@
]
},
"https://raw.githubusercontent.com/twilio/twilio-oai/refs/heads/main/spec/yaml/twilio_api_v2010.yaml": {
"Suppressions": [
{
"Language": "ruby",
"Rationale": "https://github.com/microsoft/kiota-abstractions-ruby/issues/66. The API of 06/2026 reports an error 'The element to rename was not found available_phone_number_countryModelModelModelModel'"
}
],
"IdempotencySuppressions": [
{
"Language": "ruby",
"Rationale": "https://github.com/microsoft/kiota/issues/1816"
}
]
},
"https://raw.githubusercontent.com/stripe/openapi/refs/heads/master/latest/openapi.spec3.json": {
"Suppressions": [
Expand Down
58 changes: 9 additions & 49 deletions src/Kiota.Builder/Refiners/RubyRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken
}
});
RemoveRequestConfigurationClasses(generatedCode);
var classesToDisambiguate = new HashSet<CodeClass>();
var suffix = "Model";
DisambiguateClassesWithNamespaceNames(generatedCode, classesToDisambiguate, suffix);
UpdateReferencesToDisambiguatedClasses(generatedCode, classesToDisambiguate, suffix);
DisambiguateClassesWithNamespaceNames(generatedCode, "Model");
ConvertUnionTypesToWrapper(generatedCode,
_configuration.UsesBackingStore,
static s => s,
Expand Down Expand Up @@ -144,7 +141,13 @@ private static void ShortenLongNamespaceNames(CodeElement currentElement)
}
CrawlTree(currentElement, ShortenLongNamespaceNames);
}
private static void DisambiguateClassesWithNamespaceNames(CodeElement currentElement, HashSet<CodeClass> classesToUpdate, string suffix)
/// <summary>
/// A model sharing its name with a sibling namespace is suffixed so the two do not collide.
/// References need no separate pass: CodeType.Name delegates to TypeDefinition.Name for a
/// resolved, non-external type, so every reference already reports the new name. A pass that
/// assigned to those names instead renamed the class again, once per reference it walked.
/// </summary>
private static void DisambiguateClassesWithNamespaceNames(CodeElement currentElement, string suffix)
{
if (currentElement is CodeClass currentClass &&
currentClass.IsOfKind(CodeClassKind.Model) &&
Expand All @@ -154,51 +157,8 @@ currentClass.Parent is CodeNamespace currentNamespace &&
currentNamespace.RemoveChildElement(currentClass);
currentClass.Name = $"{currentClass.Name}{suffix}";
currentNamespace.AddClass(currentClass);
classesToUpdate.Add(currentClass);
}
CrawlTree(currentElement, x => DisambiguateClassesWithNamespaceNames(x, classesToUpdate, suffix));
}
private static void UpdateReferencesToDisambiguatedClasses(CodeElement currentElement, HashSet<CodeClass> classesToUpdate, string suffix)
{
if (classesToUpdate.Count == 0) return;
if (currentElement is CodeProperty currentProperty &&
currentProperty.Type is CodeType propertyType &&
propertyType.TypeDefinition is CodeClass propertyTypeClass &&
classesToUpdate.Contains(propertyTypeClass))
propertyType.Name = $"{propertyType.Name}{suffix}";
else if (currentElement is CodeMethod currentMethod)
{
if (currentMethod.ReturnType is CodeType returnType &&
returnType.TypeDefinition is CodeClass returnTypeClass &&
classesToUpdate.Contains(returnTypeClass))
returnType.Name = $"{returnType.Name}{suffix}";
currentMethod.Parameters.Where(x => x.Type is CodeType parameterType &&
parameterType.TypeDefinition is CodeClass parameterTypeClass &&
classesToUpdate.Contains(parameterTypeClass))
.ToList()
.ForEach(x => x.Type.Name = $"{x.Type.Name}{suffix}");
}
else if (currentElement is CodeClass currentClass)
{
if (currentClass.StartBlock.Inherits?.TypeDefinition is CodeClass parentClass &&
classesToUpdate.Contains(parentClass))
currentClass.StartBlock.Inherits.Name = $"{currentClass.StartBlock.Inherits.Name}{suffix}";
currentClass.DiscriminatorInformation
.DiscriminatorMappings
.Select(static x => x.Value)
.OfType<CodeType>()
.Where(x => x.TypeDefinition is CodeClass typeClass && classesToUpdate.Contains(typeClass))
.ToList()
.ForEach(x => x.Name = $"{x.Name}{suffix}");
currentClass.Usings
.Where(static x => !x.IsExternal)
.Select(static x => x.Declaration)
.OfType<CodeType>()
.Where(x => x.TypeDefinition is CodeClass typeClass && classesToUpdate.Contains(typeClass))
.ToList()
.ForEach(x => x.Name = $"{x.Name}{suffix}");
}
CrawlTree(currentElement, x => UpdateReferencesToDisambiguatedClasses(x, classesToUpdate, suffix));
CrawlTree(currentElement, x => DisambiguateClassesWithNamespaceNames(x, suffix));
}
// `\\.` matches a literal backslash and `(<letter>...)` is an ordinary group capturing the text
// "<letter>", so the original pattern never matched and every nested model kept the dots from
Expand Down
36 changes: 36 additions & 0 deletions tests/Kiota.Builder.Tests/Refiners/RubyLanguageRefinerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,42 @@ public async Task EscapesInitializeAsync()
Assert.Contains("escaped", model.Name);
}
[Fact]
public async Task AppliesTheDisambiguationSuffixOnlyOnceAsync()
{
// CodeType.Name delegates to TypeDefinition.Name for a resolved, non-external type, so
// "updating a reference" renames the class itself. Walking several references therefore
// appended the suffix once per reference and twilio failed to generate with
// "The element to rename was not found available_phone_number_countryModelModelModelModel"
var config = new GenerationConfiguration { Language = GenerationLanguage.Ruby };
var modelsNS = root.AddNamespace(config.ModelsNamespaceName);
var collidingModel = modelsNS.AddClass(new CodeClass
{
Name = "availablePhoneNumberCountry",
Kind = CodeClassKind.Model,
}).First();
// the class only gets disambiguated when a sibling namespace carries its name
modelsNS.AddNamespace($"{modelsNS.Name}.availablePhoneNumberCountry");

var holder = modelsNS.AddClass(new CodeClass { Name = "holder", Kind = CodeClassKind.Model }).First();
holder.AddProperty(new CodeProperty
{
Name = "country",
Kind = CodePropertyKind.Custom,
Type = new CodeType { Name = "availablePhoneNumberCountry", TypeDefinition = collidingModel },
});
holder.AddMethod(new CodeMethod
{
Name = "getCountry",
ReturnType = new CodeType { Name = "availablePhoneNumberCountry", TypeDefinition = collidingModel },
});

await ILanguageRefiner.RefineAsync(config, root, cancellationToken: TestContext.Current.CancellationToken);

Assert.Equal("availablePhoneNumberCountryModel", collidingModel.Name);
// the references track the rename through the type definition, so they must agree
Assert.Equal(collidingModel.Name, holder.Properties.First(static x => x.Name.Equals("country", StringComparison.OrdinalIgnoreCase)).Type.Name);
}
[Fact]
public async Task ConvertEnumsToPascalCaseAsync()
{
var model = root.AddEnum(new CodeEnum
Expand Down
Loading