Skip to content

fix(ruby): add composed type support and disable inner classes for wrappers - #8065

Merged
Vincent Biret (baywet) merged 6 commits into
microsoft:mainfrom
andreaTP:fix/ruby-composed-types
Aug 28, 2026
Merged

fix(ruby): add composed type support and disable inner classes for wrappers#8065
Vincent Biret (baywet) merged 6 commits into
microsoft:mainfrom
andreaTP:fix/ruby-composed-types

Conversation

@andreaTP

@andreaTP Andrea Peruffo (andreaTP) commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Adds composed type (union/intersection) support to the Ruby generator and un-suppresses the Twitter Ruby integration test.

Depends on the runtime work in microsoft/kiota-ruby#142, released as gems 0.19.0, which this PR pins in appsettings.json and it/ruby/Gemfile. That PR made the JSON parse node's scalar readers type-strict, matching dotnet and Python, so a union can tell which member a payload holds; it also added a root-scalar path to the serialization writer so a composed type whose selected member is a primitive no longer serializes to {}.

Generator

  • Union, intersection and inherited factory bodies, plus the matching serializer and deserializer methods.
  • ConvertUnionTypesToWrapper(..., supportInnerClasses: false), as Python does, so composed type wrappers are emitted as top-level classes instead of inner classes.
  • initialize is now a reserved name. An API member called initialize (the real case is a path segment such as /media/upload/initialize) previously emitted a second def initialize, silently redefining the constructor.
  • No blank line at the start of a generated class body.

Integration tests

  • Removes the Ruby suppression for the Twitter description.
  • Adds one RuboCop exclusion, Metrics/PerceivedComplexity, next to the existing Metrics/CyclomaticComplexity. A composed type factory branches once per union member, so its complexity tracks the arity of the union rather than any avoidable nesting.

The remaining Ruby suppressions (github, stripe, twilio, msgraph-beta) are not touched here. Each has a distinct root cause, all four reproduce on main, and each gets its own PR.

Verification

  • Full unit suite: 2279 + 125 + 18 passing, no failures.
  • Twitter Ruby integration test run end to end through it/download-openapi-specs.ps1, it/generate-code.ps1 and it/exec-cmd.ps1: 2 examples, 0 failures, 1421 generated files RuboCop-clean.
  • Because 0.19.0 is a behavioural change in the runtime, the two Ruby mock-server suites that deserialize a live response (basic and defaultvalues) were also run against it. Both pass; defaultvalues reads booleans, floats, GUIDs, dates and times back through the now type-strict readers.

Closes #1816

@andreaTP

Copy link
Copy Markdown
Contributor Author

This should be ready for a first pass Vincent Biret (@baywet) 🙏

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds Ruby support for generated composed union/intersection wrappers and updates related generation, dependencies, linting, tests, and integration configuration.

Changes:

  • Adds composed-type factory, serializer, and deserializer generation.
  • Disables inner wrapper classes and reserves initialize.
  • Updates Ruby dependencies, RuboCop exclusions, integration settings, tests, and changelog.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Reviewed change Final review notes
tests/Kiota.Builder.Tests/Writers/Ruby/CodeMethodWriterTests.cs Tests composed-type writer output.
tests/Kiota.Builder.Tests/Writers/Ruby/CodeClassDeclarationWriterTests.cs Tests class declaration formatting.
tests/Kiota.Builder.Tests/Refiners/RubyLanguageRefinerTests.cs Tests reserved-name handling. Nit (2 votes): the test does not exercise an API member named initialize.
src/kiota/appsettings.json Updates Ruby dependencies.
src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs Generates composed-type methods. Critical (4 votes): primitive fallback parsing can mishandle mapped objects and select multiple candidates. Critical (2 votes): primitive scalar serialization can lose values. Moderate (2 votes): empty discriminator names can emit invalid lookups. Critical (2 votes): complex serialization assumes the first member is non-nil.
src/Kiota.Builder/Writers/Ruby/CodeClassDeclarationWriter.cs Adjusts class declaration formatting.
src/Kiota.Builder/Refiners/RubyReservedNamesProvider.cs Reserves initialize.
src/Kiota.Builder/Refiners/RubyRefiner.cs Disables inner wrapper classes.
it/ruby/Gemfile Updates integration dependencies.
it/ruby/.rubocop.yml Adds generated-code exclusions.
it/config.json Adjusts integration suppressions. Nit (3 votes): GitHub and Stripe Ruby suppressions remain despite the description stating they were removed.
CHANGELOG.md Documents the Ruby changes.
Suppressed comments (7)

CHANGELOG.md:21

  • The PR description says the GitHub, Twitter, and Stripe Ruby integration suppressions are being removed, but this changelog entry and it/config.json only remove Twitter; the GitHub Ruby suppression and both Stripe Ruby suppressions remain. Remove the intended remaining entries or narrow the description so the integration coverage change is not overstated.
- Ruby: added composed type (union/intersection) support to the factory, serializer and deserializer bodies, and stopped emitting composed type wrappers as inner classes. Un-suppresses the Twitter integration test. [kiota-abstractions-ruby#73](https://github.com/microsoft/kiota-abstractions-ruby/issues/73) [#1816](https://github.com/microsoft/kiota/issues/1816)

src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs:184

  • These checks are not type tests in Ruby: the JSON parse node returns a value from each scalar getter (and coerces numeric values). For an all-primitive intersection such as string/number, the first elsif is therefore selected regardless of the payload, so the wrapper can contain the wrong member. Use a type-aware runtime parse/discriminator mechanism rather than only pre-reading each getter.
            writer.StartBlock($"{elseIfPrefix}if !{GetIntersectionValueVarName(property)}.nil?");

src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs:572

  • The intersection serializer has the same nil-key primitive problem: write_*_value(nil, value) does not add a scalar to the Ruby JSON writer, so primitive intersection wrappers lose their serialized value (and false can raise). Please coordinate the runtime writer/API with this generated path instead of relying on the return value being captured.
            writer.StartBlock($"{elseIfPrefix}if !@{property.Name.ToSnakeCase()}.nil?");
            writer.WriteLine($"writer.{GetSerializationMethodName(property.Type)}(nil, @{property.Name.ToSnakeCase()})");

src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs:144

  • If a ParseNode supplies a discriminator node whose string value is nil, this condition calls downcase on nil and aborts factory creation instead of falling through to the scalar alternatives. Guard mapping_value before calling downcase, as the inherited factory's case mapping_value path already handles a missing value safely.
                writer.StartBlock($"{elseIfPrefix}if {DiscriminatorMappingVarName}.downcase == \"{RubyConventionService.SanitizeRubyDoubleQuoteLiteral(mappedKey)}\".downcase");

src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs:144

  • The new union discriminator path interpolates schema-controlled mapping and property names into Ruby literals. Although the writer calls a sanitizer, the added tests cover only ordinary keys, so a regression could reintroduce raw quote/control-character injection without detection. Add a writer regression using quotes, newline/carriage-return/tab, backslash, #, and $, and assert the generated literals contain the escaped forms.
                writer.StartBlock($"{elseIfPrefix}if {DiscriminatorMappingVarName}.downcase == \"{RubyConventionService.SanitizeRubyDoubleQuoteLiteral(mappedKey)}\".downcase");

src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs:144

  • mappedKey is a raw discriminator value, but SanitizeRubyDoubleQuoteLiteral also preserves/reconstructs already-quoted values for default literals. A valid mapping value that starts and ends with " is therefore emitted with an extra pair of quotes inside this surrounding literal, producing invalid Ruby (for example, == ""value""). Escape the raw value without the already-quoted handling at this emission site.
                writer.StartBlock($"{elseIfPrefix}if {DiscriminatorMappingVarName}.downcase == \"{RubyConventionService.SanitizeRubyDoubleQuoteLiteral(mappedKey)}\".downcase");

src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs:138

  • DiscriminatorPropertyName is also a raw schema key, so using the default-value helper here has the same quoted-value problem: a property name beginning and ending with " generates an invalid get_child_node(""name"") call. Use a raw double-quoted-literal sanitizer that only escapes the content (including Ruby interpolation markers) at this write site.
            writer.WriteLine($"{NodeVarName} = {parseNodeParameterName}.get_child_node(\"{RubyConventionService.SanitizeRubyDoubleQuoteLiteral(parentClass.DiscriminatorInformation.DiscriminatorPropertyName)}\")");

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread it/config.json
Comment thread src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs
Comment thread src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs
Comment thread src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs
Comment thread src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs Outdated
Comment thread tests/Kiota.Builder.Tests/Refiners/RubyLanguageRefinerTests.cs
…appers

Implements union and intersection composed types for the Ruby generator:
factory, serializer and deserializer bodies for each, mirroring Python's
approach adapted to Ruby idioms.

Composed type wrappers are no longer emitted as inner classes. Ruby renders
the wrapper's accessors outside the inner class body, producing invalid code;
passing supportInnerClasses: false places wrappers as namespace-level siblings,
matching what Python already does.

Each non-complex property in an intersection factory parses into its own
val_<name> variable. A shared variable would be reassigned inside the previous
branch of the if/elsif chain, so every property after the first was silently
never deserialized.

Block writers close their if/elsif chains with CloseBlock("end", false) since
the loop already restored the indent; closing again shifted the `end` and every
later `end` in the file one level left.

AssertBalancedBlocks now tracks opener columns rather than only counting, so a
misaligned `end` fails the test rather than passing silently.
Two independent sources of invalid or misleading generated Ruby:

- CodeClassDeclarationWriter wrote the mixin line unconditionally, so a class
  with no Implements got `GetIndent() + ""` as the first line of its body -- a
  whitespace-only line (and trailing whitespace) in every request builder,
  query-parameter class and composed-type wrapper.

- `initialize` was not a reserved name, so an API member of that name (e.g.
  Twitter's /2/media/upload/initialize) became a request-builder property and
  emitted a second `def initialize` in a class that already had one, returning
  a value and skipping super.

Re-enable the cops these were masking: Layout/EmptyLinesAroundClassBody,
Lint/DuplicateMethods, Lint/ReturnInVoidContext, Lint/MissingSuper.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

- a lone branch has no elsif to chain to, so it reads as `unless` or a guard clause
- drops the Style/NegatedIf and Style/GuardClause exclusions this made unnecessary
@baywet
Vincent Biret (baywet) added this pull request to the merge queue Aug 28, 2026
Merged via the queue into microsoft:main with commit 2685ead Aug 28, 2026
311 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants