fix(ruby): add composed type support and disable inner classes for wrappers - #8065
Conversation
7cad9c7 to
6e5c0ea
Compare
|
This should be ready for a first pass Vincent Biret (@baywet) 🙏 |
There was a problem hiding this comment.
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.jsononly 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
elsifis 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
downcaseon nil and aborts factory creation instead of falling through to the scalar alternatives. Guardmapping_valuebefore callingdowncase, as the inherited factory'scase mapping_valuepath 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
mappedKeyis a raw discriminator value, butSanitizeRubyDoubleQuoteLiteralalso 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
DiscriminatorPropertyNameis 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 invalidget_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.
…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.
6e5c0ea to
90b4193
Compare
- 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
4f675dd to
da1156a
Compare
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 inappsettings.jsonandit/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
ConvertUnionTypesToWrapper(..., supportInnerClasses: false), as Python does, so composed type wrappers are emitted as top-level classes instead of inner classes.initializeis now a reserved name. An API member calledinitialize(the real case is a path segment such as/media/upload/initialize) previously emitted a seconddef initialize, silently redefining the constructor.Integration tests
Metrics/PerceivedComplexity, next to the existingMetrics/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
it/download-openapi-specs.ps1,it/generate-code.ps1andit/exec-cmd.ps1: 2 examples, 0 failures, 1421 generated files RuboCop-clean.0.19.0is a behavioural change in the runtime, the two Ruby mock-server suites that deserialize a live response (basicanddefaultvalues) were also run against it. Both pass;defaultvaluesreads booleans, floats, GUIDs, dates and times back through the now type-strict readers.Closes #1816