Skip to content

Commit 8903e20

Browse files
skarimoci.datadog-api-spectherve
authored
Validate BDD models in tests (#904)
* convert params to models in bdd tests Co-authored-by: ci.datadog-api-spec <[email protected]> Co-authored-by: Thomas Hervé <[email protected]>
1 parent 2efe6ac commit 8903e20

File tree

1,427 files changed

+6944
-1633
lines changed

Some content is hidden

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

1,427 files changed

+6944
-1633
lines changed

.generator/src/generator/cli.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ def cli(specs, output):
7171
"index.ts": env.get_template("index.j2"),
7272
}
7373

74+
test_scenarios_files = {
75+
"scenarios_model_mapping.ts": env.get_template("scenarios_model_mapping.j2")
76+
}
77+
7478
all_specs = {}
7579
all_apis = {}
7680
for spec_path in specs:
@@ -127,3 +131,10 @@ def cli(specs, output):
127131
filename.parent.mkdir(parents=True, exist_ok=True)
128132
with filename.open("w+") as fp:
129133
fp.write(template.render(apis=all_apis))
134+
135+
# Parameter mappings for bdd tests
136+
scenarios_test_output = pathlib.Path("../features/support/")
137+
for name, template in test_scenarios_files.items():
138+
filename = scenarios_test_output / name
139+
with filename.open("w") as fp:
140+
fp.write(template.render(all_apis=all_apis))

.generator/src/generator/templates/model/ObjectSerializer.j2

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { {{ name }} } from "./{{ name }}";
44
{%- endif %}
55
{%- endfor %}
6-
import { UnparsedObject } from "../../datadog-api-client-common/util";
6+
import { dateFromRFC3339String, dateToRFC3339String, UnparsedObject } from "../../datadog-api-client-common/util";
77
import { logger } from "../../../logger";
88

99
const primitives = [
@@ -54,6 +54,8 @@ export class ObjectSerializer {
5454
public static serialize(data: any, type: string, format: string): any {
5555
if (data == undefined || type == "any") {
5656
return data;
57+
} else if (data instanceof UnparsedObject) {
58+
return data._data;
5759
} else if (primitives.includes(type.toLowerCase()) && typeof data == type.toLowerCase()) {
5860
return data;
5961
} else if (type.startsWith(ARRAY_PREFIX)) {
@@ -87,12 +89,8 @@ export class ObjectSerializer {
8789
if ("string" == typeof data) {
8890
return data;
8991
}
90-
if (format == "date") {
91-
let month = data.getMonth() + 1
92-
month = month < 10 ? "0" + month.toString() : month.toString()
93-
let day = data.getDate();
94-
day = day < 10 ? "0" + day.toString() : day.toString();
95-
return data.getFullYear() + "-" + month + "-" + day;
92+
if (format == "date" || format == "date-time") {
93+
return dateToRFC3339String(data)
9694
} else {
9795
return data.toISOString();
9896
}
@@ -129,21 +127,6 @@ export class ObjectSerializer {
129127
const attributesMap = typeMap[type].getAttributeTypeMap();
130128
const instance: {[index: string]: any} = {};
131129

132-
const extraAttributes = Object.keys(data)
133-
.filter((key) => !Object.prototype.hasOwnProperty.call(attributesMap, key))
134-
.reduce((obj, key) => {
135-
return Object.assign(obj, {
136-
[key]: data[key]
137-
});
138-
}, {});
139-
140-
if (Object.keys(extraAttributes).length !== 0) {
141-
if (!data.additionalProperties) {
142-
data.additionalProperties = {};
143-
}
144-
Object.assign(data.additionalProperties, extraAttributes);
145-
}
146-
147130
for (const attributeName in attributesMap) {
148131
const attributeObj = attributesMap[attributeName];
149132
if (attributeName == "additionalProperties") {
@@ -163,11 +146,8 @@ export class ObjectSerializer {
163146
if (attributeObj?.required && instance[attributeObj.baseName] === undefined) {
164147
throw new Error(`missing required property '${attributeObj.baseName}'`);
165148
}
166-
167-
if (enumsMap[attributeObj.type] && !enumsMap[attributeObj.type].includes(instance[attributeObj.baseName])) {
168-
instance.unparsedObject = instance[attributeObj.baseName];
169-
}
170149
}
150+
171151
return instance;
172152
}
173153
}
@@ -206,18 +186,20 @@ export class ObjectSerializer {
206186
}
207187
return transformedData;
208188
} else if (type === "Date") {
209-
return new Date(data);
189+
return dateFromRFC3339String(data)
210190
} else {
211191
if (enumsMap[type]) {
212-
return data;
192+
if (enumsMap[type].includes(data)) {
193+
return data;
194+
}
195+
return new UnparsedObject(data)
213196
}
214-
215197
if (oneOfMap[type]) {
216198
const oneOfs: any[] = [];
217199
for (const oneOf of oneOfMap[type]) {
218200
try {
219201
const d = ObjectSerializer.deserialize(data, oneOf, format);
220-
if (d?.unparsedObject === undefined) {
202+
if (!d?._unparsed) {
221203
oneOfs.push(d);
222204
}
223205
} catch (e) {
@@ -237,18 +219,49 @@ export class ObjectSerializer {
237219

238220
const instance = new typeMap[type]();
239221
const attributesMap = typeMap[type].getAttributeTypeMap();
222+
let extraAttributes: any = []
223+
if ("additionalProperties" in attributesMap) {
224+
const attributesBaseNames = Object.keys(attributesMap).reduce((o, key) => Object.assign(o, {[attributesMap[key].baseName]: ""}), {});
225+
extraAttributes = Object.keys(data).filter((key) => !Object.prototype.hasOwnProperty.call(attributesBaseNames, key))
226+
}
240227

241228
for (const attributeName in attributesMap) {
242229
const attributeObj = attributesMap[attributeName];
230+
if (attributeName == "additionalProperties") {
231+
if (extraAttributes.length > 0) {
232+
if (!instance.additionalProperties) {
233+
instance.additionalProperties = {};
234+
}
235+
236+
for (const key in extraAttributes) {
237+
instance.additionalProperties[extraAttributes[key]] = ObjectSerializer.deserialize(
238+
data[extraAttributes[key]],
239+
attributeObj.type,
240+
attributeObj.format
241+
);
242+
}
243+
}
244+
continue;
245+
}
246+
243247
instance[attributeName] = ObjectSerializer.deserialize(data[attributeObj.baseName], attributeObj.type, attributeObj.format);
248+
244249
// check for required properties
245250
if (attributeObj?.required && instance[attributeName] === undefined) {
246251
throw new Error(`missing required property '${attributeName}'`);
247252
}
248253

249-
// check for enum values
250-
if (enumsMap[attributeObj.type] && !enumsMap[attributeObj.type].includes(instance[attributeName])) {
251-
instance.unparsedObject = instance[attributeName];
254+
if (instance[attributeName] instanceof UnparsedObject || instance[attributeName]?._unparsed) {
255+
instance._unparsed = true
256+
}
257+
258+
if (Array.isArray(instance[attributeName])) {
259+
for (const d of instance[attributeName]) {
260+
if (d instanceof UnparsedObject || d?._unparsed) {
261+
instance._unparsed = true
262+
break
263+
}
264+
}
252265
}
253266
}
254267

.generator/src/generator/templates/model/model.j2

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ import { {{ classname }} } from "./{{ classname }}";
88

99
import { HttpFile } from "../../datadog-api-client-common/http/http";
1010

11-
{% if "enum" not in model -%}
12-
import { AttributeTypeMap{%- if "oneOf" in model %}, UnparsedObject{% endif %} } from "../../datadog-api-client-common/util";
11+
{% if "enum" not in model and "oneOf" not in model -%}
12+
import { AttributeTypeMap } from "../../datadog-api-client-common/util";
13+
{%- endif %}
14+
15+
{% if "enum" in model or "oneOf" in model -%}
16+
import { UnparsedObject } from "../../datadog-api-client-common/util";
1317
{%- endif %}
1418

1519
{% if "description" in model %}
@@ -42,7 +46,7 @@ export class {{ name }} {
4246
/**
4347
* @ignore
4448
*/
45-
"unparsedObject"?:any;
49+
"_unparsed"?: boolean;
4650
{%- if "items" not in model %}
4751

4852
/**
@@ -116,7 +120,7 @@ export class {{ name }} {
116120

117121

118122
{%- if "enum" in model %}
119-
export type {{ name }} = {% for index, value in enumerate(model.enum) %}typeof {{ model["x-enum-varnames"][index] or value.upper() }} {%- if not loop.last %}| {% endif %}{%- endfor %};
123+
export type {{ name }} = {% for index, value in enumerate(model.enum) %}typeof {{ model["x-enum-varnames"][index] or value.upper() }} {%- if not loop.last %}| {% endif %}{%- if loop.last %} | UnparsedObject{%- endif %}{%- endfor %};
120124
{%- for index, value in enumerate(model.enum) %}
121125
export const {{ model["x-enum-varnames"][index] or value.upper() }} = {{ value|format_value }};
122126
{%- endfor %}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export const ScenariosModelMappings: {[key: string]: {[key: string]: any}} = {
2+
{%- for version, apis in all_apis.items() %}
3+
{%- for _, operations in apis.items() %}
4+
{%- for path, method, operation in operations %}
5+
{%- set operationParams = operation|parameters|list %}
6+
"{{ version }}.{{ operation['operationId'] }}": {
7+
{%- for name, parameter in operation|parameters %}
8+
"{{ name|attribute_name }}": {
9+
"type": "{{ get_type_for_parameter(parameter) }}",
10+
"format": "{{ get_format_for_schema(parameter) }}",
11+
},
12+
{%- endfor %}
13+
"operationResponseType": "{{ operation|return_type }}",
14+
},
15+
{%- endfor %}
16+
{%- endfor %}
17+
{%- endfor %}
18+
}

.generator/src/generator/templates/util.j2

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11

22
export class UnparsedObject {
3-
unparsedObject:any;
3+
_data:any;
44
constructor(data:any) {
5-
this.unparsedObject = data;
5+
this._data = data;
66
}
77
}
88

@@ -18,3 +18,76 @@ export type AttributeTypeMap = {
1818
export const isBrowser: boolean = typeof window !== "undefined" && typeof window.document !== "undefined";
1919

2020
export const isNode: boolean = typeof process !== "undefined" && process.release && process.release.name === 'node';
21+
22+
export class DDate extends Date {
23+
rfc3339TzOffset: string | undefined;
24+
}
25+
26+
const RFC3339Re: RegExp = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})\.?(\d+)?(?:(?:([+-]\d{2}):?(\d{2}))|Z)?$/;
27+
export function dateFromRFC3339String(date: string): DDate {
28+
const m = RFC3339Re.exec(date);
29+
if (m) {
30+
const _date = new DDate(date)
31+
if( m[8] === undefined && m[9] === undefined){
32+
_date.rfc3339TzOffset = 'Z'
33+
} else {
34+
_date.rfc3339TzOffset = `${m[8]}:${m[9]}`
35+
}
36+
37+
return _date
38+
} else {
39+
throw new Error('unexpected date format: ' + date)
40+
}
41+
}
42+
43+
export function dateToRFC3339String(date: Date | DDate): string {
44+
const offSetArr = getRFC3339TimezoneOffset(date).split(":")
45+
const tzHour = offSetArr.length == 1 ? 0 : +offSetArr[0];
46+
const tzMin = offSetArr.length == 1 ? 0 : +offSetArr[1];
47+
48+
const year = date.getFullYear() ;
49+
const month = date.getMonth();
50+
const day = date.getUTCDate();
51+
const hour = date.getUTCHours() + +tzHour;
52+
const minute = date.getUTCMinutes() + +tzMin;
53+
const second = date.getUTCSeconds();
54+
55+
let msec = date.getUTCMilliseconds().toString();
56+
msec = +msec === 0 ? "" : `.${pad(+msec, 3)}`
57+
58+
return year + "-" +
59+
pad(month + 1) + "-" +
60+
pad(day) + "T" +
61+
pad(hour) + ":" +
62+
pad(minute) + ":" +
63+
pad(second) +
64+
msec +
65+
offSetArr.join(":");
66+
}
67+
68+
// Helpers
69+
function pad(num: number, len: number = 2): string {
70+
let paddedNum = num.toString()
71+
if (paddedNum.length < len) {
72+
paddedNum = "0".repeat(len - paddedNum.length) + paddedNum
73+
} else if (paddedNum.length > len) {
74+
paddedNum = paddedNum.slice(0, len)
75+
}
76+
77+
return paddedNum
78+
}
79+
80+
function getRFC3339TimezoneOffset(date: Date | DDate): string {
81+
if (date instanceof DDate && date.rfc3339TzOffset) {
82+
return date.rfc3339TzOffset;
83+
}
84+
85+
let offset = date.getTimezoneOffset()
86+
if (offset === 0) {
87+
return "Z";
88+
}
89+
90+
const offsetSign = (offset > 0) ? "+" : "-";
91+
offset = Math.abs(offset);
92+
return offsetSign + pad(Math.floor(offset / 60)) + ":" + pad(offset % 60);
93+
}

0 commit comments

Comments
 (0)