Repository navigation
Keep the client compatible across ThingsBoard OpenAPI type corrections - #86
Conversation
ThingsBoard corrects OpenAPI types that the generated client got wrong (thingsboard/thingsboard#16561). Regenerated as is, the client would stop compiling for code written against 4.4.0. This adds a compatibility layer: - scripts/apply_client_compat.py copies the spec with vendor extensions from openapi/client-compat.json before generation: - pageSize/page of 28 operations that 4.4.0 took as String: api.mustache adds deprecated String overloads (builder and positional) that delegate to the Integer ones; - x-parent on Alarm, so AlarmInfo (allOf Alarm) extends Alarm and assignAlarm/unassignAlarm results stay assignable to Alarm. - model/AliasEntityId is hand-written in common/ and replaces the generated model. The server writes an EntityType or an AliasEntityType name in one entityType field; the class keeps the typed accessors of 4.4.0 and can now send and read aliases (CURRENT_USER, ...). With the current specs the generated code is unchanged except for AliasEntityId.
Documenting AlarmInfo as allOf Alarm in the server spec would turn Alarm into an abstract interface in the Dart client (dart-dio), so the spec stays flat. apply_client_compat.py now rewrites listed subtypes (client-compat.json "subtypes") into allOf [parent, own properties] and marks the parent with x-parent before generation, so the Java AlarmInfo extends Alarm with the current specs already. Child classes now also override add<Name>Item / put<Name>Item of inherited list and map properties, so these fluent calls keep returning the child type, as the other inherited fluent setters already do. AliasEntityId keeps JSON_PROPERTY_ALIAS_ENTITY_TYPE as a deprecated constant.
vvlladd28
left a comment
There was a problem hiding this comment.
Review summary
Reviewed 99 changed files in Keep the client compatible across ThingsBoard OpenAPI type corrections. Most of them are regenerated models. I read the hand-written files closely and compared the PE/PaaS output with CE. Left 5 comments inline.
The overall approach holds up. I checked it against the v4.4 tag. The 28 operations in client-compat.json are exactly the PE/PaaS operations that took String pageSize/page in 4.4.0. Every 4.4.0 public AlarmInfo member is still present, declared or inherited. The hand-written AliasEntityId keeps the full 4.4.0 surface.
One real gap: the String overloads cover op(...) and the *Args builders, but not the public positional opWithHttpInfo(String pageSize, String page, ..., Map<String, String> headers). 4.4.0 code that calls those methods will stop compiling after the spec update (openapi/api.mustache:357). The *Args accessors String pageSize() / page() will also change type. Those can't be shimmed, so they belong in the known-breaks note next to ThingsboardErrorCode.getValue().
This review was auto-generated. Findings may contain errors — please verify before applying changes.
4.4.0 also had public positional <operation>WithHttpInfo methods with String pageSize/page (28 in PE). Operations with x-legacy-string-params now get a deprecated WithHttpInfo overload that takes String for these parameters and headers, converts them and delegates to the Integer one. No generated change with the current specs.
setEntityType(null) cleared an alias set with setAliasEntityType, and vice versa, since both write the one entityType field. Copying properties one by one (e.g. BeanUtils: aliasEntityType, then entityType) lost the alias. Now setEntityType(null) clears only an entity type and setAliasEntityType(null) only an alias.
…json apply_client_compat.py silently skipped every mismatch, so a spec change could drop a compatibility overload unnoticed. It now fails, naming the config entry, when a subtype schema is missing, the child does not have every parent property with the same definition (listing them), the child already uses allOf in another shape than the script produces, an operation is not in the spec, or a listed parameter is not a query parameter or is neither a string nor an int32 integer. A parameter still documented as a string is counted as pending. The summary line shows the edition, subtypes and marked/pending parameters. generate-client.sh runs it for PE and PaaS only: CE is deprecated, its spec is frozen and has none of the listed operations, so CE is generated from the spec unchanged (AlarmInfo stays as in 4.4.0 there).
ClientCompatibilityTest (PE, PaaS) is written against the 4.4.0 client: String pageSize/page in the Args builder, the positional method and the positional WithHttpInfo method, Alarm results of assignAlarm and unassignAlarm, AlarmInfo fluent setters and the AliasEntityId accessors. It must keep compiling when the client is regenerated from specs with corrected types. API calls go to a closed port.
What
Keeps code written against thingsboard-*-client 4.4.0 compiling once the client is regenerated from specs with the type corrections of thingsboard/thingsboard#16561. It works with the current specs too, so it can be merged before the spec update.
scripts/apply_client_compat.py(run bygenerate-client.shbefore generating PE and PaaS) copies the spec to<edition>/target/openapi-client.jsonwith vendor extensions fromopenapi/client-compat.json:legacyStringParams:pageSize/pageof the 28 PE/PaaS operations that 4.4.0 took asString(getUserAssets,getEntityGroupsByTypeAndPageLink,getIntegrations, ...). Once the spec documents them asint32, they getx-legacy-string, andapi.mustacheadds deprecated overloads that convert and delegate:Builder pageSize(String)/page(String)in the*Argsbuilders;Stringfor these parameters;<operation>WithHttpInfo(..., String pageSize, String page, ..., Map<String, String> headers)overload (4.4.0 had these as public methods too).subtypes:{"AlarmInfo": "Alarm"}. On the serverAlarmInfo extends Alarm, but the spec describes two unrelated schemas, and #16561 makesassignAlarm/unassignAlarmreturnAlarmInfo. The script rewritesAlarmInfointoallOf: [Alarm, its own properties]and marksAlarmwithx-parent, soAlarmInfo extends AlarmandAlarm a = client.assignAlarm(...)still compiles. This is done here rather than in the server spec because dart-dio turns anallOfparent into an abstract interface (Alarm(...)would no longer compile in Dart).readOnlyproperties stay in the child: openapi-generator 7.20 generates code that assigns the parent's private fields for inheritedreadOnlyproperties.allOfin another shape than the script produces, anoperationIdis not in the spec, or a listed parameter is not a query parameter of it (or is neither a string nor anint32). A parameter still documented as a string is counted as pending:Client compat (pe): AlarmInfo -> Alarm; 28/28 operations, 0/56 params marked, 56 pending (still string).AlarmInfostays as in 4.4.0).pojo.mustache: a child class now also overridesadd<Name>Item/put<Name>Itemof inherited list and map properties, as it already does for the other fluent setters. Without it,AlarmInfo.addPropagateRelationTypesItem(...)would returnAlarminstead ofAlarmInfoas in 4.4.0.model/AliasEntityIdis hand-written incommon/and replaces the generated model (generate-client.shalready copiescommon/on top). The server writes anEntityTypeor anAliasEntityTypename in oneentityTypefield ({"entityType": "CURRENT_USER"}), and #16561 documents it as a string. The class keeps the 4.4.0 API (entityType(EntityType),getEntityType(),aliasEntityType(AliasEntityType),getAliasEntityType(),id(UUID)) on top of that field, so code that uses entity IDs is unchanged, and aliases can now be sent and read. It also leaves out"id": null, which the server rejects for an alias, and keepsJSON_PROPERTY_ALIAS_ENTITY_TYPEas a deprecated constant.setEntityType(null)clears only an entity type andsetAliasEntityType(null)only an alias, so copying properties one by one (e.g.BeanUtils.copyProperties) keeps an alias.Source-incompatible changes that remain after #16561:
ThingsboardErrorCode.getValue():Integerinstead ofBigDecimal; the enum constants (NUMBER_2, ...) are the same.*Args.pageSize()/page()accessors of the 28 operations returnIntegerinstead ofString(a method cannot be overloaded by return type).nullforpageSize/page(pageSize(null),getUserAssets(null, null, ...)) becomes ambiguous between theStringandIntegeroverloads. These parameters are required, so such calls already failed at runtime.Testing
./generate-client.sh allwith the current specs changes only:AliasEntityId(hand-written);AlarmInfo(PE, PaaS), which now extendsAlarm. Every public member of the 4.4.0AlarmInfois still there, declared or inherited, with the same signature (compared withjavap);add<Name>Item/put<Name>Itemoverrides in existing child classes.mvn clean packagepasses for all editions.AliasEntityIdTest(common): JSON of an entity ID and of an alias, reading server JSON,SingleEntityFilterwith an alias,set*(null)keeping a value of the other kind.ClientCompatibilityTest(PE, PaaS), written against the 4.4.0 API:GetUserAssetsArgs.builder().pageSize("10").page("0"),getUserAssets("10", "0", ...),getUserAssetsWithHttpInfo("10", "0", ..., headers),Alarm a = api.assignAlarm(...)/unassignAlarm(...),AlarmInfo.addPropagateRelationTypesItem(...)returningAlarmInfo, the 4.4.0AliasEntityIdmethods. It must keep compiling after regeneration; API calls go to a closed port.apply_client_compat.pyon copies of the PE spec with a renamed operation, a renamed parameter, anint64parameter, an extra/changedAlarmproperty, a missingAlarmand a differently shapedallOffails with the entry named; re-running it on its own output is accepted.AlarmInfoleft flat, as the server documents it) to the PE spec (56/56 params marked), generated the client and compiledClientCompatibilityTestand a test that uses the 4.4.0 API (without the newWithHttpInfooverloads,getUserAssetsWithHttpInfo("10", "0", ...)does not compile):GetUserAssetsArgs.builder().pageSize("100").page("0")andgetUserAssets("100", "0", ...)compile and convert toInteger;Alarm a = client.assignAlarm(...)compiles; anAlarmInforead from JSON exposesnameandcreatedTimethrough anAlarmreference and round-trips;AliasEntityIdandThingsboardErrorCode.NUMBER_10behave as above.