Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ruby: `initialize` is now a reserved name. An API member called `initialize` previously generated a second `def initialize`, redefining the constructor; it is now escaped.
- Ruby: removed the blank line emitted at the start of every generated class body.
- Ruby: moved the runtime gem dependencies to 0.19.0, the first release able to serialize a primitive composed type member and to tell which member a payload holds.
- Ruby: models whose names differ only in where a separator falls (e.g. `codeScanningVariantAnalysis_status` and `codeScanningVariantAnalysisStatus`) snake-cased to the same file name, so one silently overwrote the other and the models barrel required it twice. Colliding file names are now disambiguated, leaving the generated type names unchanged. Un-suppresses the GitHub integration test. [#1816](https://github.com/microsoft/kiota/issues/1816)
- Ruby: fixed flattening of models nested more than one namespace deep. The separators were emitted into the class name, producing invalid constants such as `class S.v2.billingMeterEvent`, and the models namespace was itself renamed part-way through the walk, prefixing every nested model with a fragment of it (`s_v2_billing_meter_event`). Un-suppresses the Stripe integration and idempotency tests. [#1816](https://github.com/microsoft/kiota/issues/1816)
- Fixed plugin manifest generation to omit unsafe `oauth_card_path` file references that could resolve outside the plugin package.
- Bumped `Microsoft.OpenApi` and `Microsoft.OpenApi.YamlReader` to 3.10.2, and lifted the YAML reader scalar length limit so descriptions with large scalars (e.g. long markdown descriptions) can still be parsed.
Expand Down
9 changes: 5 additions & 4 deletions it/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
"https://raw.githubusercontent.com/github/rest-api-description/refs/heads/main/descriptions/api.github.com/api.github.qkg1.top.json": {
"MockServerITFolder": "gh",
"Suppressions": [
{
"Language": "ruby",
"Rationale": "https://github.com/microsoft/kiota-abstractions-ruby/issues/73"
},
{
"Language": "dart",
"Rationale": "Multiple compilation errors with api from 2026-06-22 - https://github.com/microsoft/kiota/issues/7821"
Expand All @@ -37,6 +33,11 @@
"Language": "java",
"Pattern": "/repos/{owner}/{repo}/contents/{path}#GET",
"Rationale": "Compilation fails for Java with api version 1.1.4 dated 2026-06-23 - https://github.com/microsoft/kiota/issues/7829"
},
{
"Language": "ruby",
"Pattern": "/repos/{owner}/{repo}/contents/{path}#GET",
"Rationale": "The oneOf response leaves a model detached from the DOM while a using still references it, so the composed type wrapper requires a file that is never generated. Same endpoint excluded for Java and TypeScript - https://github.com/microsoft/kiota/issues/7829"
}
]
},
Expand Down
6 changes: 6 additions & 0 deletions it/ruby/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,9 @@ Metrics/PerceivedComplexity:
# Generated factory methods may just delegate to super
Lint/UselessMethodDefinition:
Enabled: false

# The cop matches by method name, so it flags the accessor for an API property called `inherited`
# (the GitHub description has one). Ruby's inheritance hook is a class method, and an instance
# method of the same name does not shadow it, so renaming the property would be the worse trade
Lint/MissingSuper:
Enabled: false
28 changes: 27 additions & 1 deletion src/Kiota.Builder/PathSegmenters/RubyPathSegmenter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -19,7 +20,32 @@ public override IEnumerable<string> GetAdditionalSegment(CodeElement currentElem
};
}
public override string FileSuffix => ".rb";
public override string NormalizeFileName(CodeElement currentElement) => GetLastFileNameSegment(currentElement).ToSnakeCase();
private readonly ConcurrentDictionary<CodeNamespace, Dictionary<string, CodeElement[]>> collidingFileNames = new();
/// <summary>
/// Snake casing is lossy: names that differ only in where a separator falls, such as
/// codeScanningVariantAnalysis_status and codeScanningVariantAnalysisStatus, collapse onto the
/// same path. One model then silently overwrote the other and the barrel required it twice, so
/// the second and later members of a colliding set get a numeric suffix. The ordering is by
/// ordinal name, which keeps the result stable between the require path and the output path.
/// </summary>
public override string NormalizeFileName(CodeElement currentElement)
{
var fileName = GetLastFileNameSegment(currentElement).ToSnakeCase();
if (currentElement is not (CodeClass or CodeEnum) || currentElement.Parent is not CodeNamespace parentNamespace)
return fileName;
var collisions = collidingFileNames.GetOrAdd(parentNamespace, static ns =>
ns.Classes.Cast<CodeElement>()
.Concat(ns.Enums)
.GroupBy(static x => GetLastFileNameSegment(x).ToSnakeCase(), StringComparer.OrdinalIgnoreCase)
.Where(static x => x.Skip(1).Any())
.ToDictionary(static x => x.Key,
static x => x.OrderBy(static y => y.Name, StringComparer.Ordinal).ToArray(),
StringComparer.OrdinalIgnoreCase));
if (!collisions.TryGetValue(fileName, out var siblings))
return fileName;
var index = Array.FindIndex(siblings, x => ReferenceEquals(x, currentElement));
return index > 0 ? $"{fileName}_{index + 1}" : fileName;
}
public override string NormalizeNamespaceSegment(string segmentName) => segmentName.ToSnakeCase();
public override string NormalizePath(string fullPath) =>
ExceedsMaxPathLength(fullPath) && Path.GetDirectoryName(fullPath) is string directoryName ?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Linq;

Check failure on line 1 in tests/Kiota.Builder.Tests/PathSegmenters/RubyPathSegmenterTests.cs

View workflow job for this annotation

GitHub Actions / dotnet-build

Fix file encoding.
using Kiota.Builder.CodeDOM;
using Kiota.Builder.PathSegmenters;
using Xunit;

namespace Kiota.Builder.Tests.PathSegmenters
{
public class RubyPathSegmenterTests
{
private readonly RubyPathSegmenter segmenter;
public RubyPathSegmenterTests()
{
segmenter = new RubyPathSegmenter("/tmp/kiota-sample", "client");
}

[Fact]
public void RubyPathSegmenterGeneratesCorrectFileName()
{
var rootNamespace = CodeNamespace.InitRootNamespace();
var classExample = rootNamespace.AddClass(new CodeClass
{
Name = "testClass"
}).First();
Assert.Equal("test_class", segmenter.NormalizeFileName(classExample));
}

[Fact]
public void DisambiguatesEnumsThatSnakeCaseToTheSameFileName()
{
// the github description declares both a top level status enum and an inline one on the
// parent schema; snake casing collapses them onto one path, so one silently overwrote
// the other and the models barrel required the same file twice
var rootNamespace = CodeNamespace.InitRootNamespace();
var ns = rootNamespace.AddNamespace("client.models");
var withSeparator = ns.AddEnum(new CodeEnum { Name = "codeScanningVariantAnalysis_status" }).First();
var withoutSeparator = ns.AddEnum(new CodeEnum { Name = "codeScanningVariantAnalysisStatus" }).First();

var first = segmenter.NormalizeFileName(withSeparator);
var second = segmenter.NormalizeFileName(withoutSeparator);
Assert.NotEqual(first, second);
}

[Fact]
public void DisambiguatesAClassAndAnEnumSharingAFileName()
{
var rootNamespace = CodeNamespace.InitRootNamespace();
var ns = rootNamespace.AddNamespace("client.models");
var model = ns.AddClass(new CodeClass { Name = "SomeModel", Kind = CodeClassKind.Model }).First();
var enumeration = ns.AddEnum(new CodeEnum { Name = "some_model" }).First();

Assert.NotEqual(segmenter.NormalizeFileName(model), segmenter.NormalizeFileName(enumeration));
}

[Fact]
public void KeepsTheFileNameStableWhenThereIsNoCollision()
{
var rootNamespace = CodeNamespace.InitRootNamespace();
var ns = rootNamespace.AddNamespace("client.models");
var only = ns.AddClass(new CodeClass { Name = "someModel", Kind = CodeClassKind.Model }).First();
ns.AddClass(new CodeClass { Name = "otherModel", Kind = CodeClassKind.Model });

Assert.Equal("some_model", segmenter.NormalizeFileName(only));
}

[Fact]
public void ReturnsTheSameNameForRepeatedCalls()
{
// the writer resolves the require path and the file writer resolves the output path
// through separate calls, so they have to agree
var rootNamespace = CodeNamespace.InitRootNamespace();
var ns = rootNamespace.AddNamespace("client.models");
ns.AddEnum(new CodeEnum { Name = "codeScanningVariantAnalysis_status" });
var second = ns.AddEnum(new CodeEnum { Name = "codeScanningVariantAnalysisStatus" }).First();

Assert.Equal(segmenter.NormalizeFileName(second), segmenter.NormalizeFileName(second));
}
}
}
Loading