Skip to content

TR-7946 Stop discarding domain types the generator can actually build - #135

Open
dzmitry-starastsenka-paysera wants to merge 2 commits into
paysera:masterfrom
dzmitry-starastsenka-paysera:AC-2509-restore-client-generation
Open

dzmitry-starastsenka-paysera wants to merge 2 commits into
paysera:masterfrom
dzmitry-starastsenka-paysera:AC-2509-restore-client-generation

Conversation

@dzmitry-starastsenka-paysera

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

Copy link
Copy Markdown

TR-7946

Client generation from paysera/api-spec is currently impossible — this affects every developer, not one team. The failure mode is silent and points at the wrong place, so it reads as your own mistake rather than a tool defect.

Concretely: @paysera/public-transfers-client cannot be regenerated at all. It 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), so frontend consumers have been reading raw payload keys instead of using the client.

The mechanism

TypeDefinitionBuilder::buildTypeDefinitions() asks each builder supports() and takes the first match. If the winning builder returns null, or if no builder claims the type, nothing is registered. DefinitionValidator::validateType() then looks the type up with $api->getType($name), gets null, and throws:

Did not found defined type "X"

The name in that message is the consumer of the missing type, never the type that was dropped. That is what makes this so hard to diagnose — fix one, the next appears, and it looks like a moving target.

What this PR fixes

1. MetadataTypeBuilder discarded any type whose name contained "metadata"

return strpos(strtolower($name), 'metadata') !== false
    || strpos(strtolower($name), 'meta data') !== false;

…paired with buildTypeDefinition() returning null. That is correct for ResultMetadata, the pagination envelope supplied by the REST client runtime rather than generated. But matching a substring of the name also swallows unrelated domain types.

Real casualty: AccountingMetadata in app-evpbank/public-transfers (added by TSP-148). The type is well-formed; only its name is unlucky. Demonstrated by A/B — identical content named AccountingMeta generates cleanly, while FooMetadata and MetadataFoo both fail.

Now matches ResultMetadata exactly. The comparison is made against the short name, because a type
coming from a library arrives qualified — supports() is called with Paysera.ResultMetadata, not
ResultMetadata. Spaces and case are still normalised, preserving the old "meta data" tolerance for that
one name.

2. Named scalar types were claimed by no builder at all

SimpleTypeBuilder::supports() requires isset($definition['properties']) || isset($definition['queryParameters']). A named scalar — a RAML DataType that is a primitive plus constraints, in its own fragment:

#%RAML 1.0 DataType
type: string
enum: [in_progress, pending, completed]

…has neither, and no other builder claims it. It is never registered, so every property referencing it fails validation.

Real casualties: ResolvedStatus (TR-7156) and TimelineMessageCode (TR-7561), both correctly declared in raml/types.raml. RAML 1.0 permits named scalars; the generator only understood object-shaped types.

Such an alias has no entity of its own to generate — properties pointing at it are the underlying primitive. resolveScalarAliases() rewrites those properties to the primitive and leaves the alias out of the generated output entirely.

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, and compared the full output trees:

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

This is the property that matters most: the two type categories being rescued are ones that previously aborted generation, so nothing that used to generate can change. ResultMetadata is the only *Metadata*-named type in the entire fixture set (914 files), so narrowing the match is provably inert here.

Forward — the real spec now gets past both defects

Against app-evpbank/public-transfers/api.raml:

Stage Result
before Did not found defined type "AccountingMetadata"
after passes AccountingMetadata, passes ResolvedStatus / TimelineMessageCode, stops at Did not found defined type "string | nil"

That last one is union support — deliberately not in this PR, see below.

⚠️ What I could not run

The repo's own PHPUnit suite was not executed. 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 before/after comparison above is my substitute, but CI must confirm the unit suite. Please treat that as a required check rather than a formality.

No fixture in the repo currently covers a named scalar type, so the new path has no golden-master coverage. Happy to add a fixture if you would like one — I left it out rather than hand-roll expected output I could not validate against a green suite.

Not in this PR

Two further blockers exist, both tracked under TR-7946:

  • Union types (X | nil) are unsupported — DefinitionValidator.php:83 throws 'UnionType currently is not supported', and in practice the type arrives unparsed and falls through to the same "Did not found defined type" message. api-spec uses | nil pervasively for nullable fields, so this alone still blocks public-transfers. Previously reported as INV-257 and closed "Won't Do" in Feb 2025. Kept separate because it is a feature, not a patch, and deserves its own decision. A separate PR will follow.
  • Missing example files in api-spec — fixed in paysera/api-spec!1789.

Note on direction

PLT-1813 records an intent to replace this generator with OpenAPI Generator. If that migration is close, the sensible outcome may be to take these two low-risk fixes (which unblock everyone today) and leave unions permanently unsupported. Flagging it so the call is explicit.

@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
dzmitry-starastsenka-paysera force-pushed the AC-2509-restore-client-generation branch from 56ef9c5 to bfe758a Compare July 31, 2026 12:43
@dzmitry-starastsenka-paysera

Copy link
Copy Markdown
Author

@mSprunskas , please review. Thank you.

@dzmitry-starastsenka-paysera dzmitry-starastsenka-paysera changed the title AC-2509 Stop discarding domain types the generator can actually build TR-7946 Stop discarding domain types the generator can actually build Aug 3, 2026

@mSprunskas mSprunskas 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.

  • Changes should be reflected in tests
  • See
1. High — Array item types that point to a named scalar type stay unchanged, thus generation continues to fail
  
  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/TypeDefinitionBuilder.php:117-131

  Problem: The new loop changes only properties that have the type PropertyDefinition::TYPE_REFERENCE. An array property keeps its item type in ArrayPropertyDefinition::$itemsType. Its own type is array. Therefore the loop does not touch it.

  DefinitionValidator.php:28-30 validates the item type:

  if ($property instanceof ArrayPropertyDefinition) {
      $this->validateType($property->getItemsType(), $api);
  }

  The named scalar type is not in $api->getTypes(). Thus validateType() throws UnrecognizedTypeException.

  Verification: Output of the test script:

  currency     type='string'   reference=NULL   items=-           constants=0
  currencies   type='array'    reference=NULL   items='Currency'  constants=0
  
  The property currency is corrected. The array property currencies keeps 'Currency'.

  Impact: The RAML pattern items: { type: <NamedScalarType> } is common. Test fixtures use this pattern (for example tests/JavascriptGeneratorBundle/Fixtures/raml/account/api.raml:21-22). Generation still stops with Did not found defined type "Currency". The
  fix does not do what the CHANGELOG promises.

  Fix: Add the item type to the same loop:

  if ($property instanceof ArrayPropertyDefinition && isset($scalarAliases[$property->getItemsType()])) {
      $property->setItemsType($scalarAliases[$property->getItemsType()]);
  }

  ---
  2. Medium — The alias list comes from the raw RAML data, not from the types that no builder made, thus a class can be generated and bypassed at the same time

  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/TypeDefinitionBuilder.php:97-111

  Problem: The method collects aliases from $apiTypes. It does not check whether a builder made a TypeDefinition for that name. FilterTypeDefinitionBuilder::supports() returns true for each name that contains the text Filter
  (FilterTypeDefinition::BASE_FILTER = 'Filter'). Its buildTypeDefinition() does not need properties. It returns a FilterTypeDefinition with an empty property list.

  Verification: Output of the test script:

  mode         type='string'   reference=NULL

  FilterTypeDefinitionBuilder->supports('FilterMode') = true
  buildTypeDefinition('FilterMode') returns ...\FilterTypeDefinition

  Impact: For a scalar type with a name such as FilterMode, CurrencyFilter or StatusFilter, the generator makes an empty class. At the same time all references to that class become string. The generated class is dead code. The two decisions do not agree.

  Fix: Make the alias list from the names that gave no TypeDefinition. Collect the built names first, then keep only the aliases that are not in that list.

  ---
  3. Medium — Named scalar types whose name contains "Result" stop generation before the new method starts
  
  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/TypeDefinitionBuilder.php:47-64

  Problem: resolveScalarAliases() runs at line 64. The builder loop runs before it, at lines 47-60. ResultTypeBuilder has position 20 and thus runs before SimpleTypeBuilder (position 100). It supports each name that contains Result and throws when the
  definition has no properties key.

  Verification:
  
  ResultTypeBuilder->supports('ScanResult', ['type' => 'string']) = true
  InvalidDefinitionException: ResultType definition must contain "properties" list

  Impact: A scalar type with a name such as ScanResult, ResultStatus or PaymentResultCode still stops generation. The exception occurs before the new code can help.

  Fix: Add a properties test to ResultTypeBuilder::supports(), or find the aliases before the builder loop and remove them from $apiTypes.

  ---
  4. Medium — Enumeration values are lost when an alias becomes a primitive type
  
  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/TypeDefinitionBuilder.php:125-128

  Problem: The method writes the primitive type and clears the reference. It does not copy the enum values of the alias into the property. PropertyDefinitionBuilder.php:56-58 makes constants only from the enum key of the property itself.

  Verification: The test script gives constants=0 for the property currency, although the alias Currency has enum: ['EUR', 'USD'].

  Impact: A RAML enumeration alias is the most usual named scalar type. The generated client loses all constants and all value control. The generated code compiles, but it accepts each string.

  Fix: Copy the enumeration values with ConstantBuilder when the alias has an enum key.

  ---
  5. Medium — Neither change has a test
  
  Files: src/Paysera/.../TypeDefinitionBuilder.php, src/Paysera/.../MetadataTypeBuilder.php

  Problem: The change adds 55 lines of new logic and changes the match rule of a builder. The change list holds no test file and no RAML fixture. A search of tests/ finds no fixture with a named scalar type. It also finds no unit test for
  TypeDefinitionBuilder or MetadataTypeBuilder.

  Impact: The gaps in findings #1, #2, #3 and #4 are not detected. A later change can break the new logic without a signal.

  Fix: Add a RAML fixture with a named scalar type. Use it as a property, as an array item and as a body type.

  ---
  6. Medium — .phpunit.result.cache is a build product and enters the repository

  File: .phpunit.result.cache

  Problem: git check-ignore gives exit code 1 for this file. Thus .gitignore does not exclude it. The file has 6034 bytes and holds local test results.

  Impact: The file changes with each test run. It causes noise in each merge request and can cause merge conflicts.

  Fix: Add .phpunit.result.cache to .gitignore. Do not commit the file.

  ---
  7. Medium — release.sh holds personal paths, has no interpreter line and cannot run

  File: release.sh

  Problems:
  - Line 1 and line 5 use paths of one developer: ~/Projects/api-spec/app-mokejimai/... and ~/Projects/js-lib-paysera-clients/.... Other developers do not have these directories.
  - The file has no #!/usr/bin/env bash line. Byte inspection with od -c shows that the file starts with bin/console.
  - The file permission is -rw-rw-r--. Thus the file is not executable. The command ./release.sh fails.
  - The file has no set -euo pipefail. If the first command fails, the second command still runs.
  - The name release.sh is not correct. The script does not make a release. It generates two JavaScript client packages.

  Impact: The script is a local helper file. It is not usable by other persons. It gives wrong information about its function.

  Fix: Do not commit this file, or make the paths parameters, add the interpreter line, add set -e, set the execute permission and use a correct name such as generate-js-clients.sh.

  ---
  8. Low — Bodies of requests and responses that use a named scalar type are still not accepted

  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/TypeDefinitionBuilder.php:117-131

  Problem: DefinitionValidator::validateResource() (lines 39-70) reads the body types from the raw RAML data with $body->getType(). The new method changes only the properties of the decorated types. It does not change the raw RAML data.

  Impact: A method that has a named scalar type as its body or its 200 response still causes UnrecognizedTypeException. This case is less common than the case in finding #1.

  Fix: Let validateType() accept the names that are in the alias list, or resolve the aliases in the raw RAML data.

  ---
  9. Low — MetadataTypeBuilder no longer removes names that use a separator character other than a space

  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/TypeDefinitionBuilder/MetadataTypeBuilder.php:21

  Problem: The old code found the text metadata at any position. Thus it matched Result_Metadata, because that name contains metadata. The new code removes only the space character before the comparison.

  Verification:

  supports(ResultMetadata      ) = TRUE (dropped)
  supports(rest.ResultMetadata ) = TRUE (dropped)
  supports(Result Meta Data    ) = TRUE (dropped)
  supports(Result_Metadata     ) = false (generated)

  Impact: If an API specification uses Result_Metadata or Result-Metadata, the generator now makes a class for it. The REST client runtime already supplies this class. This is a small risk, because the usual name is ResultMetadata.

  Fix: Remove _ and - also, for example with preg_replace('/[\s_-]+/', '', ...).

  ---
  10. Low — Two different classes supply the same constant values in one code block

  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/TypeDefinitionBuilder.php:105-107

  Problem: Line 105 calls TypeHelper::isPrimitiveType(). Lines 106 and 107 use PropertyDefinition::TYPE_OBJECT and PropertyDefinition::TYPE_ARRAY. The class TypeHelper has the same constants (TypeHelper::TYPE_OBJECT, TypeHelper::TYPE_ARRAY).

  Impact: The code is more difficult to read. The values are equal now, thus there is no defect.

  Fix: Use TypeHelper constants at this position, because the test on line 105 uses TypeHelper.

  ---
  11. Note — A non-ASCII character is in a source comment

  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/TypeDefinitionBuilder.php:85

  Problem: The comment holds an em dash (U+2014). All other files in src/ use ASCII characters only.

  Fix: Replace the em dash with a hyphen or a comma.

  ---
  12. Note — The version number is a patch increment, but the generated output changes

  File: CHANGELOG.md:8

  Problem: The new version is 11.11.6. The last tag is 11.11.5. The change to MetadataTypeBuilder causes the generator to make new classes for each type whose name contains metadata (for example TransferMetadata, DocumentMetaData). Before this change, the
  generator did not make these classes.

  Impact: Users who regenerate a client get new files. Their build can change. The project states that it follows Semantic Versioning (CHANGELOG.md:5).

  Fix: Use 11.12.0, or add a note in the CHANGELOG that the generated output changes.

class MetadataTypeBuilder implements TypeDefinitionBuilderInterface
{
/**
* Only the result envelope's own metadata type is skipped: it is supplied by the REST client

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.

Redundant comments

}

/**
* A named scalar type is an alias for a primitive with extra constraints, e.g.

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.

Redundant comment

MetadataTypeBuilder claimed every type whose name merely contains
"metadata" and returned null for it, so domain types such as
AccountingMetadata were silently dropped and every type referencing them
failed validation. Only the result envelope's own ResultMetadata is
supplied by the REST client runtime, so only that name is skipped now.

A named scalar type - a RAML DataType that is a primitive plus
constraints, with no properties - was claimed by no builder and never
became a TypeDefinition, which failed validation for every type
referencing it. Such an alias has no entity to generate, so it is taken
out of the type list before the builders run and the properties pointing
at it become the underlying primitive instead, keeping the enum values
as constants on the referencing property.

Resolving the aliases before the builder loop also keeps them away from
the builders that match on a name substring: ResultTypeBuilder used to
abort on a scalar named ScanResult, and FilterTypeDefinitionBuilder used
to emit an empty class for a scalar named FilterMode.

Covered by the named-scalar fixture in the Javascript and PHP REST
client suites, exercising an alias as a property, as an array item type,
with and without enum values, and a domain type named PaymentMetadata.
@dzmitry-starastsenka-paysera
dzmitry-starastsenka-paysera force-pushed the AC-2509-restore-client-generation branch from bfe758a to 918e380 Compare August 12, 2026 14:37
@dzmitry-starastsenka-paysera

Copy link
Copy Markdown
Author

Thanks for the review — reworked and force-pushed as 918e380.

Tests now exist, and the suite runs

The reason the first version had none: 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():

PHPUnit requires the "dom", "json", "libxml", "mbstring", "tokenizer", "xml", "xmlwriter" extensions,
but the "mbstring" extension is not available.

Running it inside a container that has the extension works and 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, which matches config.platform.php in composer.json. 44 tests, 1777 assertions, green (was 42 before this PR).

New fixture named-scalar, added to both the Javascript and the PHP REST client suites, covering an alias as a property, an alias as an array item type, aliases with and without enum, aliases named ScanResult and FilterMode, and a domain type PaymentMetadata.

Point by point

# Change
1 Fixed. ArrayPropertyDefinition::$itemsType is rewritten too.
2 Fixed, though not quite as suggested — see below.
3 Fixed by the same change as #2; ResultTypeBuilder::supports() is untouched.
4 Fixed. The alias's enum is carried onto the referencing property via ConstantBuilder. An enum declared on the property itself is more specific and wins.
5 Fixed — see above.
6 Not in this PR, but the .gitignore gap is real. Added. Detail below.
7 Not in this PR, and not in the repository at all. Detail below.
8 Not addressed — reasoning below.
9 Fixed: preg_replace('/[\s_-]+/', '', ...), so Result_Metadata and Result-Metadata are skipped again.
10 Fixed: TypeHelper::TYPE_OBJECT / TypeHelper::TYPE_ARRAY.
11 Fixed, and both docblocks you flagged inline are cut down to why-only.
12 Fixed: 11.12.0, with the output change spelled out in the entry.

On #2 and #3 — one change instead of two

Rather than filtering the alias list against the names that produced no TypeDefinition, the aliases are now resolved before the builder loop and removed from $apiTypes:

$scalarAliases = $this->collectScalarAliases($apiTypes);
$apiTypes = array_diff_key($apiTypes, $scalarAliases);

A named scalar has no entity to generate, so the honest model is that it never reaches the builders at all. That makes the contradiction in #2 structurally impossible — nothing can be both generated as a class and rewritten to a primitive — and it fixes #3 without adding a properties check to ResultTypeBuilder::supports(), which would have changed behaviour for real result types.

On #8 — deliberately out of scope

A named scalar used as a request or response body would need DefinitionValidator::validateType() to know about the alias list, and even then the generated method has no entity to build or hydrate — so it needs a decision about what such a method should even look like, not just a validation tweak. The fixture therefore covers the property and array-item cases but not the body case. Happy to take it as a follow-up if you have seen it in a real spec; I have not found one in api-spec.

On #6 and #7 — these are not in this branch

Both findings describe files this PR adds. It does not add them.

  • The PR is 3 source files plus the new tests. The head tree bfe758a5 (the revision you reviewed) contains 1709 paths and zero matches for either name; git ls-tree -r on both master and the branch returns nothing for them.
  • .phpunit.result.cache: your underlying point is still correct, just not about the diff — the file is created by running the suite and git check-ignore exits 1 for it. Now that this PR adds tests, that matters, so I have added it to .gitignore.
  • release.sh: git log --all -- release.sh is empty. The file has never existed in this repository, on any branch or in any commit.

One supporting detail in #1 is also off: tests/JavascriptGeneratorBundle/Fixtures/raml/account/api.raml:21-22 is cited as an existing fixture using the broken pattern, but those lines are items: { type: Account }, and Account is an object type, which generates fine. The defect itself was real — just not evidenced by that fixture, which is why the new one adds the case.

Evidence that each defect was real

Running the new fixture against the older revisions, same fixture, same command:

Revision Result
master InvalidDefinitionException: ResultType definition must contain "properties" list (#3)
bfe758a5 (what you reviewed) same failure (#3)
bfe758a5, fixture minus ScanResult/FilterMode UnrecognizedTypeException: Did not found defined type "Currency" — from the array item type (#1)
bfe758a5, also minus the array property generates, but Payment.js has no constants block at all — the Currency enum silently lost (#4)
918e380 (this revision) OK (13 tests, 392 assertions)

@mSprunskas mSprunskas 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.

 1. Medium — A named scalar used as a Result item type is not resolved

  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/TypeDefinitionBuilder.php:130-135

  Problem: resolveScalarAliases() reads only $type->getProperties(). ResultTypeDefinition keeps its item type in a different field, itemsType (Entity/Definition/ResultTypeDefinition.php:15). ResultTypeBuilder fills this field
  from $definition['properties'][$dataKey]['items']['type'] (ResultTypeBuilder.php:31). The new code does not change this field. The guard at line 152 tests instanceof ArrayPropertyDefinition, and a ResultTypeDefinition is not
  a property. Thus the alias name stays.

  Impact: For this RAML:
  CurrencyResult:
    type: Paysera.Result
    properties:
      currencies:
        type: array
        items:
          type: Currency
  Result.php.twig:9 writes return new Currency($data);. The Currency class is never generated, because the alias is removed at line 52. UsedTypesResolver::filterOutTypes() also drops Currency, so no import is written. The
  generated client has a fatal error at run time. DefinitionValidator does not check ResultTypeDefinition::itemsType, so the generator gives no error message.

  Fix: In resolveScalarAliases(), also test each type for ResultTypeDefinition and replace itemsType with the primitive type, in the same way as line 154.

  ---
  2. Medium — Metadata types with "Result" in the name now go to ResultTypeBuilder
  
  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/TypeDefinitionBuilder/MetadataTypeBuilder.php:15-22

  Problem: supports() now accepts only the exact short name resultmetadata. A domain type such as SearchResultMetadata or TransferResultMetadata gives searchresultmetadata, which is not equal. The type then goes to the next
  builder. ResultTypeBuilder::supports() (position 20) accepts every name that contains Result (ResultTypeBuilder.php:14).

  Impact: Two failure modes:
  - The type has properties. The generator makes a ResultTypeDefinition, and dataKey becomes the first property name. The output is a wrong Result subclass, not an entity. There is no error message.
  - The type has no properties. ResultTypeBuilder::buildTypeDefinition() throws InvalidDefinitionException and all generation stops.

  Before this change, these types were removed and were not generated. The change makes the output wrong instead of absent.

  Fix: Test the name against ResultTypeBuilder before MetadataTypeBuilder releases it, or make ResultTypeBuilder::supports() more exact (for example, a name that ends with Result).

  ---
  3. Low — Named scalars still stop generation when used as a body type
  
  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/DefinitionValidator.php:70-104

  Problem: The fix resolves aliases only in type properties. A body type is validated from the RAML tree:
  body:
    application/json:
      type: Currency
  validateType() resolves the proxy to the name Currency, then calls $api->getType('Currency'), which returns null because line 52 removed the alias. The method throws UnrecognizedTypeException.

  Impact: The CHANGELOG says that named scalar types "no longer break generation for every type referencing them". This is not true for request bodies, response bodies, and inline nested object properties. The user gets the
  message Did not found defined type "Currency", which does not identify the cause.

  Fix: Resolve aliases for body types too, or limit the CHANGELOG text to properties and array item types.

  ---
  4. Low — release.sh is a personal scratch file and is not ignored

  File: release.sh:1-5

  Problem: The file holds two bin/console js-generator:package commands with absolute personal paths (~/Projects/api-spec/..., ~/Projects/js-lib-paysera-clients/...). It has no shebang line, no set -e, and no execute
  permission (-rw-rw-r--). The name says "release", but the file does not release anything. git check-ignore shows that .gitignore does not cover it.

  Impact: A git add . puts this file in the repository. Other developers get a broken script with paths that do not exist on their machines.

  Fix: Delete the file, or add it to .gitignore.

  ---
  5. Low — The SymfonyBundle generator has no test for the new behaviour

  File: tests/PhpGeneratorBundle/RestClient/GenerateRestClientCommandTest.php:86, tests/JavascriptGeneratorBundle/GeneratePackageCommandTest.php:93

  Problem: The named-scalar case was added to the REST client test and to the JavaScript test. The Symfony bundle generator (tests/PhpGeneratorBundle/SymfonyBundle/GenerateBundleCommandTest.php) uses the same
  TypeDefinitionBuilder service, but it has no named-scalar fixture.

  Impact: The alias removal and the metadata change also change the entities and the normalizers of the Symfony bundle generator. No test protects that output.

  Fix: Add a named-scalar fixture to the SymfonyBundle test data provider.

  ---
  6. Note — Dead condition in collectScalarAliases()

  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/TypeDefinitionBuilder.php:104

  Problem: The test is_array($definition) is always true. TypeCollection::toArray() returns name => array for each item (vendor/.../src/TypeCollection.php:197-205), and TypeDefinitionBuilderInterface::supports() already
  declares array $definition.

  Fix: Remove the condition.
  
  ---
  7. Note — Documentation is not complete for the new private methods

  File: src/Paysera/Bundle/CodeGeneratorBundle/Service/TypeDefinitionBuilder.php:20, 137, 163

  Problem: The new property $constantBuilder has no @var tag, but $builders above it has one. resolveScalarAlias() and applyAliasConstants() have no @param tags, but collectScalarAliases() and resolveScalarAliases() have them.
  The tag @return array name => definition at line 97 is not a correct docblock format.

  Fix: Use the same documentation style in all methods of the file.

  ---
  8. Note — The CHANGELOG entry does not show the behaviour change

  File: CHANGELOG.md:8-11


  File: CHANGELOG.md:8-11

  Problem: Both entries are under ### Fixed. The first entry says that a new client generation "can add files". This is a change of behaviour, not only a correction.

  Fix: Move the metadata entry to a ### Changed section. Keep a Changelog uses Changed for this type of entry.

Follow-up to review of 918e380.

- Resolve a named scalar used as the item type of a result envelope.
  ResultTypeDefinition keeps that type on the type itself rather than on a
  property, so the property walk never reached it and createItem() referenced
  a class that is never generated. Both Result templates already return the
  payload untouched for a primitive, so the rewrite is enough.
- Stop ResultTypeBuilder claiming names that end in "Metadata". Its match is a
  loose substring, so releasing SearchResultMetadata from MetadataTypeBuilder
  handed it to the result builder, which shaped it as an envelope instead of an
  entity. No api-spec type is affected today: the only name holding both words
  is ResultMetadata, still skipped at position 10.
- Drop the always-true is_array() guard, align docblocks, and split the
  behaviour change out of "Fixed" into "Changed" in the changelog. The named
  scalar entry now states that request and response bodies stay unsupported.
- Cover both cases in the named-scalar fixture and register that fixture with
  the Symfony bundle suite as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dzmitry-starastsenka-paysera

Copy link
Copy Markdown
Author

@mSprunskas , Thanks for the second pass. Pushed as babceac. Six of the eight are addressed, one I scoped rather than implemented, and one I don't think applies to this branch.

Before the per-item replies, the measurement that shaped my triage, because it changes how several of these should be weighted.

Regression sweep over the whole spec corpus

I generated a PHP REST client from every one of the 263 api.raml files in api-spec, on master and on this branch, and diffed the two output trees. Run twice — once against 918e380, once against babceac.

  • No spec regressed. Every spec that generates on both sides produces byte-identical output. That is the property I most wanted for the #2 fix, because a substantial number of spec types named *Result* are plain entities that ride on the loose name match, and the guard had to leave every one of them alone.
  • One spec that failed on master now generates. It declares a domain type literally named Metadata — a plain counters object — which master discards, so generation died on Did not found defined type "Metadata". That is a second real beneficiary besides public-transfers.
  • app-evpbank/public-transfers advances exactly as intended: AccountingMetadata (P2) → string | nil (P4, PR TR-7946 Generate nullable union types instead of aborting #136).

Reproduce with php-generator:rest-client <api.raml> <out> <Ns> --library_name=x --library_version=1.0.0 --platform_version=7.4. Two notes if you run it yourself. Some specs need those options and some don't — where they are needed, omitting them dies inside ApiMethodExtension::getLibraryVersion() on a return-type error, which reads like a generator defect and isn't one. And a fair number of specs !include their library over the network, so a long sweep will start collecting HTTP 429 from raw.githubusercontent.com; those failures are transient, land on either side at random, and are unrelated to the diff. I re-ran every spec that differed and they pass on both sides.

Fixed

#1 — named scalar as a result item type. Confirmed and fixed. ResultTypeDefinition keeps the wrapped type in itemsType on the type rather than on a property, so the property walk never reached it. resolveScalarAliases() now rewrites it too, via a small resolveResultItemsAlias(). Both Result.php.twig and Result.js.twig already branch on is_scalar_type, so the alias collapsing to string makes createItem() return the payload untouched, which is the correct shape. Verified in the generated fixture: return $data; where it previously emitted return new Currency($data); against a class that is never generated.

One correction on the framing, since it affects how the rest of the review should be read: this is not caused by this PR. On master the identical fixture produces the identical broken output. I fixed it because it is cheap and the result is genuinely broken, not because this branch introduced it.

#2 — metadata types whose name contains "Result". Real, and the one item here I'd call a genuine regression, so thank you for catching it. Reproduced both modes: with no array property it dies with a TypeError in BaseExtension::isScalarType() (null itemsType); with one it silently emits a property-less class — a client that compiles and quietly loses every field. master raised a clear error in both cases, so loud became silent.

Fixed on the ResultTypeBuilder side rather than the metadata side:

return strpos($name, 'Result') !== false
    && preg_match(self::PATTERN_METADATA_SUFFIX, $name) !== 1
    && !array_key_exists(self::ANNOTATION_ENTITY, $definition);

I deliberately did not take the other option you offered — making supports() stricter in general, e.g. "ends with Result". A substantial number of spec types named *Result* are plain entities (not type: Paysera.Result, no _metadata) and depend on that loose substring match today. Narrowing it is a much larger change than this ticket, and the looseness is pre-existing on master. The suffix guard, by contrast, is inert on the current corpus: the only type name that both contains "Result" and ends in "Metadata" is ResultMetadata itself, which MetadataTypeBuilder still skips at position 10. (MetadataResult contains both words but ends in "Result", so it is untouched — the sweep confirms its output is unchanged.)

#5 — Symfony bundle coverage. Added; the named-scalar fixture is now registered with that suite too.

Two notes. The file is GenerateSymfonyBundleCommandTest.php — GenerateBundleCommandTest.php doesn't exist on this branch, so I assume that was a slip. And I ran the bundle generator against the fixture before adding it: it already handled named scalars correctly ($currency, $scanResult, $filterMode all typed string, the metadata-named entity generated), so this was a coverage gap rather than a hidden defect.

#6 — dead is_array(). Removed. Agreed: both TypeCollection::toArray() and TraitCollection::toArray() return name => array, and the pre-existing builder loop already type-hints array $definition, so it could never be false.

#7 — docblocks. Added the missing @var and @param tags and fixed the malformed @return.

One thing I'd like a ruling on, because it points the other way from your two inline comments on the previous round — "Redundant comments" on the MetadataTypeBuilder docblock and "Redundant comment" on the TypeDefinitionBuilder one. Both of those blocks are still present, reworded. Tell me which convention you want and I'll apply it consistently in one pass: full tags throughout, or prose only where the reasoning isn't obvious from the code.

#8 — changelog. The metadata entry moved to a ### Changed section, per Keep a Changelog.

Scoped rather than implemented

#3 — named scalar as a body type. Confirmed: Did not found defined type "Currency", aborting generation — precisely the misleading message this ticket set out to remove. master fails identically.

I took the second option from your own comment and limited the changelog wording instead of widening the fix, for two reasons. Supporting it means resolving aliases in the raw RAML tree that DefinitionValidator::validateResource() reads, which is a structurally different change from rewriting built definitions. And there is no caller today: the only named scalars in the entire corpus are ResolvedStatus and TimelineMessageCode, both type: string, and both are used only as plain properties — never as a body, never as an array item, never as a result's items. So it is a latent gap rather than a live one, and the changelog now says so explicitly. Happy to open a separate ticket if you'd rather have it tracked.

Couldn't action

#4 — release.sh. I don't think this one is from this branch, so there's nothing for me to change. On 918e380:

  • not in the tree — git ls-tree -r --name-only 918e380 | grep -i release returns only src/Paysera/Bundle/ClientReleaseBundle/** PHP sources
  • never existed — git log --all -- release.sh is empty
  • not among the files this PR touches, and not in my working tree either

A local worktree or a different checkout would explain it. No action needed from you — just flagging why that one is unaddressed, in case it looks skipped. Happy to fix it immediately if you can point me at where it lives.

The neighbouring round-1 item that was real — .phpunit.result.cache being untracked — is fixed; it's in .gitignore now.

Also from round one

Version is 11.12.0, TypeHelper::TYPE_* constants are used at the type checks, and the em dash is gone (both changed files are 0 non-ASCII bytes).

Verification

  • bin/phpunit: 45 tests, 1859 assertions, green (44 before; +1 from the Symfony bundle case). This also closes out the caveat in my original PR description — I couldn't run the suite locally then, and now can, so the fixtures are validated against a green run rather than hand-rolled.
  • Fixtures now cover a named scalar used as a property, as an array item, as a result's item type, one claimed by ResultTypeBuilder (ScanResult), one claimed by FilterTypeDefinitionBuilder (FilterMode), a domain type merely containing "metadata", and one both containing "Result" and ending in "Metadata".
  • bin/raml-code-generator boots.
  • The 263-spec sweep above.

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.

2 participants