Skip to content

TR-7946 Generate nullable union types instead of aborting - #136

Open
dzmitry-starastsenka-paysera wants to merge 1 commit into
paysera:masterfrom
dzmitry-starastsenka-paysera:AC-2509-support-nullable-unions
Open

TR-7946 Generate nullable union types instead of aborting#136
dzmitry-starastsenka-paysera wants to merge 1 commit into
paysera:masterfrom
dzmitry-starastsenka-paysera:AC-2509-support-nullable-unions

Conversation

@dzmitry-starastsenka-paysera

@dzmitry-starastsenka-paysera dzmitry-starastsenka-paysera commented Jul 31, 2026

Copy link
Copy Markdown

TR-7946

Follow-up to #135. That PR rescues two type categories the generator was discarding; this one removes the last type-resolution blocker, nullable unions.

Kept separate deliberately: #135 is a pair of narrow bug fixes, whereas this changes how a RAML construct is interpreted and deserves its own decision. Neither depends on the other, but app-evpbank/public-transfers needs both.

The problem

DefinitionValidator.php:83 refuses unions outright:

if ($type instanceof UnionType) {
    throw new UnrecognizedTypeException('UnionType currently is not supported');
}

In practice you rarely see that message — a property typed integer | nil reaches PropertyDefinitionBuilder as the raw string "integer | nil", is not recognised as a simple type, becomes a TYPE_REFERENCE to a type of that name, and fails later as:

Did not found defined type "integer | nil"

This was reported in INV-257 (Apr 2024) and closed "Won't Do" in Feb 2025, so the behaviour is long-standing and known.

Why it is worth fixing rather than declining again

X | nil is not really a union. It says a property is present but may hold no value — that is nullability, and RAML has no other way to express it. A union of two genuinely different shapes (string | boolean) is a different thing.

Counting every union in paysera/api-spec:

Form Occurrences
string | nil 12
integer | nil 4
TransferTimelineInspection | nil 2
TimelineMessageCode | nil 2
datetime | nil 1
boolean | nil 1
string | boolean 1

21 of 22 are nullability. Declining the whole feature to avoid the one hard case costs the other twenty-one — and those twenty-one are why public-transfers cannot be generated.

What this PR does

X | nil collapses to X. Nothing is lost: every generated getter is return this.get('key'), which already yields null when the key is absent or null. Applies to property types and to array item types, and works for named types (TimelineMessageCode | nil) as well as primitives.

A union of two real types is left exactly as it was, so string | boolean still fails loudly rather than silently picking a side. That case has no single representation to generate and should stay a spec-authoring decision.

The change is confined to PropertyDefinitionBuilder, at the one point that already normalises a property's declared type.

Test plan

Regression — 27 fixture APIs, byte-identical output

Generated a JS client from every api.raml under tests/ on unmodified master, then again with this change:

distinct APIs generated: 27 | byte-level differences: 0

No fixture currently uses a union, so this confirms the normalisation does not disturb any existing path.

Forward — with #135, all type resolution now completes

Against the real app-evpbank/public-transfers/api.raml, with both branches applied and paysera/api-spec!1789 (the missing example files):

Stage Result
master The file ../examples/transfer_create_payza.json does not exist or is unreadable
+ api-spec!1789 Did not found defined type "AccountingMetadata"
+ #135 Did not found defined type "string | nil"
+ this PR type resolution completes — no unresolved types remain

⚠️ What I could not run

Two environment gaps on my side, both of which CI covers:

  1. PHPUnit was not run. The PHP available to me has no mbstring, which PHPUnit requires, and the only other runtimes on hand are PHP 7.4 containers that cannot run this codebase. The 27-API comparison above is my substitute — please treat the unit suite as a required check.
  2. Generation of public-transfers still does not finish for me, but no longer for any type reason: it now reaches template rendering and fails in PayseraWordNetBundle with could not find driver. My PHP reports PDO::getAvailableDrivers()none, so the WordNet SQLite lookup used for verb detection in resource paths cannot open. That is my environment, not this change — the 27 fixture APIs generate fine because their paths do not trigger that lookup. Anyone with pdo_sqlite should be able to complete the generation and confirm the resulting client.

No fixture covers a union, so the new path has no golden-master coverage. I would rather add one than not — happy to, if you tell me which shape you want pinned.

Context

@paysera/public-transfers-client is stuck at 4.13.0 and missing fields that shipped long ago (vop_check.id from CORE-5433, vop_check.error_code from TR-7778), because it has not been regenerable for a long time. Consumers read raw payload keys instead. This PR plus #135 plus api-spec!1789 are what change that.

Worth flagging: PLT-1813 records an intent to replace this generator with OpenAPI Generator. If that is close, you may prefer to take #135 alone (two narrow fixes) and leave unions unsupported. This PR is written so that decision stays open.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@dzmitry-starastsenka-paysera

Copy link
Copy Markdown
Author

@mSprunskas , please review. Thank you.

@dzmitry-starastsenka-paysera dzmitry-starastsenka-paysera changed the title AC-2509 Generate nullable union types instead of aborting TR-7946 Generate nullable union types instead of aborting Aug 3, 2026
`X | nil` says a property is present but may hold no value. That is
nullability, not a choice between two shapes, and every generated getter
already returns null when the key is absent, so the union collapses to X
with nothing lost.

Applies to property types and to array item types, and works for named
types as well as primitives. A union of two value types such as
`string | boolean` is left alone, because it has no single
representation to generate, so it still fails loudly.

Covered by the nullable-union fixture in the Javascript and PHP REST
client suites, and by a test asserting that a union of two value types
is still refused.
@dzmitry-starastsenka-paysera
dzmitry-starastsenka-paysera force-pushed the AC-2509-support-nullable-unions branch from 20c1bba to b995c89 Compare August 12, 2026 14:44
@dzmitry-starastsenka-paysera

Copy link
Copy Markdown
Author

Added test coverage before review, so this does not need the same round as #135.

How to run the suite. PHPUnit cannot start on a stock Ubuntu 24.04 PHP 8.3, because mbstring is not in the distro's default extension set and ConstantBuilder::buildName() needs mb_strtoupper(). Running it in a container that has the extension needs nothing installed on the host:

docker run --rm -v "$PWD":/app -w /app -u $(id -u):$(id -g) \
  gitlab.paysera.net:5050/paysera/developer-environment/mokejimai:dev php bin/phpunit

That image is PHP 7.4.3, matching config.platform.php in composer.json. 45 tests, 1779 assertions, green.

What is covered. A nullable-union fixture in the Javascript and the PHP REST client suites, exercising X | nil as a primitive property type, on an optional property, as a named type, as an array item type of a primitive, and as an array item type of a named type.

Plus testGenerateCodeFailsForUnionOfValueTypes, asserting that string | boolean is still refused with Did not found defined type "string | boolean" — so the collapse cannot quietly widen to real unions.

The tests bite. Against master's PropertyDefinitionBuilder, same fixture and command:

UnrecognizedTypeException: Did not found defined type "string | nil"

and green with the change.

Version number. Bumped to 11.13.0 on the assumption that #135 lands first as 11.12.0. Both PRs insert at the top of CHANGELOG.md, so whichever merges second needs a rebase there anyway; if this one goes first, the number becomes 11.12.0.

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.

1 participant