From ef17f8c218697ff30d6156a12ce04d522a5534b6 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 2 Jun 2026 11:43:01 -0500 Subject: [PATCH 1/3] parse dates via generated per-type parse functions --- oxide-api/src/Api.ts | 191 ++++++++++- oxide-api/src/date-parsers.ts | 319 ++++++++++++++++++ oxide-api/src/http-client.ts | 8 +- oxide-api/src/util.ts | 24 +- oxide-openapi-gen-ts/src/__snapshots__/Api.ts | 191 ++++++++++- .../src/__snapshots__/date-parsers.ts | 315 +++++++++++++++++ .../src/__snapshots__/http-client.ts | 10 +- .../src/__snapshots__/util.ts | 24 +- oxide-openapi-gen-ts/src/client/api.ts | 15 +- .../src/client/dateParsers.ts | 263 +++++++++++++++ .../src/client/static/http-client.test.ts | 34 +- .../src/client/static/http-client.ts | 10 +- .../src/client/static/util.test.ts | 54 +-- .../src/client/static/util.ts | 24 +- oxide-openapi-gen-ts/src/gen.test.ts | 69 ++++ oxide-openapi-gen-ts/src/generate.ts | 11 +- 16 files changed, 1434 insertions(+), 128 deletions(-) create mode 100644 oxide-api/src/date-parsers.ts create mode 100644 oxide-openapi-gen-ts/src/__snapshots__/date-parsers.ts create mode 100644 oxide-openapi-gen-ts/src/client/dateParsers.ts diff --git a/oxide-api/src/Api.ts b/oxide-api/src/Api.ts index e6e64b5..25382de 100644 --- a/oxide-api/src/Api.ts +++ b/oxide-api/src/Api.ts @@ -16,6 +16,7 @@ import { toQueryString, } from "./http-client"; import { snakeify } from "./util"; +import * as P from "./date-parsers"; export type { ApiResult, ErrorBody, ErrorResult } from "./http-client"; @@ -7600,6 +7601,7 @@ export class Api { path, query, host, + parseResponse, ...fetchParams }: FullParams): Promise> { const url = (host || this.host) + path + toQueryString(query); @@ -7607,7 +7609,7 @@ export class Api { ...mergeParams(this.baseParams, fetchParams), body: JSON.stringify(snakeify(body), dateReplacer), }; - return handleResponse(await fetch(url, init)); + return handleResponse(await fetch(url, init), parseResponse); } methods = { @@ -7671,6 +7673,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -7719,6 +7722,7 @@ export class Api { path: `/experimental/v1/system/support-bundles`, method: "GET", query, + parseResponse: P.n22, ...params, }); }, @@ -7733,6 +7737,7 @@ export class Api { path: `/experimental/v1/system/support-bundles`, method: "POST", body, + parseResponse: P.n8, ...params, }); }, @@ -7746,6 +7751,7 @@ export class Api { return this.request({ path: `/experimental/v1/system/support-bundles/${path.bundleId}`, method: "GET", + parseResponse: P.n8, ...params, }); }, @@ -7763,6 +7769,7 @@ export class Api { path: `/experimental/v1/system/support-bundles/${path.bundleId}`, method: "PUT", body, + parseResponse: P.n8, ...params, }); }, @@ -7868,6 +7875,7 @@ export class Api { path: `/v1/affinity-groups`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -7886,6 +7894,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -7906,6 +7915,7 @@ export class Api { path: `/v1/affinity-groups/${path.affinityGroup}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -7929,6 +7939,7 @@ export class Api { method: "PUT", body, query, + parseResponse: P.n0, ...params, }); }, @@ -8057,6 +8068,7 @@ export class Api { path: `/v1/alert-receivers`, method: "GET", query, + parseResponse: P.n11, ...params, }); }, @@ -8070,6 +8082,7 @@ export class Api { return this.request({ path: `/v1/alert-receivers/${path.receiver}`, method: "GET", + parseResponse: P.n10, ...params, }); }, @@ -8103,6 +8116,7 @@ export class Api { path: `/v1/alert-receivers/${path.receiver}/deliveries`, method: "GET", query, + parseResponse: P.n6, ...params, }); }, @@ -8123,6 +8137,7 @@ export class Api { path: `/v1/alert-receivers/${path.receiver}/probe`, method: "POST", query, + parseResponse: P.n7, ...params, }); }, @@ -8190,6 +8205,7 @@ export class Api { path: `/v1/anti-affinity-groups`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -8211,6 +8227,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -8231,6 +8248,7 @@ export class Api { path: `/v1/anti-affinity-groups/${path.antiAffinityGroup}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -8254,6 +8272,7 @@ export class Api { method: "PUT", body, query, + parseResponse: P.n0, ...params, }); }, @@ -8392,6 +8411,7 @@ export class Api { path: `/v1/certificates`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -8406,6 +8426,7 @@ export class Api { path: `/v1/certificates`, method: "POST", body, + parseResponse: P.n0, ...params, }); }, @@ -8419,6 +8440,7 @@ export class Api { return this.request({ path: `/v1/certificates/${path.certificate}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -8446,6 +8468,7 @@ export class Api { path: `/v1/disks`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -8461,6 +8484,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -8478,6 +8502,7 @@ export class Api { path: `/v1/disks/${path.disk}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -8595,6 +8620,7 @@ export class Api { path: `/v1/external-subnets`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -8613,6 +8639,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -8633,6 +8660,7 @@ export class Api { path: `/v1/external-subnets/${path.externalSubnet}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -8656,6 +8684,7 @@ export class Api { method: "PUT", body, query, + parseResponse: P.n0, ...params, }); }, @@ -8699,6 +8728,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -8719,6 +8749,7 @@ export class Api { path: `/v1/external-subnets/${path.externalSubnet}/detach`, method: "POST", query, + parseResponse: P.n0, ...params, }); }, @@ -8733,6 +8764,7 @@ export class Api { path: `/v1/floating-ips`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -8751,6 +8783,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -8768,6 +8801,7 @@ export class Api { path: `/v1/floating-ips/${path.floatingIp}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -8791,6 +8825,7 @@ export class Api { method: "PUT", body, query, + parseResponse: P.n0, ...params, }); }, @@ -8834,6 +8869,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -8854,6 +8890,7 @@ export class Api { path: `/v1/floating-ips/${path.floatingIp}/detach`, method: "POST", query, + parseResponse: P.n0, ...params, }); }, @@ -8868,6 +8905,7 @@ export class Api { path: `/v1/groups`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -8881,6 +8919,7 @@ export class Api { return this.request({ path: `/v1/groups/${path.groupId}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -8895,6 +8934,7 @@ export class Api { path: `/v1/images`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -8913,6 +8953,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -8930,6 +8971,7 @@ export class Api { path: `/v1/images/${path.image}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -8964,6 +9006,7 @@ export class Api { path: `/v1/images/${path.image}/demote`, method: "POST", query, + parseResponse: P.n0, ...params, }); }, @@ -8981,6 +9024,7 @@ export class Api { path: `/v1/images/${path.image}/promote`, method: "POST", query, + parseResponse: P.n0, ...params, }); }, @@ -8995,6 +9039,7 @@ export class Api { path: `/v1/instances`, method: "GET", query, + parseResponse: P.n21, ...params, }); }, @@ -9013,6 +9058,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n20, ...params, }); }, @@ -9030,6 +9076,7 @@ export class Api { path: `/v1/instances/${path.instance}`, method: "GET", query, + parseResponse: P.n20, ...params, }); }, @@ -9053,6 +9100,7 @@ export class Api { method: "PUT", body, query, + parseResponse: P.n20, ...params, }); }, @@ -9090,6 +9138,7 @@ export class Api { path: `/v1/instances/${path.instance}/affinity-groups`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9110,6 +9159,7 @@ export class Api { path: `/v1/instances/${path.instance}/anti-affinity-groups`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9130,6 +9180,7 @@ export class Api { path: `/v1/instances/${path.instance}/disks`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9153,6 +9204,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -9176,6 +9228,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -9196,6 +9249,7 @@ export class Api { path: `/v1/instances/${path.instance}/external-ips`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9219,6 +9273,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -9259,6 +9314,7 @@ export class Api { path: `/v1/instances/${path.instance}/external-subnets`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9279,6 +9335,7 @@ export class Api { path: `/v1/instances/${path.instance}/multicast-groups`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9302,6 +9359,7 @@ export class Api { method: "PUT", body, query, + parseResponse: P.n0, ...params, }); }, @@ -9339,6 +9397,7 @@ export class Api { path: `/v1/instances/${path.instance}/reboot`, method: "POST", query, + parseResponse: P.n20, ...params, }); }, @@ -9379,6 +9438,7 @@ export class Api { path: `/v1/instances/${path.instance}/ssh-public-keys`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9396,6 +9456,7 @@ export class Api { path: `/v1/instances/${path.instance}/start`, method: "POST", query, + parseResponse: P.n20, ...params, }); }, @@ -9413,6 +9474,7 @@ export class Api { path: `/v1/instances/${path.instance}/stop`, method: "POST", query, + parseResponse: P.n20, ...params, }); }, @@ -9427,6 +9489,7 @@ export class Api { path: `/v1/internet-gateway-ip-addresses`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9448,6 +9511,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -9482,6 +9546,7 @@ export class Api { path: `/v1/internet-gateway-ip-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9503,6 +9568,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -9537,6 +9603,7 @@ export class Api { path: `/v1/internet-gateways`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9558,6 +9625,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -9578,6 +9646,7 @@ export class Api { path: `/v1/internet-gateways/${path.gateway}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -9612,6 +9681,7 @@ export class Api { path: `/v1/ip-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9625,6 +9695,7 @@ export class Api { return this.request({ path: `/v1/ip-pools/${path.pool}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -9662,6 +9733,7 @@ export class Api { return this.request({ path: `/v1/me`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -9676,6 +9748,7 @@ export class Api { path: `/v1/me/access-tokens`, method: "GET", query, + parseResponse: P.n19, ...params, }); }, @@ -9703,6 +9776,7 @@ export class Api { path: `/v1/me/groups`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9717,6 +9791,7 @@ export class Api { path: `/v1/me/ssh-keys`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9731,6 +9806,7 @@ export class Api { path: `/v1/me/ssh-keys`, method: "POST", body, + parseResponse: P.n0, ...params, }); }, @@ -9744,6 +9820,7 @@ export class Api { return this.request({ path: `/v1/me/ssh-keys/${path.sshKey}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -9774,6 +9851,7 @@ export class Api { path: `/v1/metrics/${path.metricName}`, method: "GET", query, + parseResponse: P.n26, ...params, }); }, @@ -9788,6 +9866,7 @@ export class Api { path: `/v1/multicast-groups`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9801,6 +9880,7 @@ export class Api { return this.request({ path: `/v1/multicast-groups/${path.multicastGroup}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -9821,6 +9901,7 @@ export class Api { path: `/v1/multicast-groups/${path.multicastGroup}/members`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9835,6 +9916,7 @@ export class Api { path: `/v1/network-interfaces`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9856,6 +9938,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -9876,6 +9959,7 @@ export class Api { path: `/v1/network-interfaces/${path.interface}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -9899,6 +9983,7 @@ export class Api { method: "PUT", body, query, + parseResponse: P.n0, ...params, }); }, @@ -9967,6 +10052,7 @@ export class Api { path: `/v1/projects`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -9981,6 +10067,7 @@ export class Api { path: `/v1/projects`, method: "POST", body, + parseResponse: P.n0, ...params, }); }, @@ -9994,6 +10081,7 @@ export class Api { return this.request({ path: `/v1/projects/${path.project}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -10008,6 +10096,7 @@ export class Api { path: `/v1/projects/${path.project}`, method: "PUT", body, + parseResponse: P.n0, ...params, }); }, @@ -10065,6 +10154,7 @@ export class Api { path: `/v1/snapshots`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -10083,6 +10173,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -10100,6 +10191,7 @@ export class Api { path: `/v1/snapshots/${path.snapshot}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -10131,6 +10223,7 @@ export class Api { path: `/v1/subnet-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -10144,6 +10237,7 @@ export class Api { return this.request({ path: `/v1/subnet-pools/${path.pool}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -10158,6 +10252,7 @@ export class Api { path: `/v1/system/audit-log`, method: "GET", query, + parseResponse: P.n13, ...params, }); }, @@ -10172,6 +10267,7 @@ export class Api { path: `/v1/system/hardware/disks`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -10185,6 +10281,7 @@ export class Api { return this.request({ path: `/v1/system/hardware/disks/${path.diskId}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -10205,6 +10302,7 @@ export class Api { path: `/v1/system/hardware/rack-switch-port/${path.rackId}/${path.switchSlot}/${path.port}/lldp/neighbors`, method: "GET", query, + parseResponse: P.n24, ...params, }); }, @@ -10219,6 +10317,7 @@ export class Api { path: `/v1/system/hardware/racks`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -10232,6 +10331,7 @@ export class Api { return this.request({ path: `/v1/system/hardware/racks/${path.rackId}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -10252,6 +10352,7 @@ export class Api { path: `/v1/system/hardware/racks/${path.rackId}/membership`, method: "GET", query, + parseResponse: P.n31, ...params, }); }, @@ -10265,6 +10366,7 @@ export class Api { return this.request({ path: `/v1/system/hardware/racks/${path.rackId}/membership/abort`, method: "POST", + parseResponse: P.n31, ...params, }); }, @@ -10285,6 +10387,7 @@ export class Api { path: `/v1/system/hardware/racks/${path.rackId}/membership/add`, method: "POST", body, + parseResponse: P.n31, ...params, }); }, @@ -10299,6 +10402,7 @@ export class Api { path: `/v1/system/hardware/sleds`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -10312,6 +10416,7 @@ export class Api { return this.request({ path: `/v1/system/hardware/sleds/${path.sledId}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -10332,6 +10437,7 @@ export class Api { path: `/v1/system/hardware/sleds/${path.sledId}/disks`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -10352,6 +10458,7 @@ export class Api { path: `/v1/system/hardware/sleds/${path.sledId}/instances`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -10520,6 +10627,7 @@ export class Api { path: `/v1/system/hardware/switches`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -10533,6 +10641,7 @@ export class Api { return this.request({ path: `/v1/system/hardware/switches/${path.switchId}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -10547,6 +10656,7 @@ export class Api { path: `/v1/system/identity-providers`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -10565,6 +10675,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -10629,6 +10740,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -10649,6 +10761,7 @@ export class Api { path: `/v1/system/identity-providers/saml/${path.provider}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -10663,6 +10776,7 @@ export class Api { path: `/v1/system/ip-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -10677,6 +10791,7 @@ export class Api { path: `/v1/system/ip-pools`, method: "POST", body, + parseResponse: P.n0, ...params, }); }, @@ -10690,6 +10805,7 @@ export class Api { return this.request({ path: `/v1/system/ip-pools/${path.pool}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -10707,6 +10823,7 @@ export class Api { path: `/v1/system/ip-pools/${path.pool}`, method: "PUT", body, + parseResponse: P.n0, ...params, }); }, @@ -10740,6 +10857,7 @@ export class Api { path: `/v1/system/ip-pools/${path.pool}/ranges`, method: "GET", query, + parseResponse: P.n22, ...params, }); }, @@ -10754,6 +10872,7 @@ export class Api { path: `/v1/system/ip-pools/${path.pool}/ranges/add`, method: "POST", body, + parseResponse: P.n8, ...params, }); }, @@ -10861,6 +10980,7 @@ export class Api { return this.request({ path: `/v1/system/ip-pools-service`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -10875,6 +10995,7 @@ export class Api { path: `/v1/system/ip-pools-service/ranges`, method: "GET", query, + parseResponse: P.n22, ...params, }); }, @@ -10889,6 +11010,7 @@ export class Api { path: `/v1/system/ip-pools-service/ranges/add`, method: "POST", body, + parseResponse: P.n8, ...params, }); }, @@ -10920,6 +11042,7 @@ export class Api { path: `/v1/system/metrics/${path.metricName}`, method: "GET", query, + parseResponse: P.n26, ...params, }); }, @@ -10934,6 +11057,7 @@ export class Api { path: `/v1/system/networking/address-lot`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -10948,6 +11072,7 @@ export class Api { path: `/v1/system/networking/address-lot`, method: "POST", body, + parseResponse: P.n1, ...params, }); }, @@ -10961,6 +11086,7 @@ export class Api { return this.request({ path: `/v1/system/networking/address-lot/${path.addressLot}`, method: "GET", + parseResponse: P.n1, ...params, }); }, @@ -11004,6 +11130,7 @@ export class Api { return this.request({ path: `/v1/system/networking/allow-list`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -11018,6 +11145,7 @@ export class Api { path: `/v1/system/networking/allow-list`, method: "PUT", body, + parseResponse: P.n0, ...params, }); }, @@ -11070,6 +11198,7 @@ export class Api { path: `/v1/system/networking/bgp`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -11084,6 +11213,7 @@ export class Api { path: `/v1/system/networking/bgp`, method: "POST", body, + parseResponse: P.n0, ...params, }); }, @@ -11112,6 +11242,7 @@ export class Api { path: `/v1/system/networking/bgp-announce-set`, method: "GET", query, + parseResponse: P.n39, ...params, }); }, @@ -11126,6 +11257,7 @@ export class Api { path: `/v1/system/networking/bgp-announce-set`, method: "PUT", body, + parseResponse: P.n0, ...params, }); }, @@ -11279,6 +11411,7 @@ export class Api { path: `/v1/system/networking/switch-port-settings`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -11293,6 +11426,7 @@ export class Api { path: `/v1/system/networking/switch-port-settings`, method: "POST", body, + parseResponse: P.n0, ...params, }); }, @@ -11320,6 +11454,7 @@ export class Api { return this.request({ path: `/v1/system/networking/switch-port-settings/${path.port}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -11358,6 +11493,7 @@ export class Api { path: `/v1/system/scim/tokens`, method: "GET", query, + parseResponse: P.n40, ...params, }); }, @@ -11372,6 +11508,7 @@ export class Api { path: `/v1/system/scim/tokens`, method: "POST", query, + parseResponse: P.n18, ...params, }); }, @@ -11389,6 +11526,7 @@ export class Api { path: `/v1/system/scim/tokens/${path.tokenId}`, method: "GET", query, + parseResponse: P.n18, ...params, }); }, @@ -11434,6 +11572,7 @@ export class Api { path: `/v1/system/silos`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -11445,6 +11584,7 @@ export class Api { path: `/v1/system/silos`, method: "POST", body, + parseResponse: P.n0, ...params, }); }, @@ -11458,6 +11598,7 @@ export class Api { return this.request({ path: `/v1/system/silos/${path.silo}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -11488,6 +11629,7 @@ export class Api { path: `/v1/system/silos/${path.silo}/ip-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -11568,6 +11710,7 @@ export class Api { path: `/v1/system/silos/${path.silo}/subnet-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -11582,6 +11725,7 @@ export class Api { path: `/v1/system/subnet-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -11596,6 +11740,7 @@ export class Api { path: `/v1/system/subnet-pools`, method: "POST", body, + parseResponse: P.n0, ...params, }); }, @@ -11609,6 +11754,7 @@ export class Api { return this.request({ path: `/v1/system/subnet-pools/${path.pool}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -11626,6 +11772,7 @@ export class Api { path: `/v1/system/subnet-pools/${path.pool}`, method: "PUT", body, + parseResponse: P.n0, ...params, }); }, @@ -11659,6 +11806,7 @@ export class Api { path: `/v1/system/subnet-pools/${path.pool}/members`, method: "GET", query, + parseResponse: P.n22, ...params, }); }, @@ -11679,6 +11827,7 @@ export class Api { path: `/v1/system/subnet-pools/${path.pool}/members/add`, method: "POST", body, + parseResponse: P.n8, ...params, }); }, @@ -11796,6 +11945,7 @@ export class Api { path: `/v1/system/timeseries/query`, method: "POST", body, + parseResponse: P.n30, ...params, }); }, @@ -11810,6 +11960,7 @@ export class Api { path: `/v1/system/timeseries/schemas`, method: "GET", query, + parseResponse: P.n34, ...params, }); }, @@ -11838,6 +11989,7 @@ export class Api { path: `/v1/system/update/repositories`, method: "GET", query, + parseResponse: P.n22, ...params, }); }, @@ -11852,6 +12004,7 @@ export class Api { path: `/v1/system/update/repositories`, method: "PUT", query, + parseResponse: P.n35, ...params, }); }, @@ -11865,6 +12018,7 @@ export class Api { return this.request({ path: `/v1/system/update/repositories/${path.systemVersion}`, method: "GET", + parseResponse: P.n8, ...params, }); }, @@ -11875,6 +12029,7 @@ export class Api { return this.request({ path: `/v1/system/update/status`, method: "GET", + parseResponse: P.n36, ...params, }); }, @@ -11903,6 +12058,7 @@ export class Api { path: `/v1/system/update/trust-roots`, method: "GET", query, + parseResponse: P.n22, ...params, }); }, @@ -11913,6 +12069,7 @@ export class Api { return this.request({ path: `/v1/system/update/trust-roots`, method: "POST", + parseResponse: P.n8, ...params, }); }, @@ -11926,6 +12083,7 @@ export class Api { return this.request({ path: `/v1/system/update/trust-roots/${path.trustRootId}`, method: "GET", + parseResponse: P.n8, ...params, }); }, @@ -11953,6 +12111,7 @@ export class Api { path: `/v1/system/users`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -11970,6 +12129,7 @@ export class Api { path: `/v1/system/users/${path.userId}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -11984,6 +12144,7 @@ export class Api { path: `/v1/system/users-builtin`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -11997,6 +12158,7 @@ export class Api { return this.request({ path: `/v1/system/users-builtin/${path.user}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -12042,6 +12204,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n30, ...params, }); }, @@ -12056,6 +12219,7 @@ export class Api { path: `/v1/users`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -12069,6 +12233,7 @@ export class Api { return this.request({ path: `/v1/users/${path.userId}`, method: "GET", + parseResponse: P.n0, ...params, }); }, @@ -12086,6 +12251,7 @@ export class Api { path: `/v1/users/${path.userId}/access-tokens`, method: "GET", query, + parseResponse: P.n19, ...params, }); }, @@ -12119,6 +12285,7 @@ export class Api { path: `/v1/users/${path.userId}/sessions`, method: "GET", query, + parseResponse: P.n15, ...params, }); }, @@ -12143,6 +12310,7 @@ export class Api { path: `/v1/vpc-firewall-rules`, method: "GET", query, + parseResponse: P.n37, ...params, }); }, @@ -12164,6 +12332,7 @@ export class Api { method: "PUT", body, query, + parseResponse: P.n37, ...params, }); }, @@ -12178,6 +12347,7 @@ export class Api { path: `/v1/vpc-router-routes`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -12196,6 +12366,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -12216,6 +12387,7 @@ export class Api { path: `/v1/vpc-router-routes/${path.route}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -12239,6 +12411,7 @@ export class Api { method: "PUT", body, query, + parseResponse: P.n0, ...params, }); }, @@ -12273,6 +12446,7 @@ export class Api { path: `/v1/vpc-routers`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -12291,6 +12465,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -12308,6 +12483,7 @@ export class Api { path: `/v1/vpc-routers/${path.router}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -12331,6 +12507,7 @@ export class Api { method: "PUT", body, query, + parseResponse: P.n0, ...params, }); }, @@ -12365,6 +12542,7 @@ export class Api { path: `/v1/vpc-subnets`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -12383,6 +12561,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -12400,6 +12579,7 @@ export class Api { path: `/v1/vpc-subnets/${path.subnet}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -12423,6 +12603,7 @@ export class Api { method: "PUT", body, query, + parseResponse: P.n0, ...params, }); }, @@ -12463,6 +12644,7 @@ export class Api { path: `/v1/vpc-subnets/${path.subnet}/network-interfaces`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -12477,6 +12659,7 @@ export class Api { path: `/v1/vpcs`, method: "GET", query, + parseResponse: P.n2, ...params, }); }, @@ -12492,6 +12675,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n0, ...params, }); }, @@ -12509,6 +12693,7 @@ export class Api { path: `/v1/vpcs/${path.vpc}`, method: "GET", query, + parseResponse: P.n0, ...params, }); }, @@ -12532,6 +12717,7 @@ export class Api { method: "PUT", body, query, + parseResponse: P.n0, ...params, }); }, @@ -12563,6 +12749,7 @@ export class Api { path: `/v1/webhook-receivers`, method: "POST", body, + parseResponse: P.n38, ...params, }); }, @@ -12594,6 +12781,7 @@ export class Api { path: `/v1/webhook-secrets`, method: "GET", query, + parseResponse: P.n9, ...params, }); }, @@ -12612,6 +12800,7 @@ export class Api { method: "POST", body, query, + parseResponse: P.n8, ...params, }); }, diff --git a/oxide-api/src/date-parsers.ts b/oxide-api/src/date-parsers.ts new file mode 100644 index 0000000..b8afe1d --- /dev/null +++ b/oxide-api/src/date-parsers.ts @@ -0,0 +1,319 @@ +/* eslint-disable */ + +/** + * Date parsing for API responses. Each exported function takes a (camelized) + * response value and converts its `date-time` fields to `Date` in place, + * recursing into nested objects and arrays. Generated from `format: date-time` + * in the OpenAPI schema and deduplicated by body, so types with the same date + * shape share one function. `Api.ts` passes the right one as `parseResponse`. + */ + +const D = (v: any) => { + if (typeof v !== "string") return v; + const d = new Date(v); + return isNaN(d.getTime()) ? v : d; +}; + +const A = (v: any, f: (x: any) => any) => (Array.isArray(v) ? v.map(f) : v); + +// 45 types: AddressLot, AffinityGroup, AllowList, AntiAffinityGroup, +41 more +export const n0 = (o: any) => { + if (!o) return o; + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + if (o.timeModified != null) o.timeModified = D(o.timeModified); + return o; +}; + +// 2 types: AddressLotCreateResponse, AddressLotViewResponse +export const n1 = (o: any) => { + if (!o) return o; + if (o.lot != null) o.lot = n0(o.lot); + return o; +}; + +// 38 types: AddressLotResultsPage, AffinityGroupResultsPage, AntiAffinityGroupResultsPage, BgpConfigResultsPage, +34 more +export const n2 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n0); + return o; +}; + +// 1 type: WebhookDeliveryAttempt +export const n3 = (o: any) => { + if (!o) return o; + if (o.timeSent != null) o.timeSent = D(o.timeSent); + return o; +}; + +// 1 type: AlertDeliveryAttempts +export const n4 = (o: any) => { + if (!o) return o; + if (o.webhook != null) o.webhook = A(o.webhook, n3); + return o; +}; + +// 1 type: AlertDelivery +export const n5 = (o: any) => { + if (!o) return o; + if (o.attempts != null) o.attempts = n4(o.attempts); + if (o.timeStarted != null) o.timeStarted = D(o.timeStarted); + return o; +}; + +// 1 type: AlertDeliveryResultsPage +export const n6 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n5); + return o; +}; + +// 1 type: AlertProbeResult +export const n7 = (o: any) => { + if (!o) return o; + if (o.probe != null) o.probe = n5(o.probe); + return o; +}; + +// 6 types: WebhookSecret, IpPoolRange, SubnetPoolMember, SupportBundleInfo, +2 more +export const n8 = (o: any) => { + if (!o) return o; + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + return o; +}; + +// 2 types: AlertReceiverKind, WebhookSecrets +export const n9 = (o: any) => { + if (!o) return o; + if (o.secrets != null) o.secrets = A(o.secrets, n8); + return o; +}; + +// 1 type: AlertReceiver +export const n10 = (o: any) => { + if (!o) return o; + if (o.kind != null) o.kind = n9(o.kind); + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + if (o.timeModified != null) o.timeModified = D(o.timeModified); + return o; +}; + +// 1 type: AlertReceiverResultsPage +export const n11 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n10); + return o; +}; + +// 1 type: AuditLogEntry +export const n12 = (o: any) => { + if (!o) return o; + if (o.timeCompleted != null) o.timeCompleted = D(o.timeCompleted); + if (o.timeStarted != null) o.timeStarted = D(o.timeStarted); + return o; +}; + +// 1 type: AuditLogEntryResultsPage +export const n13 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n12); + return o; +}; + +// 1 type: ConsoleSession +export const n14 = (o: any) => { + if (!o) return o; + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + if (o.timeLastUsed != null) o.timeLastUsed = D(o.timeLastUsed); + return o; +}; + +// 1 type: ConsoleSessionResultsPage +export const n15 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n14); + return o; +}; + +// 15 types: Cumulativedouble, Cumulativefloat, Cumulativeint64, Cumulativeuint64, +11 more +export const n16 = (o: any) => { + if (!o) return o; + if (o.startTime != null) o.startTime = D(o.startTime); + return o; +}; + +// 1 type: Datum +export const n17 = (o: any) => { + if (!o) return o; + if (o.datum != null) o.datum = n16(o.datum); + return o; +}; + +// 3 types: DeviceAccessToken, ScimClientBearerToken, ScimClientBearerTokenValue +export const n18 = (o: any) => { + if (!o) return o; + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + if (o.timeExpires != null) o.timeExpires = D(o.timeExpires); + return o; +}; + +// 1 type: DeviceAccessTokenResultsPage +export const n19 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n18); + return o; +}; + +// 1 type: Instance +export const n20 = (o: any) => { + if (!o) return o; + if (o.autoRestartCooldownExpiration != null) + o.autoRestartCooldownExpiration = D(o.autoRestartCooldownExpiration); + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + if (o.timeLastAutoRestarted != null) + o.timeLastAutoRestarted = D(o.timeLastAutoRestarted); + if (o.timeModified != null) o.timeModified = D(o.timeModified); + if (o.timeRunStateUpdated != null) + o.timeRunStateUpdated = D(o.timeRunStateUpdated); + return o; +}; + +// 1 type: InstanceResultsPage +export const n21 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n20); + return o; +}; + +// 5 types: IpPoolRangeResultsPage, SubnetPoolMemberResultsPage, SupportBundleInfoResultsPage, TufRepoResultsPage, +1 more +export const n22 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n8); + return o; +}; + +// 1 type: LldpNeighbor +export const n23 = (o: any) => { + if (!o) return o; + if (o.firstSeen != null) o.firstSeen = D(o.firstSeen); + if (o.lastSeen != null) o.lastSeen = D(o.lastSeen); + return o; +}; + +// 1 type: LldpNeighborResultsPage +export const n24 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n23); + return o; +}; + +// 1 type: Measurement +export const n25 = (o: any) => { + if (!o) return o; + if (o.datum != null) o.datum = n17(o.datum); + if (o.timestamp != null) o.timestamp = D(o.timestamp); + return o; +}; + +// 1 type: MeasurementResultsPage +export const n26 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n25); + return o; +}; + +// 1 type: Points +export const n27 = (o: any) => { + if (!o) return o; + if (o.startTimes != null) o.startTimes = A(o.startTimes, D); + if (o.timestamps != null) o.timestamps = A(o.timestamps, D); + return o; +}; + +// 1 type: Timeseries +export const n28 = (o: any) => { + if (!o) return o; + if (o.points != null) o.points = n27(o.points); + return o; +}; + +// 1 type: OxqlTable +export const n29 = (o: any) => { + if (!o) return o; + if (o.timeseries != null) o.timeseries = A(o.timeseries, n28); + return o; +}; + +// 1 type: OxqlQueryResult +export const n30 = (o: any) => { + if (!o) return o; + if (o.tables != null) o.tables = A(o.tables, n29); + return o; +}; + +// 1 type: RackMembershipStatus +export const n31 = (o: any) => { + if (!o) return o; + if (o.timeAborted != null) o.timeAborted = D(o.timeAborted); + if (o.timeCommitted != null) o.timeCommitted = D(o.timeCommitted); + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + return o; +}; + +// 1 type: TargetRelease +export const n32 = (o: any) => { + if (!o) return o; + if (o.timeRequested != null) o.timeRequested = D(o.timeRequested); + return o; +}; + +// 1 type: TimeseriesSchema +export const n33 = (o: any) => { + if (!o) return o; + if (o.created != null) o.created = D(o.created); + return o; +}; + +// 1 type: TimeseriesSchemaResultsPage +export const n34 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n33); + return o; +}; + +// 1 type: TufRepoUpload +export const n35 = (o: any) => { + if (!o) return o; + if (o.repo != null) o.repo = n8(o.repo); + return o; +}; + +// 1 type: UpdateStatus +export const n36 = (o: any) => { + if (!o) return o; + if (o.targetRelease != null) o.targetRelease = n32(o.targetRelease); + if (o.timeLastStepPlanned != null) + o.timeLastStepPlanned = D(o.timeLastStepPlanned); + return o; +}; + +// 1 type: VpcFirewallRules +export const n37 = (o: any) => { + if (!o) return o; + if (o.rules != null) o.rules = A(o.rules, n0); + return o; +}; + +// 1 type: WebhookReceiver +export const n38 = (o: any) => { + if (!o) return o; + if (o.secrets != null) o.secrets = A(o.secrets, n8); + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + if (o.timeModified != null) o.timeModified = D(o.timeModified); + return o; +}; + +// 1 type: BgpAnnounceSet[] +export const n39 = (o: any) => A(o, n0); + +// 1 type: ScimClientBearerToken[] +export const n40 = (o: any) => A(o, n18); diff --git a/oxide-api/src/http-client.ts b/oxide-api/src/http-client.ts index 13838da..b2b22fd 100644 --- a/oxide-api/src/http-client.ts +++ b/oxide-api/src/http-client.ts @@ -62,6 +62,10 @@ function encodeQueryParam(key: string, value: unknown) { export async function handleResponse( response: Response, + // Converts `date-time` fields to `Date` in place. The generated client passes + // the parser for the endpoint's success type; defaults to a no-op so this + // file stays testable without generating anything. + parseResponse: (data: unknown) => unknown = (data) => data, ): Promise> { const respText = await response.text(); @@ -93,7 +97,7 @@ export async function handleResponse( return { type: "success", response, - data: respJson as Data, + data: parseResponse(respJson) as Data, }; } @@ -115,6 +119,8 @@ export interface FullParams extends FetchParams { body?: unknown; host?: string; method?: string; + /** Parses `date-time` fields in the response body to `Date`. */ + parseResponse?: (data: unknown) => unknown; } export function mergeParams(a: FetchParams, b: FetchParams): FetchParams { diff --git a/oxide-api/src/util.ts b/oxide-api/src/util.ts index 4a41a6b..ec33800 100644 --- a/oxide-api/src/util.ts +++ b/oxide-api/src/util.ts @@ -45,27 +45,13 @@ export const mapObj = return newObj; }; -const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/; - -export const parseIfDate = (k: string | undefined, v: unknown) => { - if ( - typeof v === "string" && - isoDateRegex.test(v) && - (k?.startsWith("time_") || - k?.endsWith("_time") || - k?.endsWith("_expiration") || - k === "timestamp") - ) { - const d = new Date(v); - if (isNaN(d.getTime())) return v; - return d; - } - return v; -}; - export const snakeify = mapObj(camelToSnake); -export const processResponseBody = mapObj(snakeToCamel, parseIfDate); +// Snake-case to camel-case key conversion only. Date parsing used to happen +// here via a field-name heuristic; it now lives in the generated +// date-parsers.ts, which is driven by `format: date-time` in the schema and +// applied per-endpoint via the `parseResponse` callback in handleResponse. +export const processResponseBody = mapObj(snakeToCamel); export function isNotNull(value: T): value is NonNullable { return value != null; diff --git a/oxide-openapi-gen-ts/src/__snapshots__/Api.ts b/oxide-openapi-gen-ts/src/__snapshots__/Api.ts index d8ee3be..34c85a6 100644 --- a/oxide-openapi-gen-ts/src/__snapshots__/Api.ts +++ b/oxide-openapi-gen-ts/src/__snapshots__/Api.ts @@ -3,6 +3,7 @@ import type { FetchParams, FullParams, ApiResult } from "./http-client"; import { dateReplacer, handleResponse, mergeParams, toQueryString } from './http-client' import { snakeify } from './util' + import * as P from './date-parsers' export type { ApiResult, ErrorBody, ErrorResult } from './http-client' @@ -7699,6 +7700,7 @@ export interface ApiConfig { path, query, host, + parseResponse, ...fetchParams }: FullParams): Promise> { const url = (host || this.host) + path + toQueryString(query); @@ -7706,7 +7708,7 @@ export interface ApiConfig { ...mergeParams(this.baseParams, fetchParams), body: JSON.stringify(snakeify(body), dateReplacer), }; - return handleResponse(await fetch(url, init)); + return handleResponse(await fetch(url, init), parseResponse); } methods = { @@ -7773,6 +7775,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -7817,6 +7820,7 @@ params: FetchParams = {}) => { path: `/experimental/v1/system/support-bundles`, method: "GET", query, + parseResponse: P.n22, ...params, }) }, @@ -7831,6 +7835,7 @@ params: FetchParams = {}) => { path: `/experimental/v1/system/support-bundles`, method: "POST", body, + parseResponse: P.n8, ...params, }) }, @@ -7844,6 +7849,7 @@ params: FetchParams = {}) => { return this.request({ path: `/experimental/v1/system/support-bundles/${path.bundleId}`, method: "GET", + parseResponse: P.n8, ...params, }) }, @@ -7859,6 +7865,7 @@ params: FetchParams = {}) => { path: `/experimental/v1/system/support-bundles/${path.bundleId}`, method: "PUT", body, + parseResponse: P.n8, ...params, }) }, @@ -7964,6 +7971,7 @@ params: FetchParams = {}) => { path: `/v1/affinity-groups`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -7980,6 +7988,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -7995,6 +8004,7 @@ params: FetchParams = {}) => { path: `/v1/affinity-groups/${path.affinityGroup}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -8012,6 +8022,7 @@ params: FetchParams = {}) => { method: "PUT", body, query, + parseResponse: P.n0, ...params, }) }, @@ -8115,6 +8126,7 @@ params: FetchParams = {}) => { path: `/v1/alert-receivers`, method: "GET", query, + parseResponse: P.n11, ...params, }) }, @@ -8128,6 +8140,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/alert-receivers/${path.receiver}`, method: "GET", + parseResponse: P.n10, ...params, }) }, @@ -8156,6 +8169,7 @@ params: FetchParams = {}) => { path: `/v1/alert-receivers/${path.receiver}/deliveries`, method: "GET", query, + parseResponse: P.n6, ...params, }) }, @@ -8171,6 +8185,7 @@ params: FetchParams = {}) => { path: `/v1/alert-receivers/${path.receiver}/probe`, method: "POST", query, + parseResponse: P.n7, ...params, }) }, @@ -8228,6 +8243,7 @@ params: FetchParams = {}) => { path: `/v1/anti-affinity-groups`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -8244,6 +8260,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -8259,6 +8276,7 @@ params: FetchParams = {}) => { path: `/v1/anti-affinity-groups/${path.antiAffinityGroup}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -8276,6 +8294,7 @@ params: FetchParams = {}) => { method: "PUT", body, query, + parseResponse: P.n0, ...params, }) }, @@ -8390,6 +8409,7 @@ params: FetchParams = {}) => { path: `/v1/certificates`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -8404,6 +8424,7 @@ params: FetchParams = {}) => { path: `/v1/certificates`, method: "POST", body, + parseResponse: P.n0, ...params, }) }, @@ -8417,6 +8438,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/certificates/${path.certificate}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -8444,6 +8466,7 @@ params: FetchParams = {}) => { path: `/v1/disks`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -8460,6 +8483,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -8475,6 +8499,7 @@ params: FetchParams = {}) => { path: `/v1/disks/${path.disk}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -8568,6 +8593,7 @@ params: FetchParams = {}) => { path: `/v1/external-subnets`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -8584,6 +8610,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -8599,6 +8626,7 @@ params: FetchParams = {}) => { path: `/v1/external-subnets/${path.externalSubnet}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -8616,6 +8644,7 @@ params: FetchParams = {}) => { method: "PUT", body, query, + parseResponse: P.n0, ...params, }) }, @@ -8648,6 +8677,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -8663,6 +8693,7 @@ params: FetchParams = {}) => { path: `/v1/external-subnets/${path.externalSubnet}/detach`, method: "POST", query, + parseResponse: P.n0, ...params, }) }, @@ -8677,6 +8708,7 @@ params: FetchParams = {}) => { path: `/v1/floating-ips`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -8693,6 +8725,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -8708,6 +8741,7 @@ params: FetchParams = {}) => { path: `/v1/floating-ips/${path.floatingIp}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -8725,6 +8759,7 @@ params: FetchParams = {}) => { method: "PUT", body, query, + parseResponse: P.n0, ...params, }) }, @@ -8757,6 +8792,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -8772,6 +8808,7 @@ params: FetchParams = {}) => { path: `/v1/floating-ips/${path.floatingIp}/detach`, method: "POST", query, + parseResponse: P.n0, ...params, }) }, @@ -8786,6 +8823,7 @@ params: FetchParams = {}) => { path: `/v1/groups`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -8799,6 +8837,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/groups/${path.groupId}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -8813,6 +8852,7 @@ params: FetchParams = {}) => { path: `/v1/images`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -8829,6 +8869,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -8844,6 +8885,7 @@ params: FetchParams = {}) => { path: `/v1/images/${path.image}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -8874,6 +8916,7 @@ params: FetchParams = {}) => { path: `/v1/images/${path.image}/demote`, method: "POST", query, + parseResponse: P.n0, ...params, }) }, @@ -8889,6 +8932,7 @@ params: FetchParams = {}) => { path: `/v1/images/${path.image}/promote`, method: "POST", query, + parseResponse: P.n0, ...params, }) }, @@ -8903,6 +8947,7 @@ params: FetchParams = {}) => { path: `/v1/instances`, method: "GET", query, + parseResponse: P.n21, ...params, }) }, @@ -8919,6 +8964,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n20, ...params, }) }, @@ -8934,6 +8980,7 @@ params: FetchParams = {}) => { path: `/v1/instances/${path.instance}`, method: "GET", query, + parseResponse: P.n20, ...params, }) }, @@ -8951,6 +8998,7 @@ params: FetchParams = {}) => { method: "PUT", body, query, + parseResponse: P.n20, ...params, }) }, @@ -8981,6 +9029,7 @@ params: FetchParams = {}) => { path: `/v1/instances/${path.instance}/affinity-groups`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -8996,6 +9045,7 @@ params: FetchParams = {}) => { path: `/v1/instances/${path.instance}/anti-affinity-groups`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9011,6 +9061,7 @@ params: FetchParams = {}) => { path: `/v1/instances/${path.instance}/disks`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9028,6 +9079,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -9045,6 +9097,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -9060,6 +9113,7 @@ params: FetchParams = {}) => { path: `/v1/instances/${path.instance}/external-ips`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9077,6 +9131,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -9107,6 +9162,7 @@ params: FetchParams = {}) => { path: `/v1/instances/${path.instance}/external-subnets`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9122,6 +9178,7 @@ params: FetchParams = {}) => { path: `/v1/instances/${path.instance}/multicast-groups`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9139,6 +9196,7 @@ params: FetchParams = {}) => { method: "PUT", body, query, + parseResponse: P.n0, ...params, }) }, @@ -9169,6 +9227,7 @@ params: FetchParams = {}) => { path: `/v1/instances/${path.instance}/reboot`, method: "POST", query, + parseResponse: P.n20, ...params, }) }, @@ -9199,6 +9258,7 @@ params: FetchParams = {}) => { path: `/v1/instances/${path.instance}/ssh-public-keys`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9214,6 +9274,7 @@ params: FetchParams = {}) => { path: `/v1/instances/${path.instance}/start`, method: "POST", query, + parseResponse: P.n20, ...params, }) }, @@ -9229,6 +9290,7 @@ params: FetchParams = {}) => { path: `/v1/instances/${path.instance}/stop`, method: "POST", query, + parseResponse: P.n20, ...params, }) }, @@ -9243,6 +9305,7 @@ params: FetchParams = {}) => { path: `/v1/internet-gateway-ip-addresses`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9259,6 +9322,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -9288,6 +9352,7 @@ params: FetchParams = {}) => { path: `/v1/internet-gateway-ip-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9304,6 +9369,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -9333,6 +9399,7 @@ params: FetchParams = {}) => { path: `/v1/internet-gateways`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9349,6 +9416,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -9364,6 +9432,7 @@ params: FetchParams = {}) => { path: `/v1/internet-gateways/${path.gateway}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -9393,6 +9462,7 @@ params: FetchParams = {}) => { path: `/v1/ip-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9406,6 +9476,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/ip-pools/${path.pool}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -9443,6 +9514,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/me`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -9457,6 +9529,7 @@ params: FetchParams = {}) => { path: `/v1/me/access-tokens`, method: "GET", query, + parseResponse: P.n19, ...params, }) }, @@ -9484,6 +9557,7 @@ params: FetchParams = {}) => { path: `/v1/me/groups`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9498,6 +9572,7 @@ params: FetchParams = {}) => { path: `/v1/me/ssh-keys`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9512,6 +9587,7 @@ params: FetchParams = {}) => { path: `/v1/me/ssh-keys`, method: "POST", body, + parseResponse: P.n0, ...params, }) }, @@ -9525,6 +9601,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/me/ssh-keys/${path.sshKey}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -9553,6 +9630,7 @@ params: FetchParams = {}) => { path: `/v1/metrics/${path.metricName}`, method: "GET", query, + parseResponse: P.n26, ...params, }) }, @@ -9567,6 +9645,7 @@ params: FetchParams = {}) => { path: `/v1/multicast-groups`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9580,6 +9659,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/multicast-groups/${path.multicastGroup}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -9595,6 +9675,7 @@ params: FetchParams = {}) => { path: `/v1/multicast-groups/${path.multicastGroup}/members`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9609,6 +9690,7 @@ params: FetchParams = {}) => { path: `/v1/network-interfaces`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9625,6 +9707,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -9640,6 +9723,7 @@ params: FetchParams = {}) => { path: `/v1/network-interfaces/${path.interface}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -9657,6 +9741,7 @@ params: FetchParams = {}) => { method: "PUT", body, query, + parseResponse: P.n0, ...params, }) }, @@ -9722,6 +9807,7 @@ params: FetchParams = {}) => { path: `/v1/projects`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9736,6 +9822,7 @@ params: FetchParams = {}) => { path: `/v1/projects`, method: "POST", body, + parseResponse: P.n0, ...params, }) }, @@ -9749,6 +9836,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/projects/${path.project}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -9764,6 +9852,7 @@ params: FetchParams = {}) => { path: `/v1/projects/${path.project}`, method: "PUT", body, + parseResponse: P.n0, ...params, }) }, @@ -9819,6 +9908,7 @@ params: FetchParams = {}) => { path: `/v1/snapshots`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9835,6 +9925,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -9850,6 +9941,7 @@ params: FetchParams = {}) => { path: `/v1/snapshots/${path.snapshot}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -9879,6 +9971,7 @@ params: FetchParams = {}) => { path: `/v1/subnet-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9892,6 +9985,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/subnet-pools/${path.pool}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -9906,6 +10000,7 @@ params: FetchParams = {}) => { path: `/v1/system/audit-log`, method: "GET", query, + parseResponse: P.n13, ...params, }) }, @@ -9920,6 +10015,7 @@ params: FetchParams = {}) => { path: `/v1/system/hardware/disks`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9933,6 +10029,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/hardware/disks/${path.diskId}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -9948,6 +10045,7 @@ params: FetchParams = {}) => { path: `/v1/system/hardware/rack-switch-port/${path.rackId}/${path.switchSlot}/${path.port}/lldp/neighbors`, method: "GET", query, + parseResponse: P.n24, ...params, }) }, @@ -9962,6 +10060,7 @@ params: FetchParams = {}) => { path: `/v1/system/hardware/racks`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -9975,6 +10074,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/hardware/racks/${path.rackId}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -9990,6 +10090,7 @@ params: FetchParams = {}) => { path: `/v1/system/hardware/racks/${path.rackId}/membership`, method: "GET", query, + parseResponse: P.n31, ...params, }) }, @@ -10003,6 +10104,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/hardware/racks/${path.rackId}/membership/abort`, method: "POST", + parseResponse: P.n31, ...params, }) }, @@ -10018,6 +10120,7 @@ params: FetchParams = {}) => { path: `/v1/system/hardware/racks/${path.rackId}/membership/add`, method: "POST", body, + parseResponse: P.n31, ...params, }) }, @@ -10032,6 +10135,7 @@ params: FetchParams = {}) => { path: `/v1/system/hardware/sleds`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -10045,6 +10149,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/hardware/sleds/${path.sledId}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -10060,6 +10165,7 @@ params: FetchParams = {}) => { path: `/v1/system/hardware/sleds/${path.sledId}/disks`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -10075,6 +10181,7 @@ params: FetchParams = {}) => { path: `/v1/system/hardware/sleds/${path.sledId}/instances`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -10211,6 +10318,7 @@ params: FetchParams = {}) => { path: `/v1/system/hardware/switches`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -10224,6 +10332,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/hardware/switches/${path.switchId}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -10238,6 +10347,7 @@ params: FetchParams = {}) => { path: `/v1/system/identity-providers`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -10254,6 +10364,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -10302,6 +10413,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -10317,6 +10429,7 @@ params: FetchParams = {}) => { path: `/v1/system/identity-providers/saml/${path.provider}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -10331,6 +10444,7 @@ params: FetchParams = {}) => { path: `/v1/system/ip-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -10345,6 +10459,7 @@ params: FetchParams = {}) => { path: `/v1/system/ip-pools`, method: "POST", body, + parseResponse: P.n0, ...params, }) }, @@ -10358,6 +10473,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/ip-pools/${path.pool}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -10373,6 +10489,7 @@ params: FetchParams = {}) => { path: `/v1/system/ip-pools/${path.pool}`, method: "PUT", body, + parseResponse: P.n0, ...params, }) }, @@ -10401,6 +10518,7 @@ params: FetchParams = {}) => { path: `/v1/system/ip-pools/${path.pool}/ranges`, method: "GET", query, + parseResponse: P.n22, ...params, }) }, @@ -10416,6 +10534,7 @@ params: FetchParams = {}) => { path: `/v1/system/ip-pools/${path.pool}/ranges/add`, method: "POST", body, + parseResponse: P.n8, ...params, }) }, @@ -10513,6 +10632,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/ip-pools-service`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -10527,6 +10647,7 @@ params: FetchParams = {}) => { path: `/v1/system/ip-pools-service/ranges`, method: "GET", query, + parseResponse: P.n22, ...params, }) }, @@ -10541,6 +10662,7 @@ params: FetchParams = {}) => { path: `/v1/system/ip-pools-service/ranges/add`, method: "POST", body, + parseResponse: P.n8, ...params, }) }, @@ -10570,6 +10692,7 @@ params: FetchParams = {}) => { path: `/v1/system/metrics/${path.metricName}`, method: "GET", query, + parseResponse: P.n26, ...params, }) }, @@ -10584,6 +10707,7 @@ params: FetchParams = {}) => { path: `/v1/system/networking/address-lot`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -10598,6 +10722,7 @@ params: FetchParams = {}) => { path: `/v1/system/networking/address-lot`, method: "POST", body, + parseResponse: P.n1, ...params, }) }, @@ -10611,6 +10736,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/networking/address-lot/${path.addressLot}`, method: "GET", + parseResponse: P.n1, ...params, }) }, @@ -10650,6 +10776,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/networking/allow-list`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -10664,6 +10791,7 @@ params: FetchParams = {}) => { path: `/v1/system/networking/allow-list`, method: "PUT", body, + parseResponse: P.n0, ...params, }) }, @@ -10717,6 +10845,7 @@ params: FetchParams = {}) => { path: `/v1/system/networking/bgp`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -10731,6 +10860,7 @@ params: FetchParams = {}) => { path: `/v1/system/networking/bgp`, method: "POST", body, + parseResponse: P.n0, ...params, }) }, @@ -10759,6 +10889,7 @@ params: FetchParams = {}) => { path: `/v1/system/networking/bgp-announce-set`, method: "GET", query, + parseResponse: P.n39, ...params, }) }, @@ -10773,6 +10904,7 @@ params: FetchParams = {}) => { path: `/v1/system/networking/bgp-announce-set`, method: "PUT", body, + parseResponse: P.n0, ...params, }) }, @@ -10929,6 +11061,7 @@ params: FetchParams = {}) => { path: `/v1/system/networking/switch-port-settings`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -10943,6 +11076,7 @@ params: FetchParams = {}) => { path: `/v1/system/networking/switch-port-settings`, method: "POST", body, + parseResponse: P.n0, ...params, }) }, @@ -10970,6 +11104,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/networking/switch-port-settings/${path.port}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -11009,6 +11144,7 @@ params: FetchParams = {}) => { path: `/v1/system/scim/tokens`, method: "GET", query, + parseResponse: P.n40, ...params, }) }, @@ -11023,6 +11159,7 @@ params: FetchParams = {}) => { path: `/v1/system/scim/tokens`, method: "POST", query, + parseResponse: P.n18, ...params, }) }, @@ -11038,6 +11175,7 @@ params: FetchParams = {}) => { path: `/v1/system/scim/tokens/${path.tokenId}`, method: "GET", query, + parseResponse: P.n18, ...params, }) }, @@ -11081,6 +11219,7 @@ params: FetchParams = {}) => { path: `/v1/system/silos`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -11095,6 +11234,7 @@ params: FetchParams = {}) => { path: `/v1/system/silos`, method: "POST", body, + parseResponse: P.n0, ...params, }) }, @@ -11108,6 +11248,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/silos/${path.silo}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -11136,6 +11277,7 @@ params: FetchParams = {}) => { path: `/v1/system/silos/${path.silo}/ip-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -11207,6 +11349,7 @@ params: FetchParams = {}) => { path: `/v1/system/silos/${path.silo}/subnet-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -11221,6 +11364,7 @@ params: FetchParams = {}) => { path: `/v1/system/subnet-pools`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -11235,6 +11379,7 @@ params: FetchParams = {}) => { path: `/v1/system/subnet-pools`, method: "POST", body, + parseResponse: P.n0, ...params, }) }, @@ -11248,6 +11393,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/subnet-pools/${path.pool}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -11263,6 +11409,7 @@ params: FetchParams = {}) => { path: `/v1/system/subnet-pools/${path.pool}`, method: "PUT", body, + parseResponse: P.n0, ...params, }) }, @@ -11291,6 +11438,7 @@ params: FetchParams = {}) => { path: `/v1/system/subnet-pools/${path.pool}/members`, method: "GET", query, + parseResponse: P.n22, ...params, }) }, @@ -11306,6 +11454,7 @@ params: FetchParams = {}) => { path: `/v1/system/subnet-pools/${path.pool}/members/add`, method: "POST", body, + parseResponse: P.n8, ...params, }) }, @@ -11406,6 +11555,7 @@ params: FetchParams = {}) => { path: `/v1/system/timeseries/query`, method: "POST", body, + parseResponse: P.n30, ...params, }) }, @@ -11420,6 +11570,7 @@ params: FetchParams = {}) => { path: `/v1/system/timeseries/schemas`, method: "GET", query, + parseResponse: P.n34, ...params, }) }, @@ -11448,6 +11599,7 @@ params: FetchParams = {}) => { path: `/v1/system/update/repositories`, method: "GET", query, + parseResponse: P.n22, ...params, }) }, @@ -11462,6 +11614,7 @@ params: FetchParams = {}) => { path: `/v1/system/update/repositories`, method: "PUT", query, + parseResponse: P.n35, ...params, }) }, @@ -11475,6 +11628,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/update/repositories/${path.systemVersion}`, method: "GET", + parseResponse: P.n8, ...params, }) }, @@ -11486,6 +11640,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/update/status`, method: "GET", + parseResponse: P.n36, ...params, }) }, @@ -11514,6 +11669,7 @@ params: FetchParams = {}) => { path: `/v1/system/update/trust-roots`, method: "GET", query, + parseResponse: P.n22, ...params, }) }, @@ -11525,6 +11681,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/update/trust-roots`, method: "POST", + parseResponse: P.n8, ...params, }) }, @@ -11538,6 +11695,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/update/trust-roots/${path.trustRootId}`, method: "GET", + parseResponse: P.n8, ...params, }) }, @@ -11565,6 +11723,7 @@ params: FetchParams = {}) => { path: `/v1/system/users`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -11580,6 +11739,7 @@ params: FetchParams = {}) => { path: `/v1/system/users/${path.userId}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -11594,6 +11754,7 @@ params: FetchParams = {}) => { path: `/v1/system/users-builtin`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -11607,6 +11768,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/system/users-builtin/${path.user}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -11650,6 +11812,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n30, ...params, }) }, @@ -11664,6 +11827,7 @@ params: FetchParams = {}) => { path: `/v1/users`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -11677,6 +11841,7 @@ params: FetchParams = {}) => { return this.request({ path: `/v1/users/${path.userId}`, method: "GET", + parseResponse: P.n0, ...params, }) }, @@ -11692,6 +11857,7 @@ params: FetchParams = {}) => { path: `/v1/users/${path.userId}/access-tokens`, method: "GET", query, + parseResponse: P.n19, ...params, }) }, @@ -11720,6 +11886,7 @@ params: FetchParams = {}) => { path: `/v1/users/${path.userId}/sessions`, method: "GET", query, + parseResponse: P.n15, ...params, }) }, @@ -11745,6 +11912,7 @@ params: FetchParams = {}) => { path: `/v1/vpc-firewall-rules`, method: "GET", query, + parseResponse: P.n37, ...params, }) }, @@ -11761,6 +11929,7 @@ params: FetchParams = {}) => { method: "PUT", body, query, + parseResponse: P.n37, ...params, }) }, @@ -11775,6 +11944,7 @@ params: FetchParams = {}) => { path: `/v1/vpc-router-routes`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -11791,6 +11961,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -11806,6 +11977,7 @@ params: FetchParams = {}) => { path: `/v1/vpc-router-routes/${path.route}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -11823,6 +11995,7 @@ params: FetchParams = {}) => { method: "PUT", body, query, + parseResponse: P.n0, ...params, }) }, @@ -11852,6 +12025,7 @@ params: FetchParams = {}) => { path: `/v1/vpc-routers`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -11868,6 +12042,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -11883,6 +12058,7 @@ params: FetchParams = {}) => { path: `/v1/vpc-routers/${path.router}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -11900,6 +12076,7 @@ params: FetchParams = {}) => { method: "PUT", body, query, + parseResponse: P.n0, ...params, }) }, @@ -11929,6 +12106,7 @@ params: FetchParams = {}) => { path: `/v1/vpc-subnets`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -11945,6 +12123,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -11960,6 +12139,7 @@ params: FetchParams = {}) => { path: `/v1/vpc-subnets/${path.subnet}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -11977,6 +12157,7 @@ params: FetchParams = {}) => { method: "PUT", body, query, + parseResponse: P.n0, ...params, }) }, @@ -12007,6 +12188,7 @@ params: FetchParams = {}) => { path: `/v1/vpc-subnets/${path.subnet}/network-interfaces`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -12021,6 +12203,7 @@ params: FetchParams = {}) => { path: `/v1/vpcs`, method: "GET", query, + parseResponse: P.n2, ...params, }) }, @@ -12037,6 +12220,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n0, ...params, }) }, @@ -12052,6 +12236,7 @@ params: FetchParams = {}) => { path: `/v1/vpcs/${path.vpc}`, method: "GET", query, + parseResponse: P.n0, ...params, }) }, @@ -12069,6 +12254,7 @@ params: FetchParams = {}) => { method: "PUT", body, query, + parseResponse: P.n0, ...params, }) }, @@ -12098,6 +12284,7 @@ params: FetchParams = {}) => { path: `/v1/webhook-receivers`, method: "POST", body, + parseResponse: P.n38, ...params, }) }, @@ -12127,6 +12314,7 @@ params: FetchParams = {}) => { path: `/v1/webhook-secrets`, method: "GET", query, + parseResponse: P.n9, ...params, }) }, @@ -12143,6 +12331,7 @@ params: FetchParams = {}) => { method: "POST", body, query, + parseResponse: P.n8, ...params, }) }, diff --git a/oxide-openapi-gen-ts/src/__snapshots__/date-parsers.ts b/oxide-openapi-gen-ts/src/__snapshots__/date-parsers.ts new file mode 100644 index 0000000..1c504e6 --- /dev/null +++ b/oxide-openapi-gen-ts/src/__snapshots__/date-parsers.ts @@ -0,0 +1,315 @@ +/* eslint-disable */ + +/** + * Date parsing for API responses. Each exported function takes a (camelized) + * response value and converts its `date-time` fields to `Date` in place, + * recursing into nested objects and arrays. Generated from `format: date-time` + * in the OpenAPI schema and deduplicated by body, so types with the same date + * shape share one function. `Api.ts` passes the right one as `parseResponse`. + */ + +const D = (v: any) => { + if (typeof v !== "string") return v; + const d = new Date(v); + return isNaN(d.getTime()) ? v : d; +}; + +const A = (v: any, f: (x: any) => any) => (Array.isArray(v) ? v.map(f) : v); + +// 45 types: AddressLot, AffinityGroup, AllowList, AntiAffinityGroup, +41 more +export const n0 = (o: any) => { + if (!o) return o; + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + if (o.timeModified != null) o.timeModified = D(o.timeModified); + return o; +}; + +// 2 types: AddressLotCreateResponse, AddressLotViewResponse +export const n1 = (o: any) => { + if (!o) return o; + if (o.lot != null) o.lot = n0(o.lot); + return o; +}; + +// 38 types: AddressLotResultsPage, AffinityGroupResultsPage, AntiAffinityGroupResultsPage, BgpConfigResultsPage, +34 more +export const n2 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n0); + return o; +}; + +// 1 type: WebhookDeliveryAttempt +export const n3 = (o: any) => { + if (!o) return o; + if (o.timeSent != null) o.timeSent = D(o.timeSent); + return o; +}; + +// 1 type: AlertDeliveryAttempts +export const n4 = (o: any) => { + if (!o) return o; + if (o.webhook != null) o.webhook = A(o.webhook, n3); + return o; +}; + +// 1 type: AlertDelivery +export const n5 = (o: any) => { + if (!o) return o; + if (o.attempts != null) o.attempts = n4(o.attempts); + if (o.timeStarted != null) o.timeStarted = D(o.timeStarted); + return o; +}; + +// 1 type: AlertDeliveryResultsPage +export const n6 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n5); + return o; +}; + +// 1 type: AlertProbeResult +export const n7 = (o: any) => { + if (!o) return o; + if (o.probe != null) o.probe = n5(o.probe); + return o; +}; + +// 6 types: WebhookSecret, IpPoolRange, SubnetPoolMember, SupportBundleInfo, +2 more +export const n8 = (o: any) => { + if (!o) return o; + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + return o; +}; + +// 2 types: AlertReceiverKind, WebhookSecrets +export const n9 = (o: any) => { + if (!o) return o; + if (o.secrets != null) o.secrets = A(o.secrets, n8); + return o; +}; + +// 1 type: AlertReceiver +export const n10 = (o: any) => { + if (!o) return o; + if (o.kind != null) o.kind = n9(o.kind); + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + if (o.timeModified != null) o.timeModified = D(o.timeModified); + return o; +}; + +// 1 type: AlertReceiverResultsPage +export const n11 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n10); + return o; +}; + +// 1 type: AuditLogEntry +export const n12 = (o: any) => { + if (!o) return o; + if (o.timeCompleted != null) o.timeCompleted = D(o.timeCompleted); + if (o.timeStarted != null) o.timeStarted = D(o.timeStarted); + return o; +}; + +// 1 type: AuditLogEntryResultsPage +export const n13 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n12); + return o; +}; + +// 1 type: ConsoleSession +export const n14 = (o: any) => { + if (!o) return o; + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + if (o.timeLastUsed != null) o.timeLastUsed = D(o.timeLastUsed); + return o; +}; + +// 1 type: ConsoleSessionResultsPage +export const n15 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n14); + return o; +}; + +// 15 types: Cumulativedouble, Cumulativefloat, Cumulativeint64, Cumulativeuint64, +11 more +export const n16 = (o: any) => { + if (!o) return o; + if (o.startTime != null) o.startTime = D(o.startTime); + return o; +}; + +// 1 type: Datum +export const n17 = (o: any) => { + if (!o) return o; + if (o.datum != null) o.datum = n16(o.datum); + return o; +}; + +// 3 types: DeviceAccessToken, ScimClientBearerToken, ScimClientBearerTokenValue +export const n18 = (o: any) => { + if (!o) return o; + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + if (o.timeExpires != null) o.timeExpires = D(o.timeExpires); + return o; +}; + +// 1 type: DeviceAccessTokenResultsPage +export const n19 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n18); + return o; +}; + +// 1 type: Instance +export const n20 = (o: any) => { + if (!o) return o; + if (o.autoRestartCooldownExpiration != null) o.autoRestartCooldownExpiration = D(o.autoRestartCooldownExpiration); + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + if (o.timeLastAutoRestarted != null) o.timeLastAutoRestarted = D(o.timeLastAutoRestarted); + if (o.timeModified != null) o.timeModified = D(o.timeModified); + if (o.timeRunStateUpdated != null) o.timeRunStateUpdated = D(o.timeRunStateUpdated); + return o; +}; + +// 1 type: InstanceResultsPage +export const n21 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n20); + return o; +}; + +// 5 types: IpPoolRangeResultsPage, SubnetPoolMemberResultsPage, SupportBundleInfoResultsPage, TufRepoResultsPage, +1 more +export const n22 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n8); + return o; +}; + +// 1 type: LldpNeighbor +export const n23 = (o: any) => { + if (!o) return o; + if (o.firstSeen != null) o.firstSeen = D(o.firstSeen); + if (o.lastSeen != null) o.lastSeen = D(o.lastSeen); + return o; +}; + +// 1 type: LldpNeighborResultsPage +export const n24 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n23); + return o; +}; + +// 1 type: Measurement +export const n25 = (o: any) => { + if (!o) return o; + if (o.datum != null) o.datum = n17(o.datum); + if (o.timestamp != null) o.timestamp = D(o.timestamp); + return o; +}; + +// 1 type: MeasurementResultsPage +export const n26 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n25); + return o; +}; + +// 1 type: Points +export const n27 = (o: any) => { + if (!o) return o; + if (o.startTimes != null) o.startTimes = A(o.startTimes, D); + if (o.timestamps != null) o.timestamps = A(o.timestamps, D); + return o; +}; + +// 1 type: Timeseries +export const n28 = (o: any) => { + if (!o) return o; + if (o.points != null) o.points = n27(o.points); + return o; +}; + +// 1 type: OxqlTable +export const n29 = (o: any) => { + if (!o) return o; + if (o.timeseries != null) o.timeseries = A(o.timeseries, n28); + return o; +}; + +// 1 type: OxqlQueryResult +export const n30 = (o: any) => { + if (!o) return o; + if (o.tables != null) o.tables = A(o.tables, n29); + return o; +}; + +// 1 type: RackMembershipStatus +export const n31 = (o: any) => { + if (!o) return o; + if (o.timeAborted != null) o.timeAborted = D(o.timeAborted); + if (o.timeCommitted != null) o.timeCommitted = D(o.timeCommitted); + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + return o; +}; + +// 1 type: TargetRelease +export const n32 = (o: any) => { + if (!o) return o; + if (o.timeRequested != null) o.timeRequested = D(o.timeRequested); + return o; +}; + +// 1 type: TimeseriesSchema +export const n33 = (o: any) => { + if (!o) return o; + if (o.created != null) o.created = D(o.created); + return o; +}; + +// 1 type: TimeseriesSchemaResultsPage +export const n34 = (o: any) => { + if (!o) return o; + if (o.items != null) o.items = A(o.items, n33); + return o; +}; + +// 1 type: TufRepoUpload +export const n35 = (o: any) => { + if (!o) return o; + if (o.repo != null) o.repo = n8(o.repo); + return o; +}; + +// 1 type: UpdateStatus +export const n36 = (o: any) => { + if (!o) return o; + if (o.targetRelease != null) o.targetRelease = n32(o.targetRelease); + if (o.timeLastStepPlanned != null) o.timeLastStepPlanned = D(o.timeLastStepPlanned); + return o; +}; + +// 1 type: VpcFirewallRules +export const n37 = (o: any) => { + if (!o) return o; + if (o.rules != null) o.rules = A(o.rules, n0); + return o; +}; + +// 1 type: WebhookReceiver +export const n38 = (o: any) => { + if (!o) return o; + if (o.secrets != null) o.secrets = A(o.secrets, n8); + if (o.timeCreated != null) o.timeCreated = D(o.timeCreated); + if (o.timeModified != null) o.timeModified = D(o.timeModified); + return o; +}; + +// 1 type: BgpAnnounceSet[] +export const n39 = (o: any) => A(o, n0); + +// 1 type: ScimClientBearerToken[] +export const n40 = (o: any) => A(o, n18); diff --git a/oxide-openapi-gen-ts/src/__snapshots__/http-client.ts b/oxide-openapi-gen-ts/src/__snapshots__/http-client.ts index 121ec35..f964ec6 100644 --- a/oxide-openapi-gen-ts/src/__snapshots__/http-client.ts +++ b/oxide-openapi-gen-ts/src/__snapshots__/http-client.ts @@ -61,7 +61,11 @@ function encodeQueryParam(key: string, value: unknown) { } export async function handleResponse( - response: Response + response: Response, + // Converts `date-time` fields to `Date` in place. The generated client passes + // the parser for the endpoint's success type; defaults to a no-op so this + // file stays testable without generating anything. + parseResponse: (data: unknown) => unknown = (data) => data ): Promise> { const respText = await response.text(); @@ -93,7 +97,7 @@ export async function handleResponse( return { type: "success", response, - data: respJson as Data, + data: parseResponse(respJson) as Data, }; } @@ -115,6 +119,8 @@ export interface FullParams extends FetchParams { body?: unknown; host?: string; method?: string; + /** Parses `date-time` fields in the response body to `Date`. */ + parseResponse?: (data: unknown) => unknown; } export function mergeParams(a: FetchParams, b: FetchParams): FetchParams { diff --git a/oxide-openapi-gen-ts/src/__snapshots__/util.ts b/oxide-openapi-gen-ts/src/__snapshots__/util.ts index 02a80d2..f15c96d 100644 --- a/oxide-openapi-gen-ts/src/__snapshots__/util.ts +++ b/oxide-openapi-gen-ts/src/__snapshots__/util.ts @@ -45,27 +45,13 @@ export const mapObj = return newObj; }; -const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/; - -export const parseIfDate = (k: string | undefined, v: unknown) => { - if ( - typeof v === "string" && - isoDateRegex.test(v) && - (k?.startsWith("time_") || - k?.endsWith("_time") || - k?.endsWith("_expiration") || - k === "timestamp") - ) { - const d = new Date(v); - if (isNaN(d.getTime())) return v; - return d; - } - return v; -}; - export const snakeify = mapObj(camelToSnake); -export const processResponseBody = mapObj(snakeToCamel, parseIfDate); +// Snake-case to camel-case key conversion only. Date parsing used to happen +// here via a field-name heuristic; it now lives in the generated +// date-parsers.ts, which is driven by `format: date-time` in the schema and +// applied per-endpoint via the `parseResponse` callback in handleResponse. +export const processResponseBody = mapObj(snakeToCamel); export function isNotNull(value: T): value is NonNullable { return value != null; diff --git a/oxide-openapi-gen-ts/src/client/api.ts b/oxide-openapi-gen-ts/src/client/api.ts index f22e46c..dde8198 100644 --- a/oxide-openapi-gen-ts/src/client/api.ts +++ b/oxide-openapi-gen-ts/src/client/api.ts @@ -27,6 +27,7 @@ import { getSortedSchemas, getOperations, } from "./base"; +import type { DateParsers } from "./dateParsers"; import { schemaToTypes } from "../schema/types"; /** @@ -115,7 +116,11 @@ function genParamsInterface( io.w("}\n"); } -export async function generateApi(spec: OpenAPIV3.Document, destDir: string) { +export async function generateApi( + spec: OpenAPIV3.Document, + destDir: string, + dateParsers: DateParsers +) { if (!spec.components) return; const outFile = path.resolve(destDir, "Api.ts"); @@ -128,6 +133,7 @@ export async function generateApi(spec: OpenAPIV3.Document, destDir: string) { import type { FetchParams, FullParams, ApiResult } from "./http-client"; import { dateReplacer, handleResponse, mergeParams, toQueryString } from './http-client' import { snakeify } from './util' + import * as P from './date-parsers' export type { ApiResult, ErrorBody, ErrorResult } from './http-client' `); @@ -218,6 +224,7 @@ export async function generateApi(spec: OpenAPIV3.Document, destDir: string) { path, query, host, + parseResponse, ...fetchParams }: FullParams): Promise> { const url = (host || this.host) + path + toQueryString(query); @@ -225,7 +232,7 @@ export async function generateApi(spec: OpenAPIV3.Document, destDir: string) { ...mergeParams(this.baseParams, fetchParams), body: JSON.stringify(snakeify(body), dateReplacer), }; - return handleResponse(await fetch(url, init)); + return handleResponse(await fetch(url, init), parseResponse); } methods = {`); @@ -271,6 +278,10 @@ export async function generateApi(spec: OpenAPIV3.Document, destDir: string) { if (queryParams.length > 0) { w(" query,"); } + const parseExpr = dateParsers.parserExpr(successType); + if (parseExpr) { + w(` parseResponse: ${parseExpr},`); + } w(` ...params, }) },`); diff --git a/oxide-openapi-gen-ts/src/client/dateParsers.ts b/oxide-openapi-gen-ts/src/client/dateParsers.ts new file mode 100644 index 0000000..408e5be --- /dev/null +++ b/oxide-openapi-gen-ts/src/client/dateParsers.ts @@ -0,0 +1,263 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import type { OpenAPIV3 } from "openapi-types"; + +import { refToSchemaName, type Schema } from "../schema/base"; +import { snakeToCamel } from "../util"; +import { getSortedSchemas, getOperations } from "./base"; + +/** + * Where dates live within a value of some type. Built from `format: date-time` + * in the OpenAPI schema, so it carries the same information as the zod + * `z.coerce.date()` annotations — just compiled to tiny mutating functions + * instead of full validators (see the design notes for why we don't reuse zod). + */ +type DateNode = + | { t: "date" } // an ISO string that should become a Date + | { t: "array"; elem: DateNode } // array whose items carry dates + | { t: "ref"; name: string } // $ref to another date-bearing schema + | { t: "fields"; fields: [field: string, node: DateNode][] }; // object/union + +/** A valid JS identifier can be accessed with dot notation; everything else + * (none today, but cheap insurance) falls back to bracket notation. */ +const access = (obj: string, field: string) => + /^[a-zA-Z_$][\w$]*$/.test(field) + ? `${obj}.${field}` + : `${obj}[${JSON.stringify(field)}]`; + +export interface DateParsers { + /** Full source of the generated `date-parsers.ts` file. */ + content: string; + /** + * Given an operation's success type (e.g. `"Instance"`, `"Instance[]"`, or + * null), return the parser expression a generated method should pass as + * `parseResponse`, e.g. `"P.n2"`, or null if the type carries no dates. + */ + parserExpr: (successType: string | null) => string | null; +} + +/** + * Analyze a spec and compile a date-parsing function for every type that + * transitively contains a `date-time`. Functions are deduplicated by body, so + * the ~145 date-bearing types collapse to a few dozen functions (e.g. every + * `{ time_created, time_modified }` type shares one). + */ +export function buildDateParsers(spec: OpenAPIV3.Document): DateParsers { + const schemas = spec.components?.schemas ?? {}; + + // --- compute a DateNode for each schema, memoized ----------------------- + + const nodeMemo = new Map(); + + const nodeForName = (name: string, stack: Set): DateNode | null => { + if (nodeMemo.has(name)) return nodeMemo.get(name)!; + // Break recursion on cycles. A date-bearing cycle would also break the + // leaf-first emission order below and throw there with a clearer message; + // benign (non-date) cycles just resolve to null on the back-edge. + if (stack.has(name)) return null; + stack.add(name); + const node = computeNode(schemas[name], stack); + stack.delete(name); + nodeMemo.set(name, node); + return node; + }; + + /** Resolve a node to its date fields, following refs. oneOf/allOf members + * that are `$ref`s to object types get flattened into the merged field set + * rather than referencing a separate function, since the union/intersection + * has no function of its own. */ + const fieldsOf = (node: DateNode): [string, DateNode][] => { + if (node.t === "fields") return node.fields; + if (node.t === "ref") { + const target = nodeForName(node.name, new Set()); + if (target) return fieldsOf(target); + } + throw new Error( + `Cannot merge union/intersection member of shape '${node.t}' into date fields` + ); + }; + + /** Merge the date fields of several object-like schemas (oneOf/allOf) by + * field name. Safe because no date field name maps to different shapes + * across the spec. */ + const mergeFields = ( + members: (Schema | undefined)[], + stack: Set + ): DateNode | null => { + const merged = new Map(); + for (const member of members) { + const sub = member && computeNode(member, stack); + if (!sub) continue; + for (const [field, node] of fieldsOf(sub)) { + if (!merged.has(field)) merged.set(field, node); + } + } + return merged.size ? { t: "fields", fields: [...merged] } : null; + }; + + const computeNode = ( + schema: Schema | undefined, + stack: Set + ): DateNode | null => { + if (!schema) return null; + if ("$ref" in schema) { + const name = refToSchemaName(schema.$ref); + return nodeForName(name, stack) ? { t: "ref", name } : null; + } + if (schema.type === "string" && schema.format === "date-time") { + return { t: "date" }; + } + if (schema.type === "array") { + const elem = computeNode(schema.items, stack); + return elem ? { t: "array", elem } : null; + } + if (schema.type === "object" && schema.properties) { + const fields: [string, DateNode][] = []; + for (const [name, sub] of Object.entries(schema.properties)) { + const node = computeNode(sub, stack); + if (node) fields.push([snakeToCamel(name), node]); + } + return fields.length ? { t: "fields", fields } : null; + } + // A single-member oneOf/allOf is just that member (dropshot wraps a $ref + // this way to attach a description); collapse so it stays a ref node. + if (schema.oneOf?.length === 1) return computeNode(schema.oneOf[0], stack); + if (schema.allOf?.length === 1) return computeNode(schema.allOf[0], stack); + if (schema.oneOf) return mergeFields(schema.oneOf, stack); + if (schema.allOf) return mergeFields(schema.allOf, stack); + // scalars, enums, and record types (additionalProperties) carry no dates + return null; + }; + + // --- assign deduplicated function names in dependency order ------------- + + // schema name -> the function name that parses it (e.g. "n0"). Refs alias to + // the referenced schema's function, so multiple names can share one. + const fnForSchema = new Map(); + // function body source -> assigned name, for dedup + const nameForBody = new Map(); + // assigned name -> { body, members }, in assignment order + const fns: { name: string; body: string; members: string[] }[] = []; + let counter = 0; + + /** Name of the function applied to each element of an array. */ + const elemFn = (node: DateNode): string => { + if (node.t === "date") return "D"; + if (node.t === "ref") return fnForSchema.get(node.name)!; + throw new Error(`Unsupported array element shape '${node.t}'`); + }; + + /** Source of the function that parses a value with the given node shape. */ + const bodyFor = (node: DateNode): string => { + if (node.t === "date") return "(o: any) => D(o)"; + if (node.t === "array") return `(o: any) => A(o, ${elemFn(node.elem)})`; + if (node.t === "ref") throw new Error("ref nodes alias, no body"); + const lines = node.fields.map(([field, child]) => { + const lhs = access("o", field); + if (child.t === "date") + return ` if (${lhs} != null) ${lhs} = D(${lhs});`; + if (child.t === "array") + return ` if (${lhs} != null) ${lhs} = A(${lhs}, ${elemFn( + child.elem + )});`; + if (child.t === "ref") + return ` if (${lhs} != null) ${lhs} = ${fnForSchema.get( + child.name + )}(${lhs});`; + throw new Error(`Unsupported nested object field shape '${child.t}'`); + }); + return `(o: any) => {\n if (!o) return o;\n${lines.join( + "\n" + )}\n return o;\n}`; + }; + + /** Register a body, deduping. Returns the assigned function name. */ + const register = (body: string, member: string): string => { + const existing = nameForBody.get(body); + if (existing) { + fns.find((f) => f.name === existing)!.members.push(member); + return existing; + } + const name = `n${counter++}`; + nameForBody.set(body, name); + fns.push({ name, body, members: [member] }); + return name; + }; + + for (const schemaName of getSortedSchemas(spec)) { + const node = nodeForName(schemaName, new Set()); + if (!node) continue; + if (node.t === "ref") { + const target = fnForSchema.get(node.name); + if (!target) { + throw new Error( + `Date-bearing reference cycle through '${schemaName}'; option C needs a function-declaration fallback` + ); + } + fnForSchema.set(schemaName, target); + continue; + } + fnForSchema.set(schemaName, register(bodyFor(node), schemaName)); + } + + // --- array success types (e.g. ScimClientBearerToken[]) need a wrapper -- + + const arrayElemFn = new Map(); // elem schema name -> fn name + for (const op of getOperations(spec)) { + const success = op.successType; + if (!success || !success.endsWith("[]")) continue; + const elem = success.slice(0, -2); + if (arrayElemFn.has(elem) || !fnForSchema.has(elem)) continue; + const body = `(o: any) => A(o, ${fnForSchema.get(elem)})`; + arrayElemFn.set(elem, register(body, `${elem}[]`)); + } + + // --- render the file ---------------------------------------------------- + + const decls = fns.map(({ name, body, members }) => { + const shown = members.slice(0, 4).join(", "); + const more = members.length > 4 ? `, +${members.length - 4} more` : ""; + return `// ${members.length} type${ + members.length > 1 ? "s" : "" + }: ${shown}${more}\nexport const ${name} = ${body};`; + }); + + const content = `/* eslint-disable */ + +/** + * Date parsing for API responses. Each exported function takes a (camelized) + * response value and converts its \`date-time\` fields to \`Date\` in place, + * recursing into nested objects and arrays. Generated from \`format: date-time\` + * in the OpenAPI schema and deduplicated by body, so types with the same date + * shape share one function. \`Api.ts\` passes the right one as \`parseResponse\`. + */ + +const D = (v: any) => { + if (typeof v !== "string") return v; + const d = new Date(v); + return isNaN(d.getTime()) ? v : d; +}; + +const A = (v: any, f: (x: any) => any) => (Array.isArray(v) ? v.map(f) : v); + +${decls.join("\n\n")} +`; + + const parserExpr = (successType: string | null): string | null => { + if (!successType) return null; + if (successType.endsWith("[]")) { + const name = arrayElemFn.get(successType.slice(0, -2)); + return name ? `P.${name}` : null; + } + const name = fnForSchema.get(successType); + return name ? `P.${name}` : null; + }; + + return { content, parserExpr }; +} diff --git a/oxide-openapi-gen-ts/src/client/static/http-client.test.ts b/oxide-openapi-gen-ts/src/client/static/http-client.test.ts index a6c5561..c1264d0 100644 --- a/oxide-openapi-gen-ts/src/client/static/http-client.test.ts +++ b/oxide-openapi-gen-ts/src/client/static/http-client.test.ts @@ -50,29 +50,43 @@ describe("handleResponse", () => { expect(response.headers.get("Content-Type")).toBe("application/json"); }); - it("parses dates and converts to camel case", async () => { + it("converts keys to camel case but does not parse dates on its own", async () => { const resp = json({ time_created: "2022-05-01T02:03:04Z" }); const { response, ...rest } = await handleResponse(resp); expect(rest).toMatchObject({ type: "success", - data: { - timeCreated: new Date(Date.UTC(2022, 4, 1, 2, 3, 4)), - }, + // no parseResponse callback, so the date stays a string + data: { timeCreated: "2022-05-01T02:03:04Z" }, }); expect(response.headers.get("Content-Type")).toBe("application/json"); }); - it("leaves unparseable dates alone", async () => { - const resp = json({ time_created: "abc" }); - const { response, ...rest } = await handleResponse(resp); + // mimics what a generated date-parser does + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const parseTimeCreated = (data: any) => { + if (data?.timeCreated) data.timeCreated = new Date(data.timeCreated); + return data; + }; + + it("applies parseResponse on success", async () => { + const resp = json({ time_created: "2022-05-01T02:03:04Z" }); + const { response, ...rest } = await handleResponse(resp, parseTimeCreated); expect(rest).toMatchObject({ type: "success", - data: { - timeCreated: "abc", - }, + data: { timeCreated: new Date(Date.UTC(2022, 4, 1, 2, 3, 4)) }, }); expect(response.headers.get("Content-Type")).toBe("application/json"); }); + + it("does not apply parseResponse on error", async () => { + const resp = json({ time_created: "2022-05-01T02:03:04Z" }, 400); + const { response, ...rest } = await handleResponse(resp, parseTimeCreated); + expect(rest).toMatchObject({ + type: "error", + data: { timeCreated: "2022-05-01T02:03:04Z" }, + }); + expect(response.status).toEqual(400); + }); }); describe("mergeParams", () => { diff --git a/oxide-openapi-gen-ts/src/client/static/http-client.ts b/oxide-openapi-gen-ts/src/client/static/http-client.ts index 121ec35..f964ec6 100644 --- a/oxide-openapi-gen-ts/src/client/static/http-client.ts +++ b/oxide-openapi-gen-ts/src/client/static/http-client.ts @@ -61,7 +61,11 @@ function encodeQueryParam(key: string, value: unknown) { } export async function handleResponse( - response: Response + response: Response, + // Converts `date-time` fields to `Date` in place. The generated client passes + // the parser for the endpoint's success type; defaults to a no-op so this + // file stays testable without generating anything. + parseResponse: (data: unknown) => unknown = (data) => data ): Promise> { const respText = await response.text(); @@ -93,7 +97,7 @@ export async function handleResponse( return { type: "success", response, - data: respJson as Data, + data: parseResponse(respJson) as Data, }; } @@ -115,6 +119,8 @@ export interface FullParams extends FetchParams { body?: unknown; host?: string; method?: string; + /** Parses `date-time` fields in the response body to `Date`. */ + parseResponse?: (data: unknown) => unknown; } export function mergeParams(a: FetchParams, b: FetchParams): FetchParams { diff --git a/oxide-openapi-gen-ts/src/client/static/util.test.ts b/oxide-openapi-gen-ts/src/client/static/util.test.ts index 4674886..cfc8da6 100644 --- a/oxide-openapi-gen-ts/src/client/static/util.test.ts +++ b/oxide-openapi-gen-ts/src/client/static/util.test.ts @@ -10,7 +10,6 @@ import { camelToSnake, isObjectOrArray, mapObj, - parseIfDate, processResponseBody, snakeify, snakeToCamel, @@ -63,61 +62,18 @@ describe("mapObj", () => { test("processResponseBody", () => { expect(processResponseBody({})).toEqual({}); - const date = new Date(); - const dateStr = date.toISOString(); + const dateStr = new Date().toISOString(); const resp = { id: "big-uuid", another_prop: "abc", time_created: dateStr, }; - expect(processResponseBody(resp)).toMatchObject({ + // only converts keys to camel case; date parsing now happens per-endpoint + // via the generated date-parsers, not here + expect(processResponseBody(resp)).toEqual({ id: "big-uuid", anotherProp: "abc", - timeCreated: expect.any(Date), - }); -}); - -describe("parseIfDate", () => { - it("passes through non-date values", () => { - expect(parseIfDate("abc", 123)).toEqual(123); - expect(parseIfDate("abc", "def")).toEqual("def"); - }); - - const timestamp = 1643092429315; - const dateStr = new Date(timestamp).toISOString(); - - it("doesn't parse dates if key doesn't start with time_", () => { - expect(parseIfDate("abc", dateStr)).toEqual(dateStr); - }); - - it.each(["time_whatever", "auto_thing_expiration", "timestamp"])( - "parses dates if key is '%s'", - (key) => { - const value = parseIfDate(key, dateStr); - expect(value).toBeInstanceOf(Date); - expect((value as Date).getTime()).toEqual(timestamp); - } - ); - - it("passes through values that fail to parse as dates", () => { - const value = parseIfDate("time_whatever", "blah"); - expect(value).toEqual("blah"); - }); - - it.each([ - "2023-01-01T12:00:00Z", - "2023-01-01T12:00:00.1Z", - "2023-01-01T12:00:00.12Z", - "2023-01-01T12:00:00.123Z", - "2023-01-01T12:00:00.1234Z", - "2023-01-01T12:00:00.12345Z", - "2023-01-01T12:00:00.123456Z", - "2023-01-01T12:00:00.123456789Z", - "2023-01-01T12:00:00.123456789123Z", - "2023-01-01T12:00:00.123456789123123Z", - ])("parses dates with fractional digits: %s", (dateString) => { - const value = parseIfDate("time_whatever", dateString); - expect(value).toBeInstanceOf(Date); + timeCreated: dateStr, }); }); diff --git a/oxide-openapi-gen-ts/src/client/static/util.ts b/oxide-openapi-gen-ts/src/client/static/util.ts index 02a80d2..f15c96d 100644 --- a/oxide-openapi-gen-ts/src/client/static/util.ts +++ b/oxide-openapi-gen-ts/src/client/static/util.ts @@ -45,27 +45,13 @@ export const mapObj = return newObj; }; -const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/; - -export const parseIfDate = (k: string | undefined, v: unknown) => { - if ( - typeof v === "string" && - isoDateRegex.test(v) && - (k?.startsWith("time_") || - k?.endsWith("_time") || - k?.endsWith("_expiration") || - k === "timestamp") - ) { - const d = new Date(v); - if (isNaN(d.getTime())) return v; - return d; - } - return v; -}; - export const snakeify = mapObj(camelToSnake); -export const processResponseBody = mapObj(snakeToCamel, parseIfDate); +// Snake-case to camel-case key conversion only. Date parsing used to happen +// here via a field-name heuristic; it now lives in the generated +// date-parsers.ts, which is driven by `format: date-time` in the schema and +// applied per-endpoint via the `parseResponse` callback in handleResponse. +export const processResponseBody = mapObj(snakeToCamel); export function isNotNull(value: T): value is NonNullable { return value != null; diff --git a/oxide-openapi-gen-ts/src/gen.test.ts b/oxide-openapi-gen-ts/src/gen.test.ts index 4fcda5b..b4f60df 100644 --- a/oxide-openapi-gen-ts/src/gen.test.ts +++ b/oxide-openapi-gen-ts/src/gen.test.ts @@ -7,7 +7,10 @@ */ import { test, expect, beforeAll, afterAll } from "vitest"; +import SwaggerParser from "@apidevtools/swagger-parser"; +import type { OpenAPIV3 } from "openapi-types"; import { generate } from "./generate"; +import { buildDateParsers } from "./client/dateParsers"; import { mkdtempSync, rmSync, readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { tmpdir } from "node:os"; @@ -18,10 +21,16 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const SPEC_FILE = getSpecFilePath(join(__dirname, "../../OMICRON_VERSION")); let tempDir: string; +// schema name -> parser expression (e.g. "P.n20"), the same mapping the +// generator bakes into Api.ts. Lets the behavioral test below resolve parsers +// by type name instead of hard-coding `nXX`, which renumbers when the spec does. +let parserExpr: (successType: string | null) => string | null; beforeAll(async () => { tempDir = mkdtempSync(join(tmpdir(), "gen-test-")); await generate(SPEC_FILE, tempDir, { zod: true, msw: true, typetests: true }); + const spec = (await SwaggerParser.parse(SPEC_FILE)) as OpenAPIV3.Document; + parserExpr = buildDateParsers(spec).parserExpr; }); afterAll(() => { @@ -56,6 +65,66 @@ test("util.ts", async () => { await expect(read("util.ts")).toMatchFileSnapshot("./__snapshots__/util.ts"); }); +test("date-parsers.ts", async () => { + await expect(read("date-parsers.ts")).toMatchFileSnapshot( + "./__snapshots__/date-parsers.ts" + ); +}); + +test("generated date parsers convert dates in place", async () => { + // import the freshly generated module and exercise a few representative + // shapes: a flat type, a results page, an array-of-dates, and an array + // success wrapper. Resolve each parser by schema name via parserExpr rather + // than hard-coding `nXX`, so the test keeps asserting the right type even + // when the spec changes and the generated function numbers shift. + const P = (await import(join(tempDir, "date-parsers.ts"))) as Record< + string, + (o: T) => T + >; + const parser = (successType: string) => { + const expr = parserExpr(successType); + if (!expr) throw new Error(`no date parser for '${successType}'`); + return P[expr.slice(2)]; // strip the "P." prefix Api.ts imports under + }; + + const iso = "2022-05-01T02:03:04Z"; + const date = new Date(Date.UTC(2022, 4, 1, 2, 3, 4)); + + // Instance: flat date fields, including a nullable one left untouched + expect( + parser("Instance")({ + timeCreated: iso, + timeModified: iso, + timeLastAutoRestarted: null, + }) + ).toEqual({ + timeCreated: date, + timeModified: date, + timeLastAutoRestarted: null, + }); + + // a results page recurses into items + const page = parser("AddressLotResultsPage")({ + items: [{ timeCreated: iso, timeModified: iso }], + }); + expect(page.items[0]).toEqual({ timeCreated: date, timeModified: date }); + + // Points: arrays of dates + expect(parser("Points")({ timestamps: [iso, iso] })).toEqual({ + timestamps: [date, date], + }); + + // unparseable strings are left alone, absent fields are not added + expect(parser("Instance")({ timeCreated: "nope" })).toEqual({ + timeCreated: "nope", + }); + + // array success wrapper maps the element parser over the array + expect(parser("ScimClientBearerToken[]")([{ timeCreated: iso }])).toEqual([ + { timeCreated: date }, + ]); +}); + test("validate.ts", async () => { await expect(read("validate.ts")).toMatchFileSnapshot( "./__snapshots__/validate.ts" diff --git a/oxide-openapi-gen-ts/src/generate.ts b/oxide-openapi-gen-ts/src/generate.ts index 542c0f9..6b82ea5 100644 --- a/oxide-openapi-gen-ts/src/generate.ts +++ b/oxide-openapi-gen-ts/src/generate.ts @@ -10,11 +10,12 @@ import SwaggerParser from "@apidevtools/swagger-parser"; import type { OpenAPIV3 } from "openapi-types"; import { copyStaticFiles, generateApi } from "./client/api"; +import { buildDateParsers } from "./client/dateParsers"; import { generateMSWHandlers } from "./client/msw-handlers"; import { generateTypeTests } from "./client/type-tests"; import { generateZodValidators } from "./client/zodValidators"; -import { resolve } from "node:path"; -import { existsSync } from "node:fs"; +import { resolve, join } from "node:path"; +import { existsSync, writeFileSync } from "node:fs"; export type Feature = "zod" | "msw" | "typetests"; export const ALL_FEATURES: Feature[] = ["zod", "msw", "typetests"]; @@ -45,7 +46,11 @@ export async function generate( const spec = rawSpec as OpenAPIV3.Document; copyStaticFiles(destDirAbs); - await generateApi(spec, destDirAbs); + // Date parsing is driven by `format: date-time` in the schema, compiled to + // small per-type functions in date-parsers.ts that Api.ts references. + const dateParsers = buildDateParsers(spec); + writeFileSync(join(destDirAbs, "date-parsers.ts"), dateParsers.content); + await generateApi(spec, destDirAbs, dateParsers); if (features.typetests) await generateTypeTests(spec, destDirAbs); if (features.msw) await generateMSWHandlers(spec, destDirAbs); // msw requires zod From 47ed57a6eb86abdb11dc013c2494a4de08a95d13 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Wed, 3 Jun 2026 14:40:17 -0500 Subject: [PATCH 2/3] improve date parser generator tests --- .../src/client/dateParsers.test.ts | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 oxide-openapi-gen-ts/src/client/dateParsers.test.ts diff --git a/oxide-openapi-gen-ts/src/client/dateParsers.test.ts b/oxide-openapi-gen-ts/src/client/dateParsers.test.ts new file mode 100644 index 0000000..c67559b --- /dev/null +++ b/oxide-openapi-gen-ts/src/client/dateParsers.test.ts @@ -0,0 +1,183 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { afterEach, expect, test } from "vitest"; +import type { OpenAPIV3 } from "openapi-types"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { buildDateParsers, type DateParsers } from "./dateParsers"; + +const iso = "2022-05-01T02:03:04Z"; +const date = new Date(Date.UTC(2022, 4, 1, 2, 3, 4)); + +const tempDirs: string[] = []; + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +const ref = (name: string): OpenAPIV3.ReferenceObject => ({ + $ref: `#/components/schemas/${name}`, +}); + +const dateTime: OpenAPIV3.SchemaObject = { + type: "string", + format: "date-time", +}; + +const okResponse = ( + schema: OpenAPIV3.ReferenceObject | OpenAPIV3.ArraySchemaObject +): OpenAPIV3.ResponseObject => ({ + description: "ok", + content: { "application/json": { schema } }, +}); + +const get = ( + operationId: string, + schema: OpenAPIV3.ReferenceObject | OpenAPIV3.ArraySchemaObject +): OpenAPIV3.OperationObject => ({ + operationId, + responses: { "200": okResponse(schema) }, +}); + +const spec = ( + schemas: OpenAPIV3.ComponentsObject["schemas"], + paths: OpenAPIV3.PathsObject = {} +): OpenAPIV3.Document => ({ + openapi: "3.0.3", + info: { title: "test", version: "0.0.0" }, + paths, + components: { schemas }, +}); + +async function importGeneratedParsers(dateParsers: DateParsers) { + const dir = mkdtempSync(join(tmpdir(), "date-parsers-test-")); + tempDirs.push(dir); + const file = join(dir, "date-parsers.ts"); + writeFileSync(file, dateParsers.content); + return (await import(file)) as Record unknown>; +} + +async function parserFor(dateParsers: DateParsers, successType: string) { + const expr = dateParsers.parserExpr(successType); + if (!expr) throw new Error(`no date parser for ${successType}`); + const P = await importGeneratedParsers(dateParsers); + return P[expr.slice(2)]; +} + +test("builds parsers for object, results page, and array success types", async () => { + const dateParsers = buildDateParsers( + spec( + { + Thing: { + type: "object", + properties: { + id: { type: "string" }, + time_created: dateTime, + }, + }, + ThingResultsPage: { + type: "object", + properties: { + items: { type: "array", items: ref("Thing") }, + next_page: { type: "string", nullable: true }, + }, + }, + }, + { + "/things": { get: get("thing_list", ref("ThingResultsPage")) }, + "/things/all": { + get: get("thing_list_all", { type: "array", items: ref("Thing") }), + }, + } + ) + ); + + expect(dateParsers.parserExpr("Plain")).toBeNull(); + + const pageParser = await parserFor(dateParsers, "ThingResultsPage"); + expect(pageParser({ items: [{ id: "x", timeCreated: iso }] })).toEqual({ + items: [{ id: "x", timeCreated: date }], + }); + + const arrayParser = await parserFor(dateParsers, "Thing[]"); + expect(arrayParser([{ id: "x", timeCreated: iso }])).toEqual([ + { id: "x", timeCreated: date }, + ]); +}); + +test("deduplicates schemas with the same date shape", () => { + const dateParsers = buildDateParsers( + spec({ + AddressLot: { + type: "object", + properties: { + time_created: dateTime, + time_modified: dateTime, + }, + }, + Project: { + type: "object", + properties: { + time_created: dateTime, + time_modified: dateTime, + }, + }, + }) + ); + + expect(dateParsers.parserExpr("AddressLot")).toBe( + dateParsers.parserExpr("Project") + ); + expect(dateParsers.content.match(/^export const n/gm)).toHaveLength(1); +}); + +test("handles the Nexus-style oneOf shape where only one variant has dates", async () => { + const dateParsers = buildDateParsers( + spec({ + ExternalIp: { + oneOf: [ + { + type: "object", + properties: { + kind: { type: "string", enum: ["snat"] }, + ip: { type: "string" }, + }, + }, + { + type: "object", + properties: { + kind: { type: "string", enum: ["floating"] }, + ip: { type: "string" }, + time_created: dateTime, + time_modified: dateTime, + }, + }, + ], + }, + }) + ); + + const parser = await parserFor(dateParsers, "ExternalIp"); + + expect(parser({ kind: "snat", ip: "192.0.2.1" })).toEqual({ + kind: "snat", + ip: "192.0.2.1", + }); + expect( + parser({ kind: "floating", ip: "192.0.2.2", timeCreated: iso }) + ).toEqual({ + kind: "floating", + ip: "192.0.2.2", + timeCreated: date, + }); +}); From c6bc17726fdc66bdb280eec8000e1806542513ec Mon Sep 17 00:00:00 2001 From: David Crespo Date: Wed, 3 Jun 2026 17:08:39 -0500 Subject: [PATCH 3/3] fix license header --- oxide-api/src/date-parsers.ts | 8 ++++++++ tools/gen.sh | 10 ++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/oxide-api/src/date-parsers.ts b/oxide-api/src/date-parsers.ts index b8afe1d..798d827 100644 --- a/oxide-api/src/date-parsers.ts +++ b/oxide-api/src/date-parsers.ts @@ -1,3 +1,11 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + /* eslint-disable */ /** diff --git a/tools/gen.sh b/tools/gen.sh index 1e6d367..e92262c 100755 --- a/tools/gen.sh +++ b/tools/gen.sh @@ -48,9 +48,11 @@ rm -f "$DEST_DIR/*" # remove after we add --clean flag to generator # note no features, API client only npx tsx "$ROOT_DIR/oxide-openapi-gen-ts/src/index.ts" $SPEC_FILE $DEST_DIR -# prepend HEADER to Api.ts -API_FILE="$DEST_DIR/Api.ts" -(printf '%s\n\n' "$HEADER"; cat "$API_FILE") > "${API_FILE}.tmp" -mv "${API_FILE}.tmp" "$API_FILE" +# prepend HEADER to generated files that don't emit it themselves +for f in Api.ts date-parsers.ts; do + FILE="$DEST_DIR/$f" + (printf '%s\n\n' "$HEADER"; cat "$FILE") > "${FILE}.tmp" + mv "${FILE}.tmp" "$FILE" +done npx prettier@3.2.5 --write --log-level error "$DEST_DIR"