Skip to content

Commit 8b80182

Browse files
api-clients-generation-pipeline[bot]NouemanKHALci.datadog-api-spec
authored
Improve resiliency of typescript SDK when deserialising enums/oneOfs (#217)
* Add unparsedObject attribute to handle enum/OneOf changes * Add ParseException * Remove Enum Value Check * Keep Enum value Check but ignore failing oneOfs * refactor unparsedObject * delete empty unparsedObject attributes * Refactor ParseException * fix model.mustache * Fix Serialize Method * handle ParseException * fix serialization * serialize objects in response comparison steps * update cassette * fix model * update cassettes * update model * code fixes * fix serialize * apply suggestions - wip * fix typo * add UnparsedObject class to all OneOfs * code fixes * code fixes 2 * return UnparsedObject on enum check fail * code fixes 3 on sync * fix serialization issues * fix pathLookUp * Add Jest unit test framework * Add Deserialization tests * run unit tests in run-tests.sh * remove unused import * code cleanup * fix test names * optimize unit tests time down to 10 seconds * Regenerate client from commit 692cbbb of spec repo Co-authored-by: NouemanKHAL <[email protected]> Co-authored-by: api-clients-generation-pipeline[bot] <54105614+api-clients-generation-pipeline[bot]@users.noreply.github.com> Co-authored-by: ci.datadog-api-spec <[email protected]>
1 parent 7796bf5 commit 8b80182

File tree

734 files changed

+7209
-476
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

734 files changed

+7209
-476
lines changed

.apigentools-info

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
"spec_versions": {
55
"v1": {
66
"apigentools_version": "1.4.1.dev11",
7-
"regenerated": "2021-08-06 09:28:23.037469",
8-
"spec_repo_commit": "47465fb"
7+
"regenerated": "2021-08-06 11:34:02.239847",
8+
"spec_repo_commit": "692cbbb"
99
},
1010
"v2": {
1111
"apigentools_version": "1.4.1.dev11",
12-
"regenerated": "2021-08-06 09:29:50.565498",
13-
"spec_repo_commit": "47465fb"
12+
"regenerated": "2021-08-06 11:35:31.935726",
13+
"spec_repo_commit": "692cbbb"
1414
}
1515
}
1616
}

.generator/templates/model/ObjectSerializer.mustache

+16-7
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const supportedMediaTypes: { [mediaType: string]: number } = {
2121
"application/octet-stream": 0
2222
}
2323

24-
24+
2525
let enumsMap: Set<string> = new Set<string>([
2626
{{#models}}
2727
{{#model}}
@@ -195,16 +195,17 @@ export class ObjectSerializer {
195195
let oneOfs: any[] = [];
196196
for(let oneOf of oneOfMap[type]) {
197197
try {
198-
oneOfs.push(ObjectSerializer.deserialize(data, oneOf, format))
198+
let d = ObjectSerializer.deserialize(data, oneOf, format);
199+
if (d?.unparsedObject === undefined) {
200+
oneOfs.push(d);
201+
}
199202
} catch (e) {
200203
console.debug(`could not deserialize ${oneOf} (${e})`)
201204
}
205+
202206
}
203-
if (oneOfs.length > 1) {
204-
throw new TypeError(`${data} matches multiple types from ${oneOfMap[type]} ${oneOfs}`);
205-
}
206-
if (oneOfs.length == 0) {
207-
throw new TypeError(`${data} doesn't match any type from ${oneOfMap[type]} ${oneOfs}`);
207+
if (oneOfs.length != 1) {
208+
return new UnparsedObject(data);
208209
}
209210
return oneOfs[0];
210211
}
@@ -285,3 +286,11 @@ export class ObjectSerializer {
285286
throw new Error("The mediaType " + mediaType + " is not supported by ObjectSerializer.parse.");
286287
}
287288
}
289+
290+
export class UnparsedObject {
291+
unparsedObject:any;
292+
constructor(data:any) {
293+
this.unparsedObject = data;
294+
}
295+
}
296+

.generator/templates/model/model.mustache

+13-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { {{classname}} } from './{{filename}}{{extensionForDeno}}';
66
{{/tsImports}}
77
import { HttpFile } from '../http/http{{extensionForDeno}}';
8-
import { ObjectSerializer } from './ObjectSerializer';
8+
import { ObjectSerializer, UnparsedObject } from './ObjectSerializer';
99

1010
{{#description}}
1111
/**
@@ -23,6 +23,8 @@ export class {{classname}} {{#parent}}extends {{{parent}}} {{/parent}}{
2323
'{{name}}'{{^required}}?{{/required}}: {{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}};
2424
{{/vars}}
2525

26+
'unparsedObject'?:any;
27+
2628
{{#discriminator}}
2729
static readonly discriminator: string | undefined = "{{discriminatorName}}";
2830
{{/discriminator}}
@@ -65,8 +67,11 @@ export class {{classname}} {{#parent}}extends {{{parent}}} {{/parent}}{
6567
if ([{{#enumVars}}{{{value}}}, {{/enumVars}}undefined].includes(data.{{{baseName}}})) {
6668
res.{{name}} = data.{{{baseName}}};
6769
} else {
68-
throw TypeError(`invalid enum value ${ data.{{{baseName}}} } for {{baseName}}`);
70+
let raw = new {{classname}}();
71+
raw.unparsedObject = data;
72+
return raw;
6973
}
74+
7075
{{/allowableValues}}
7176
{{^allowableValues}}
7277
res.{{{name}}} = ObjectSerializer.deserialize(data.{{{baseName}}}, "{{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}", "{{dataFormat}}")
@@ -85,6 +90,9 @@ export class {{classname}} {{#parent}}extends {{{parent}}} {{/parent}}{
8590
throw new TypeError(`${key} attribute not in schema`);
8691
}
8792
}
93+
if (data?.unparsedObject !== undefined) {
94+
return data.unparsedObject;
95+
}
8896
{{#vars}}
8997
{{#required}}
9098
if (data.{{{name}}} === undefined) {
@@ -105,7 +113,7 @@ export class {{classname}} {{#parent}}extends {{{parent}}} {{/parent}}{
105113
{{/vars}}
106114
return res
107115
}
108-
116+
109117
public constructor() {
110118
{{#parent}}
111119
super();
@@ -137,6 +145,6 @@ export type {{classname}} = {{#allowableValues}}{{#enumVars}}typeof {{#enumClass
137145
export const {{#enumClassPrefix}}{{{classname.toUpperCase}}}_{{/enumClassPrefix}}{{name}} = {{{value}}};
138146
{{/enumVars}}{{/allowableValues}}
139147
{{/isEnum}}
140-
{{#oneOf}}{{#-first}}export type {{classname}} = {{/-first}}{{{.}}}{{^-last}} | {{/-last}}{{#-last}};{{/-last}}{{/oneOf}}
148+
{{#oneOf}}{{#-first}}export type {{classname}} = {{/-first}}{{{.}}}{{^-last}} | {{/-last}}{{#-last}} | UnparsedObject;{{/-last}}{{/oneOf}}
141149
{{/model}}
142-
{{/models}}
150+
{{/models}}

LICENSE-3rdparty.csv

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Component,Licence,Copyright
55
@pollyjs/core,Apache-2.0,
66
@pollyjs/persister-fs,Apache-2.0,
77
@types/chai,MIT,Copyright Microsoft Corporation
8+
@types/jest,MIT,Copyright Microsoft Corporation
89
@types/lodash,MIT,Copyright Microsoft Corporation
910
@types/node-fetch,MIT,Copyright Microsoft Corporation
1011
@types/node,MIT,Copyright Microsoft Corporation
@@ -18,6 +19,8 @@ Component,Licence,Copyright
1819
@typescript-eslint/parser,BSD-2-Clause,
1920
btoa,(MIT OR Apache-2.0),
2021
chai,MIT,Copyright (c) 2017 Chai.js Assertion Library
22+
jest,MIT,Copyright (c) Facebook, Inc. and its affiliates.
23+
ts-jest,MIT,Copyright (c) 2016-2018
2124
dd-trace,BSD-3-Clause,Copyright 2016-Present Datadog Inc.
2225
durations,MIT,Copyright (c) 2015, Joel Edwards
2326
es6-promise,MIT,Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"2021-07-23T14:46:28.617Z"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
{
2+
"log": {
3+
"_recordingName": "Synthetics/Client is resilient to enum and oneOf deserialization errors",
4+
"creator": {
5+
"comment": "persister:fs",
6+
"name": "Polly.JS",
7+
"version": "5.1.0"
8+
},
9+
"entries": [
10+
{
11+
"_id": "e05c99aa30c67aa268cd349e71046fcb",
12+
"_order": 0,
13+
"cache": {},
14+
"request": {
15+
"bodySize": 0,
16+
"cookies": [],
17+
"headers": [
18+
{
19+
"_fromType": "array",
20+
"name": "user-agent",
21+
"value": "datadog-api-client-typescript/1.0.0-beta.4 (node 15.11.0; os Darwin; arch x64)"
22+
},
23+
{
24+
"_fromType": "array",
25+
"name": "accept",
26+
"value": "application/json, */*;q=0.8"
27+
},
28+
{
29+
"_fromType": "array",
30+
"name": "x-datadog-parent-id",
31+
"value": "7301385942137946346"
32+
},
33+
{
34+
"_fromType": "array",
35+
"name": "x-datadog-trace-id",
36+
"value": "7301385942137946346"
37+
},
38+
{
39+
"_fromType": "array",
40+
"name": "connection",
41+
"value": "close"
42+
},
43+
{
44+
"name": "host",
45+
"value": "api.datadoghq.com"
46+
}
47+
],
48+
"headersSize": 430,
49+
"httpVersion": "HTTP/1.1",
50+
"method": "GET",
51+
"queryString": [],
52+
"url": "https://api.datadoghq.com/api/v1/synthetics/tests"
53+
},
54+
"response": {
55+
"bodySize": 4308,
56+
"content": {
57+
"mimeType": "application/json",
58+
"size": 4308,
59+
"text": "{\"tests\":[{\"status\":\"paused\",\"public_id\":\"jv7-wfd-kvt\",\"tags\":[],\"locations\":[\"pl:pl-kevin-y-6382df0d72d4588e1817f090b131541f\"],\"message\":\"\",\"name\":\"Test on www.example.com\",\"monitor_id\":28558768,\"type\":\"api\",\"created_at\":\"2021-01-12T10:11:40.802074+00:00\",\"modified_at\":\"2021-01-22T16:42:10.520384+00:00\",\"subtype\":\"http\",\"config\":{\"request\":{\"url\":\"https://www.example.com\",\"method\":\"GET\",\"timeout\":30},\"assertions\":[{\"operator\":\"lessThan\",\"type\":\"responseTime\",\"target\":1000},{\"operator\":\"is\",\"type\":\"statusCode\",\"target\":200},{\"operator\":\"A non existent operator\",\"type\":\"body\",\"target\":{\"xPath\":\"//html/head/title\",\"operator\":\"contains\",\"targetValue\":\"Example\"}}],\"configVariables\":[]},\"options\":{\"monitor_options\":{\"notify_audit\":false,\"locked\":false,\"include_tags\":true,\"new_host_delay\":300,\"notify_no_data\":false,\"renotify_interval\":0},\"retry\":{\"count\":0,\"interval\":300},\"min_location_failed\":1,\"min_failure_duration\":0,\"tick_every\":60}},{\"status\":\"paused\",\"public_id\":\"jv7-wfd-kvt\",\"tags\":[],\"locations\":[\"pl:pl-kevin-y-6382df0d72d4588e1817f090b131541f\"],\"message\":\"\",\"name\":\"Test on www.example.com\",\"monitor_id\":28558768,\"type\":\"api\",\"created_at\":\"2021-01-12T10:11:40.802074+00:00\",\"modified_at\":\"2021-01-22T16:42:10.520384+00:00\",\"subtype\":\"http\",\"config\":{\"request\":{\"url\":\"https://www.example.com\",\"method\":\"GET\",\"timeout\":30},\"assertions\":[{\"operator\":\"lessThan\",\"type\":\"responseTime\",\"target\":1000},{\"operator\":\"is\",\"type\":\"A non existent assertion type\",\"target\":200}],\"configVariables\":[]},\"options\":{\"monitor_options\":{\"notify_audit\":false,\"locked\":false,\"include_tags\":true,\"new_host_delay\":300,\"notify_no_data\":false,\"renotify_interval\":0},\"retry\":{\"count\":0,\"interval\":300},\"min_location_failed\":1,\"min_failure_duration\":0,\"tick_every\":60}},{\"status\":\"live\",\"public_id\":\"2fx-64b-fb8\",\"tags\":[\"mini-website\",\"team:synthetics\",\"firefox\",\"synthetics-ci-browser\",\"edge\",\"chrome\"],\"locations\":[\"aws:ap-northeast-1\",\"aws:eu-north-1\",\"aws:eu-west-3\",\"aws:eu-central-1\"],\"message\":\"This mini-website check failed, please investigate why. @slack-synthetics-ops-worker\",\"name\":\"Mini Website - Click Trap\",\"monitor_id\":7647262,\"type\":\"browser\",\"created_at\":\"2018-12-20T13:19:23.734004+00:00\",\"modified_at\":\"2021-06-30T15:46:49.387631+00:00\",\"config\":{\"variables\":[],\"setCookie\":\"\",\"request\":{\"url\":\"http://34.95.79.70/click-trap\",\"headers\":{},\"method\":\"GET\"},\"assertions\":[],\"configVariables\":[]},\"options\":{\"ci\":{\"executionRule\":\"blocking\"},\"retry\":{\"count\":1,\"interval\":1000},\"min_location_failed\":1,\"min_failure_duration\":0,\"noScreenshot\":false,\"tick_every\":300,\"forwardProxy\":false,\"disableCors\":false,\"device_ids\":[\"chrome.laptop_large\",\"firefox.laptop_large\",\"A non existent device ID\"],\"monitor_options\":{\"renotify_interval\":360},\"ignoreServerCertificateError\":true}},{\"status\":\"live\",\"public_id\":\"g6d-gcm-pdq\",\"tags\":[],\"locations\":[\"aws:eu-central-1\",\"aws:ap-northeast-1\"],\"message\":\"\",\"name\":\"Check on www.10.0.0.1.xip.io\",\"monitor_id\":7464050,\"type\":\"A non existent test type\",\"created_at\":\"2018-12-07T17:30:49.785089+00:00\",\"modified_at\":\"2019-09-04T17:01:09.921070+00:00\",\"subtype\":\"http\",\"config\":{\"request\":{\"url\":\"https://www.10.0.0.1.xip.io\",\"method\":\"GET\",\"timeout\":30},\"assertions\":[{\"operator\":\"is\",\"type\":\"statusCode\",\"target\":200}]},\"options\":{\"tick_every\":60}},{\"status\":\"live\",\"public_id\":\"g6d-gcm-pdq\",\"tags\":[],\"locations\":[\"aws:eu-central-1\",\"aws:ap-northeast-1\"],\"message\":\"\",\"name\":\"Check on www.10.0.0.1.xip.io\",\"monitor_id\":7464050,\"type\":\"api\",\"created_at\":\"2018-12-07T17:30:49.785089+00:00\",\"modified_at\":\"2019-09-04T17:01:09.921070+00:00\",\"subtype\":\"http\",\"config\":{\"request\":{\"url\":\"https://www.10.0.0.1.xip.io\",\"method\":\"A non existent method\",\"timeout\":30},\"assertions\":[{\"operator\":\"is\",\"type\":\"statusCode\",\"target\":200}]},\"options\":{\"tick_every\":60}},{\"status\":\"live\",\"public_id\":\"g6d-gcm-pdq\",\"tags\":[],\"locations\":[\"aws:eu-central-1\",\"aws:ap-northeast-1\"],\"message\":\"A fully valid test\",\"name\":\"Check on www.10.0.0.1.xip.io\",\"monitor_id\":7464050,\"type\":\"api\",\"created_at\":\"2018-12-07T17:30:49.785089+00:00\",\"modified_at\":\"2019-09-04T17:01:09.921070+00:00\",\"subtype\":\"http\",\"config\":{\"request\":{\"url\":\"https://www.10.0.0.1.xip.io\",\"method\":\"GET\",\"timeout\":30},\"assertions\":[{\"operator\":\"is\",\"type\":\"statusCode\",\"target\":200}]},\"options\":{\"tick_every\":60}}]}"
60+
},
61+
"cookies": [],
62+
"headers": [
63+
{
64+
"name": "date",
65+
"value": "Fri, 23 Jul 2021 14:46:29 GMT"
66+
},
67+
{
68+
"name": "content-type",
69+
"value": "application/json"
70+
},
71+
{
72+
"name": "content-length",
73+
"value": "4308"
74+
},
75+
{
76+
"name": "connection",
77+
"value": "close"
78+
},
79+
{
80+
"name": "vary",
81+
"value": "Accept-Encoding"
82+
},
83+
{
84+
"name": "pragma",
85+
"value": "no-cache"
86+
},
87+
{
88+
"name": "cache-control",
89+
"value": "no-cache"
90+
},
91+
{
92+
"name": "x-ratelimit-limit",
93+
"value": "120"
94+
},
95+
{
96+
"name": "x-ratelimit-period",
97+
"value": "60"
98+
},
99+
{
100+
"name": "x-ratelimit-reset",
101+
"value": "31"
102+
},
103+
{
104+
"name": "x-ratelimit-remaining",
105+
"value": "119"
106+
},
107+
{
108+
"name": "x-content-type-options",
109+
"value": "nosniff"
110+
},
111+
{
112+
"name": "strict-transport-security",
113+
"value": "max-age=15724800;"
114+
},
115+
{
116+
"name": "content-security-policy",
117+
"value": "frame-ancestors 'self'; report-uri https://api.datadoghq.com/csp-report"
118+
},
119+
{
120+
"name": "x-frame-options",
121+
"value": "SAMEORIGIN"
122+
}
123+
],
124+
"headersSize": 484,
125+
"httpVersion": "HTTP/1.1",
126+
"redirectURL": "",
127+
"status": 200,
128+
"statusText": "OK"
129+
},
130+
"startedDateTime": "2021-07-23T14:46:28.637Z",
131+
"time": 817,
132+
"timings": {
133+
"blocked": -1,
134+
"connect": -1,
135+
"dns": -1,
136+
"receive": 0,
137+
"send": 0,
138+
"ssl": -1,
139+
"wait": 817
140+
}
141+
}
142+
],
143+
"pages": [],
144+
"version": "1.2"
145+
}
146+
}

features/support/templating.ts

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ function pathLookup(data: any, dottedPath: string): any {
6565
result = value[part];
6666
} else if (part.toAttributeName() in value) {
6767
result = value[part.toAttributeName()];
68+
} else if ("unparsedObject" in value && part in value["unparsedObject"]) {
69+
result = value["unparsedObject"][part];
6870
} else {
6971
throw new Error(
7072
`${part} not found in ${JSON.stringify(

features/v1/synthetics.feature

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Feature: Synthetics
1515
And a valid "appKeyAuth" key in the system
1616
And an instance of "Synthetics" API
1717

18-
@replay-only @skip-java @skip-python @skip-ruby @skip-typescript
18+
@replay-only @skip-python @skip-java @skip-ruby
1919
Scenario: Client is resilient to enum and oneOf deserialization errors
2020
Given new "ListTests" request
2121
When the request is sent

jest.config.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
transform: {'^.+\\.ts?$': 'ts-jest'},
3+
testEnvironment: 'node',
4+
testRegex: '/tests/api/.*\\.(test|spec)?\\.(ts|tsx)$',
5+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
6+
};

package.json

+7-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
4646
"prepare": "yarn run build",
4747
"test": "node bin/dd-cucumber-js features/",
48-
"test:rerecord": "RECORD=true node bin/dd-cucumber-js @rerun.txt"
48+
"test:rerecord": "RECORD=true node bin/dd-cucumber-js @rerun.txt",
49+
"jest-test": "jest --no-cache",
50+
"jest-coverage": "jest --no-cache --coverage"
4951
},
5052
"dependencies": {
5153
"@types/node": "*",
@@ -64,6 +66,7 @@
6466
"@pollyjs/core": "^5.1.0",
6567
"@pollyjs/persister-fs": "^5.0.0",
6668
"@types/chai": "^4.2.14",
69+
"@types/jest": "^26.0.24",
6770
"@types/lodash": "^4.14.168",
6871
"@types/pollyjs__adapter-node-http": "^2.0.1",
6972
"@types/pollyjs__core": "^4.3.2",
@@ -81,9 +84,11 @@
8184
"eslint-plugin-node": "^11.1.0",
8285
"eslint-plugin-prettier": "^3.3.1",
8386
"eslint-plugin-unused-imports": "^1.1.1",
87+
"jest": "^27.0.6",
8488
"prettier": "^2.2.1",
89+
"ts-jest": "^27.0.4",
8590
"ts-node": "^9.1.1",
86-
"typescript": "^3.9.8"
91+
"typescript": "^3.9.10"
8792
},
8893
"engines": {
8994
"node": ">=12.0.0"

packages/datadog-api-client-v1/models/APIErrorResponse.ts

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export class APIErrorResponse {
2020
*/
2121
"errors": Array<string>;
2222

23+
"unparsedObject"?: any;
24+
2325
static readonly discriminator: string | undefined = undefined;
2426

2527
static readonly attributeTypeMap: {
@@ -57,6 +59,9 @@ export class APIErrorResponse {
5759
throw new TypeError(`${key} attribute not in schema`);
5860
}
5961
}
62+
if (data?.unparsedObject !== undefined) {
63+
return data.unparsedObject;
64+
}
6065
if (data.errors === undefined) {
6166
throw new TypeError(
6267
"missing required attribute 'errors' on 'APIErrorResponse' object"

packages/datadog-api-client-v1/models/AWSAccount.ts

+5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export class AWSAccount {
4848
*/
4949
"secretAccessKey"?: string;
5050

51+
"unparsedObject"?: any;
52+
5153
static readonly discriminator: string | undefined = undefined;
5254

5355
static readonly attributeTypeMap: {
@@ -153,6 +155,9 @@ export class AWSAccount {
153155
throw new TypeError(`${key} attribute not in schema`);
154156
}
155157
}
158+
if (data?.unparsedObject !== undefined) {
159+
return data.unparsedObject;
160+
}
156161
res.access_key_id = ObjectSerializer.serialize(
157162
data.accessKeyId,
158163
"string",

0 commit comments

Comments
 (0)