@grafana/openapi-to-k6 0.2.6 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/constants.js +7 -3
- package/dist/generator/index.js +13 -6
- package/dist/generator/k6Client.js +8 -56
- package/dist/generator/k6ScriptBuilder.js +256 -0
- package/dist/helper.js +16 -0
- package/examples/basic_schema/schema.json +2 -1
- package/examples/basic_schema/single/k6-script.sample.ts +3 -2
- package/examples/basic_schema/single/simpleAPI.ts +4 -1
- package/examples/basic_schema/split/k6-script.sample.ts +3 -2
- package/examples/basic_schema/split/simpleAPI.schemas.ts +2 -1
- package/examples/basic_schema/split/simpleAPI.ts +3 -1
- package/examples/basic_schema/tags/default.ts +3 -1
- package/examples/basic_schema/tags/k6-script.sample.ts +4 -3
- package/examples/basic_schema/tags/simpleAPI.schemas.ts +2 -1
- package/examples/form_data_schema/schema.json +6 -3
- package/examples/form_data_schema/single/formDataAPI.ts +8 -6
- package/examples/form_data_schema/single/k6-script.sample.ts +10 -2
- package/examples/form_data_schema/split/formDataAPI.schemas.ts +5 -5
- package/examples/form_data_schema/split/formDataAPI.ts +4 -2
- package/examples/form_data_schema/split/k6-script.sample.ts +10 -2
- package/examples/form_data_schema/tags/default.ts +4 -2
- package/examples/form_data_schema/tags/formDataAPI.schemas.ts +5 -5
- package/examples/form_data_schema/tags/k6-script.sample.ts +11 -3
- package/examples/form_url_encoded_data_schema/schema.json +6 -3
- package/examples/form_url_encoded_data_schema/single/formURLEncodedAPI.ts +8 -6
- package/examples/form_url_encoded_data_schema/single/k6-script.sample.ts +11 -2
- package/examples/form_url_encoded_data_schema/split/formURLEncodedAPI.schemas.ts +5 -5
- package/examples/form_url_encoded_data_schema/split/formURLEncodedAPI.ts +4 -2
- package/examples/form_url_encoded_data_schema/split/k6-script.sample.ts +11 -2
- package/examples/form_url_encoded_data_schema/tags/default.ts +4 -2
- package/examples/form_url_encoded_data_schema/tags/formURLEncodedAPI.schemas.ts +5 -5
- package/examples/form_url_encoded_data_schema/tags/k6-script.sample.ts +12 -3
- package/examples/form_url_encoded_data_with_query_params_schema/schema.json +8 -4
- package/examples/form_url_encoded_data_with_query_params_schema/single/formURLEncodedAPIWithQueryParameters.ts +8 -6
- package/examples/form_url_encoded_data_with_query_params_schema/single/k6-script.sample.ts +19 -6
- package/examples/form_url_encoded_data_with_query_params_schema/split/formURLEncodedAPIWithQueryParameters.schemas.ts +5 -5
- package/examples/form_url_encoded_data_with_query_params_schema/split/formURLEncodedAPIWithQueryParameters.ts +4 -2
- package/examples/form_url_encoded_data_with_query_params_schema/split/k6-script.sample.ts +19 -6
- package/examples/form_url_encoded_data_with_query_params_schema/tags/default.ts +4 -2
- package/examples/form_url_encoded_data_with_query_params_schema/tags/formURLEncodedAPIWithQueryParameters.schemas.ts +5 -5
- package/examples/form_url_encoded_data_with_query_params_schema/tags/k6-script.sample.ts +14 -3
- package/examples/get_request_with_path_parameters_schema/schema.json +2 -1
- package/examples/get_request_with_path_parameters_schema/single/k6-script.sample.ts +6 -2
- package/examples/get_request_with_path_parameters_schema/single/simpleAPI.ts +4 -2
- package/examples/get_request_with_path_parameters_schema/split/k6-script.sample.ts +6 -2
- package/examples/get_request_with_path_parameters_schema/split/simpleAPI.schemas.ts +2 -2
- package/examples/get_request_with_path_parameters_schema/split/simpleAPI.ts +3 -1
- package/examples/get_request_with_path_parameters_schema/tags/default.ts +3 -1
- package/examples/get_request_with_path_parameters_schema/tags/k6-script.sample.ts +7 -3
- package/examples/get_request_with_path_parameters_schema/tags/simpleAPI.schemas.ts +2 -2
- package/examples/headers_schema/schema.json +2 -1
- package/examples/headers_schema/single/headerDemoAPI.ts +5 -3
- package/examples/headers_schema/single/k6-script.sample.ts +15 -4
- package/examples/headers_schema/split/headerDemoAPI.schemas.ts +1 -1
- package/examples/headers_schema/split/headerDemoAPI.ts +5 -3
- package/examples/headers_schema/split/k6-script.sample.ts +15 -4
- package/examples/headers_schema/tags/default.ts +5 -3
- package/examples/headers_schema/tags/headerDemoAPI.schemas.ts +1 -1
- package/examples/headers_schema/tags/k6-script.sample.ts +16 -5
- package/examples/no_title_schema/single/k6-script.sample.ts +3 -2
- package/examples/no_title_schema/single/k6Client.ts +3 -1
- package/examples/no_title_schema/split/k6-script.sample.ts +3 -2
- package/examples/no_title_schema/split/k6Client.schemas.ts +1 -1
- package/examples/no_title_schema/split/k6Client.ts +3 -1
- package/examples/no_title_schema/tags/default.ts +3 -1
- package/examples/no_title_schema/tags/k6-script.sample.ts +4 -3
- package/examples/no_title_schema/tags/k6Client.schemas.ts +1 -1
- package/examples/post_request_with_query_params/schema.json +14 -7
- package/examples/post_request_with_query_params/single/exampleAPI.ts +8 -6
- package/examples/post_request_with_query_params/single/k6-script.sample.ts +11 -2
- package/examples/post_request_with_query_params/split/exampleAPI.schemas.ts +5 -5
- package/examples/post_request_with_query_params/split/exampleAPI.ts +4 -2
- package/examples/post_request_with_query_params/split/k6-script.sample.ts +11 -2
- package/examples/post_request_with_query_params/tags/default.ts +4 -2
- package/examples/post_request_with_query_params/tags/exampleAPI.schemas.ts +5 -5
- package/examples/post_request_with_query_params/tags/k6-script.sample.ts +12 -3
- package/examples/query_params_schema/schema.json +20 -10
- package/examples/query_params_schema/single/exampleAPI.ts +5 -3
- package/examples/query_params_schema/single/k6-script.sample.ts +8 -2
- package/examples/query_params_schema/split/exampleAPI.schemas.ts +3 -3
- package/examples/query_params_schema/split/exampleAPI.ts +3 -1
- package/examples/query_params_schema/split/k6-script.sample.ts +8 -2
- package/examples/query_params_schema/tags/default.ts +3 -1
- package/examples/query_params_schema/tags/exampleAPI.schemas.ts +3 -3
- package/examples/query_params_schema/tags/k6-script.sample.ts +9 -3
- package/examples/simple_post_request_schema/schema.json +30 -15
- package/examples/simple_post_request_schema/single/exampleAPI.ts +14 -12
- package/examples/simple_post_request_schema/single/k6-script.sample.ts +16 -2
- package/examples/simple_post_request_schema/split/exampleAPI.schemas.ts +11 -11
- package/examples/simple_post_request_schema/split/exampleAPI.ts +4 -2
- package/examples/simple_post_request_schema/split/k6-script.sample.ts +16 -2
- package/examples/simple_post_request_schema/tags/default.ts +4 -2
- package/examples/simple_post_request_schema/tags/exampleAPI.schemas.ts +11 -11
- package/examples/simple_post_request_schema/tags/k6-script.sample.ts +17 -3
- package/package.json +5 -2
- package/src/constants.ts +7 -3
- package/src/generator/index.ts +20 -7
- package/src/generator/k6Client.ts +6 -73
- package/src/generator/k6ScriptBuilder.ts +328 -0
- package/src/helper.ts +17 -0
- package/tests/e2e/schema.json +135 -18
- package/tests/e2e/single/k6Script.ts +54 -1
- package/tests/functional-tests/helper.ts +16 -0
- package/tests/functional-tests/test-generator/fixtures/basic_parameter_in_ref.json +59 -0
- package/tests/functional-tests/{fixtures/schemas → test-generator/fixtures}/form_data_schema.json +1 -1
- package/tests/functional-tests/{fixtures/schemas → test-generator/fixtures}/form_url_encoded_data_schema.json +1 -1
- package/tests/functional-tests/{fixtures/schemas → test-generator/fixtures}/headers_schema.json +2 -2
- package/tests/functional-tests/{fixtures/schemas → test-generator/fixtures}/simple_post_request_schema.json +1 -1
- package/tests/functional-tests/test-generator/generator.test.ts +154 -0
- package/tests/functional-tests/test-sample-k6-scripts/fixtures/schema_using_ref_models.json +394 -0
- package/tests/functional-tests/test-sample-k6-scripts/fixtures/schema_with_examples.json +416 -0
- package/tests/functional-tests/test-sample-k6-scripts/fixtures/schema_with_no_variables.json +32 -0
- package/tests/functional-tests/test-sample-k6-scripts/sampleK6Scripts.test.ts +248 -0
- package/tests/functional-tests/test-tags-filtering/tagsFiltering.test.ts +166 -0
- package/{vite.config.js → vite.config.mjs} +4 -0
- package/tests/functional-tests/generator.test.ts +0 -319
- /package/tests/functional-tests/{fixtures/schemas → test-generator/fixtures}/basic_schema.json +0 -0
- /package/tests/functional-tests/{fixtures/schemas → test-generator/fixtures}/form_url_encoded_data_with_query_params_schema.json +0 -0
- /package/tests/functional-tests/{fixtures/schemas → test-generator/fixtures}/get_request_with_path_parameters_schema.json +0 -0
- /package/tests/functional-tests/{fixtures/schemas → test-generator/fixtures}/no_title_schema.json +0 -0
- /package/tests/functional-tests/{fixtures/schemas → test-generator/fixtures}/post_request_with_query_params.json +0 -0
- /package/tests/functional-tests/{fixtures/schemas → test-generator/fixtures}/query_params_schema.json +0 -0
- /package/tests/functional-tests/{fixtures → test-tags-filtering/fixtures}/tags_filtering.json +0 -0
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Following are some of the configuration options supported by the tool.
|
|
|
63
63
|
2. `--only-tags`: Filter the generated client to only include routes with specific tags from your OpenAPI schema. Multiple tags can be specified to include routes matching any of those tags. Routes without tags will be excluded. This is useful for generating focused clients that only contain the endpoints you need.
|
|
64
64
|
e.g. `openapi-to-k6 <path-to-openapi-schema> <output path> --only-tags ItemsHeader` will generate a client with only the routes that have the `ItemsHeader` tag. Multiple tags can be specified by using multiple `--only-tags` flags or by separating them with spaces.
|
|
65
65
|
3. `--disable-analytics`: Disable anonymous usage analytics reporting which helping making the tool better. You can also set an environment variable `DISABLE_ANALYTICS=true` to disable the analytics.
|
|
66
|
-
4. `--include-sample-script`: Generate a sample k6 script.
|
|
66
|
+
4. `--include-sample-script`: Generate a sample k6 script. The generated sample script uses the examples defined in the OpenAPI schema requests to make the script usable out of the box. If the examples are not defined, it will use Faker to generate random data.
|
|
67
67
|
5. `--verbose` or `-v` : Enable verbose logging to see more detailed logging output.
|
|
68
68
|
6. `--help` or `-h` : Show help message.
|
|
69
69
|
|
|
@@ -132,4 +132,8 @@ k6 run --compatibility-mode=experimental_enhanced ./K6Script.ts
|
|
|
132
132
|
2. Install the compiled package locally by using `npm install .` or `npm install -g .`.
|
|
133
133
|
3. Use the CLI `k6-sdkgen <path-to-openapi-schema> <output path>`
|
|
134
134
|
|
|
135
|
+
## Releasing
|
|
136
|
+
|
|
137
|
+
To release a new version of the tool, create a new release on GitHub with the new version number as tag (e.g. `0.1.0` ) and the release notes. After the release is created, the GitHub actions will automatically package the tool and publish it to npm.
|
|
138
|
+
|
|
135
139
|
Special mention for the the open-source library [Orval](https://orval.dev/) which is used for the generation of the TypeScript client.
|
package/dist/constants.js
CHANGED
|
@@ -4,17 +4,21 @@ exports.Mode = exports.K6_SCRIPT_TEMPLATE = exports.SAMPLE_K6_SCRIPT_FILE_NAME =
|
|
|
4
4
|
exports.DEFAULT_SCHEMA_TITLE = 'K6Client';
|
|
5
5
|
exports.SAMPLE_K6_SCRIPT_FILE_NAME = 'k6-script.sample.ts';
|
|
6
6
|
exports.K6_SCRIPT_TEMPLATE = `
|
|
7
|
-
|
|
7
|
+
{{{importStatements}}}
|
|
8
8
|
|
|
9
9
|
const baseUrl = '<BASE_URL>';
|
|
10
|
-
|
|
10
|
+
{{{clientInitializationStatement}}}
|
|
11
|
+
|
|
11
12
|
|
|
12
13
|
export default function () {
|
|
14
|
+
{{{this.variableDefinition}}}
|
|
15
|
+
|
|
13
16
|
{{#each clientFunctionsList}}
|
|
14
17
|
/**
|
|
15
18
|
* {{this.summary}}
|
|
16
19
|
*/
|
|
17
|
-
|
|
20
|
+
{{{this.exampleValues}}}
|
|
21
|
+
const {{this.operationName}}ResponseData = {{clientObjectName}}.{{this.operationName}}({{this.requiredParametersString}});
|
|
18
22
|
|
|
19
23
|
{{/each}}
|
|
20
24
|
}
|
package/dist/generator/index.js
CHANGED
|
@@ -31,7 +31,7 @@ const generatedFileHeaderGenerator = (info) => {
|
|
|
31
31
|
...(info.version ? [`Service version: ${info.version}`] : []),
|
|
32
32
|
];
|
|
33
33
|
};
|
|
34
|
-
const afterAllFilesWriteHandler = (filePaths) => __awaiter(void 0, void 0, void 0, function* () {
|
|
34
|
+
const afterAllFilesWriteHandler = (filePaths, outputOverrider) => __awaiter(void 0, void 0, void 0, function* () {
|
|
35
35
|
const removeSingleFile = (filePath) => {
|
|
36
36
|
try {
|
|
37
37
|
fs_1.default.unlinkSync(filePath);
|
|
@@ -54,7 +54,14 @@ const afterAllFilesWriteHandler = (filePaths) => __awaiter(void 0, void 0, void
|
|
|
54
54
|
removeSingleFile(filePath);
|
|
55
55
|
continue;
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
try {
|
|
58
|
+
yield (0, helper_1.formatFileWithPrettier)(filePath);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
yield outputOverrider.temporarilyWriteToStdoutAndStderr(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
62
|
+
logger_1.logger.error(`Error in formatting file ${filePath}: ${error}`);
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
58
65
|
const fileName = path_1.default.basename(filePath);
|
|
59
66
|
if (fileName === '.ts') {
|
|
60
67
|
// Generated SDK had no name because there was no title in the schema
|
|
@@ -102,7 +109,7 @@ exports.default = (_a) => __awaiter(void 0, [_a], void 0, function* ({ openApiPa
|
|
|
102
109
|
},
|
|
103
110
|
hooks: {
|
|
104
111
|
afterAllFilesWrite: (filePaths) => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
-
const filteredFilePaths = yield afterAllFilesWriteHandler(filePaths);
|
|
112
|
+
const filteredFilePaths = yield afterAllFilesWriteHandler(filePaths, outputOverrider);
|
|
106
113
|
generatedFilePaths.push(...filteredFilePaths);
|
|
107
114
|
}),
|
|
108
115
|
},
|
|
@@ -110,8 +117,8 @@ exports.default = (_a) => __awaiter(void 0, [_a], void 0, function* ({ openApiPa
|
|
|
110
117
|
}));
|
|
111
118
|
if (generatedFilePaths.length === 0) {
|
|
112
119
|
const tagsMessage = (tags === null || tags === void 0 ? void 0 : tags.length) && tags.length > 0
|
|
113
|
-
? ` Applied tag filter(s): ${tags.join(', ')}`
|
|
114
|
-
: '';
|
|
115
|
-
throw new errors_1.NoFilesGeneratedError(`No files were generated.${tagsMessage}
|
|
120
|
+
? ` Applied tag filter(s): ${tags.join(', ')}. `
|
|
121
|
+
: ' ';
|
|
122
|
+
throw new errors_1.NoFilesGeneratedError(`No files were generated.${tagsMessage}Try running with --verbose flag to get more details.`);
|
|
116
123
|
}
|
|
117
124
|
});
|
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
-
};
|
|
14
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateTitle = void 0;
|
|
15
4
|
exports.getK6ClientBuilder = getK6ClientBuilder;
|
|
16
5
|
const core_1 = require("@orval/core");
|
|
17
|
-
const handlebars_1 = __importDefault(require("handlebars"));
|
|
18
|
-
const path_1 = __importDefault(require("path"));
|
|
19
6
|
const constants_1 = require("../constants");
|
|
20
|
-
const
|
|
21
|
-
const logger_1 = require("../logger");
|
|
7
|
+
const k6ScriptBuilder_1 = require("./k6ScriptBuilder");
|
|
22
8
|
/**
|
|
23
9
|
* In case the supplied schema does not have a title set, it will set the default title to ensure
|
|
24
10
|
* proper client generation
|
|
@@ -84,6 +70,7 @@ const _getRequestParamsValue = ({ response, queryParams, headers, body, }) => {
|
|
|
84
70
|
// Expand the headers
|
|
85
71
|
if (body.contentType || headers) {
|
|
86
72
|
let headersValue = `\n headers: {`;
|
|
73
|
+
headersValue += '\n...mergedRequestParameters?.headers,';
|
|
87
74
|
if (body.contentType) {
|
|
88
75
|
if (body.formData) {
|
|
89
76
|
headersValue += `\n'Content-Type': '${body.contentType}; boundary=' + formData.boundary,`;
|
|
@@ -96,7 +83,7 @@ const _getRequestParamsValue = ({ response, queryParams, headers, body, }) => {
|
|
|
96
83
|
headersValue += `\n// In the schema, headers can be of any type like number but k6 accepts only strings as headers, hence converting all headers to string`;
|
|
97
84
|
headersValue += `\n...Object.fromEntries(Object.entries(headers || {}).map(([key, value]) => [key, String(value)])),`;
|
|
98
85
|
}
|
|
99
|
-
headersValue += `\n
|
|
86
|
+
headersValue += `\n},`;
|
|
100
87
|
value += headersValue;
|
|
101
88
|
}
|
|
102
89
|
return `{${value}}`;
|
|
@@ -209,6 +196,7 @@ const generateTitle = (title) => {
|
|
|
209
196
|
const sanTitle = (0, core_1.sanitize)(title || constants_1.DEFAULT_SCHEMA_TITLE);
|
|
210
197
|
return `${(0, core_1.pascal)(sanTitle)}Client`;
|
|
211
198
|
};
|
|
199
|
+
exports.generateTitle = generateTitle;
|
|
212
200
|
const generateK6Header = ({ title }) => {
|
|
213
201
|
return `
|
|
214
202
|
/**
|
|
@@ -223,6 +211,7 @@ const generateK6Header = ({ title }) => {
|
|
|
223
211
|
commonRequestParameters?: Params
|
|
224
212
|
}) {
|
|
225
213
|
this.cleanBaseUrl = clientOptions.baseUrl.replace(/\\/+$/, '');\n
|
|
214
|
+
this.commonRequestParameters = clientOptions.commonRequestParameters || {};
|
|
226
215
|
}\n
|
|
227
216
|
`;
|
|
228
217
|
};
|
|
@@ -237,43 +226,6 @@ const generateFooter = () => {
|
|
|
237
226
|
`;
|
|
238
227
|
return footer;
|
|
239
228
|
};
|
|
240
|
-
const k6ScriptBuilder = (verbOptions, output, context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
241
|
-
var _a;
|
|
242
|
-
const schemaTitle = ((_a = context.specs[context.specKey]) === null || _a === void 0 ? void 0 : _a.info.title) || constants_1.DEFAULT_SCHEMA_TITLE;
|
|
243
|
-
const { path: pathOfGeneratedClient, filename, extension, } = yield (0, helper_1.getGeneratedClientPath)(output.target, schemaTitle);
|
|
244
|
-
const directoryPath = (0, helper_1.getDirectoryForPath)(pathOfGeneratedClient);
|
|
245
|
-
const generateScriptPath = path_1.default.join(directoryPath, constants_1.SAMPLE_K6_SCRIPT_FILE_NAME);
|
|
246
|
-
logger_1.logger.debug(`k6ScriptBuilder ~ Generating sample K6 Script\n${JSON.stringify({
|
|
247
|
-
pathOfGeneratedClient,
|
|
248
|
-
filename,
|
|
249
|
-
extension,
|
|
250
|
-
schemaTitle,
|
|
251
|
-
directoryPath,
|
|
252
|
-
generateScriptPath,
|
|
253
|
-
}, null, 2)}`);
|
|
254
|
-
const clientFunctionsList = [];
|
|
255
|
-
for (const verbOption of Object.values(verbOptions)) {
|
|
256
|
-
const { operationName, summary, props } = verbOption;
|
|
257
|
-
const requiredProps = props.filter((prop) => prop.required);
|
|
258
|
-
clientFunctionsList.push({
|
|
259
|
-
operationName,
|
|
260
|
-
summary,
|
|
261
|
-
requiredParametersString: (0, core_1.toObjectString)(requiredProps, 'name'),
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
const scriptContentData = {
|
|
265
|
-
clientFunctionName: generateTitle(schemaTitle),
|
|
266
|
-
clientPath: `./${filename}${extension}`,
|
|
267
|
-
clientFunctionsList,
|
|
268
|
-
};
|
|
269
|
-
const template = handlebars_1.default.compile(constants_1.K6_SCRIPT_TEMPLATE);
|
|
270
|
-
return [
|
|
271
|
-
{
|
|
272
|
-
path: generateScriptPath,
|
|
273
|
-
content: template(scriptContentData),
|
|
274
|
-
},
|
|
275
|
-
];
|
|
276
|
-
});
|
|
277
229
|
function getK6Client(analyticsData) {
|
|
278
230
|
return function (verbOptions, options) {
|
|
279
231
|
_setDefaultSchemaTitle(options.context);
|
|
@@ -294,7 +246,7 @@ function getK6ClientBuilder(shouldGenerateSampleK6Script, analyticsData) {
|
|
|
294
246
|
header: generateK6Header,
|
|
295
247
|
dependencies: getK6Dependencies,
|
|
296
248
|
footer: generateFooter,
|
|
297
|
-
title: generateTitle,
|
|
298
|
-
extraFiles: shouldGenerateSampleK6Script ? k6ScriptBuilder : undefined,
|
|
249
|
+
title: exports.generateTitle,
|
|
250
|
+
extraFiles: shouldGenerateSampleK6Script ? k6ScriptBuilder_1.k6ScriptBuilder : undefined,
|
|
299
251
|
};
|
|
300
252
|
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.k6ScriptBuilder = void 0;
|
|
16
|
+
const faker_1 = require("@faker-js/faker");
|
|
17
|
+
const core_1 = require("@orval/core");
|
|
18
|
+
const handlebars_1 = __importDefault(require("handlebars"));
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
const constants_1 = require("../constants");
|
|
21
|
+
const helper_1 = require("../helper");
|
|
22
|
+
const logger_1 = require("../logger");
|
|
23
|
+
const k6Client_1 = require("./k6Client");
|
|
24
|
+
function getExampleValueForSchema(schema, context) {
|
|
25
|
+
// Handle $ref
|
|
26
|
+
if ('$ref' in schema) {
|
|
27
|
+
const { schema: resolvedSchema } = (0, core_1.resolveRef)(schema, context);
|
|
28
|
+
return getExampleValueForSchema(resolvedSchema, context);
|
|
29
|
+
}
|
|
30
|
+
if ('example' in schema) {
|
|
31
|
+
return `'${schema.example}'`;
|
|
32
|
+
}
|
|
33
|
+
let schemaType = schema.type;
|
|
34
|
+
if (Array.isArray(schemaType)) {
|
|
35
|
+
schemaType = schemaType[0];
|
|
36
|
+
}
|
|
37
|
+
if (!schemaType) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const enumValues = schema.enum;
|
|
41
|
+
switch (schemaType) {
|
|
42
|
+
case 'string':
|
|
43
|
+
return enumValues ? `'${enumValues[0]}'` : `'${faker_1.faker.word.sample()}'`;
|
|
44
|
+
case 'number':
|
|
45
|
+
return enumValues ? enumValues[0] : faker_1.faker.number.int();
|
|
46
|
+
case 'integer':
|
|
47
|
+
return enumValues ? enumValues[0] : faker_1.faker.number.int();
|
|
48
|
+
case 'boolean':
|
|
49
|
+
return enumValues ? enumValues[0] : faker_1.faker.datatype.boolean();
|
|
50
|
+
case 'array':
|
|
51
|
+
return '[]';
|
|
52
|
+
case 'object': {
|
|
53
|
+
let objectString = '{\n';
|
|
54
|
+
for (const property in schema.properties) {
|
|
55
|
+
if (schema.properties[property]) {
|
|
56
|
+
const propertyValue = getExampleValueForSchema(schema.properties[property], context);
|
|
57
|
+
objectString += `${property}: ${propertyValue},\n`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
objectString += '\n}';
|
|
61
|
+
return objectString;
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function getExampleValues(requiredProps, originalOperation, context) {
|
|
68
|
+
var _a;
|
|
69
|
+
let exampleValues = '';
|
|
70
|
+
for (const prop of requiredProps) {
|
|
71
|
+
const propType = prop.type;
|
|
72
|
+
switch (propType) {
|
|
73
|
+
case core_1.GetterPropType.QUERY_PARAM: {
|
|
74
|
+
let exampleValue = '{\n';
|
|
75
|
+
for (const param of originalOperation.parameters || []) {
|
|
76
|
+
let resolvedParam;
|
|
77
|
+
if ('$ref' in param) {
|
|
78
|
+
const { schema: resolvedSchema } = (0, core_1.resolveRef)(param, context);
|
|
79
|
+
resolvedParam = resolvedSchema;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
resolvedParam = param;
|
|
83
|
+
}
|
|
84
|
+
// Only add required query parameters to the example values
|
|
85
|
+
if (resolvedParam.required && resolvedParam.in === 'query') {
|
|
86
|
+
if ('schema' in resolvedParam && resolvedParam.schema) {
|
|
87
|
+
exampleValue += `'${resolvedParam.name}': ${getExampleValueForSchema(resolvedParam.schema, context)},\n`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exampleValue += '\n}';
|
|
92
|
+
exampleValues += `params = ${exampleValue};\n`;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case core_1.GetterPropType.HEADER: {
|
|
96
|
+
let exampleValue = '{\n';
|
|
97
|
+
for (const param of originalOperation.parameters || []) {
|
|
98
|
+
let resolvedParam;
|
|
99
|
+
if ('$ref' in param) {
|
|
100
|
+
const { schema: resolvedSchema } = (0, core_1.resolveRef)(param, context);
|
|
101
|
+
resolvedParam = resolvedSchema;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
resolvedParam = param;
|
|
105
|
+
}
|
|
106
|
+
// Only add required query parameters to the example values
|
|
107
|
+
if (resolvedParam.required && resolvedParam.in === 'header') {
|
|
108
|
+
if ('schema' in resolvedParam && resolvedParam.schema) {
|
|
109
|
+
exampleValue += `'${resolvedParam.name}': ${getExampleValueForSchema(resolvedParam.schema, context)},\n`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exampleValue += '\n}';
|
|
114
|
+
exampleValues += `headers = ${exampleValue};\n`;
|
|
115
|
+
break;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case core_1.GetterPropType.PARAM: {
|
|
119
|
+
let example, paramSchema;
|
|
120
|
+
for (const parameter of originalOperation.parameters || []) {
|
|
121
|
+
if ('name' in parameter) {
|
|
122
|
+
paramSchema = parameter.schema;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
else if ('$ref' in parameter) {
|
|
126
|
+
const { schema: resolvedSchema } = (0, core_1.resolveRef)(parameter, context);
|
|
127
|
+
paramSchema = resolvedSchema.schema;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (paramSchema) {
|
|
132
|
+
example = getExampleValueForSchema(paramSchema, context);
|
|
133
|
+
}
|
|
134
|
+
if (example) {
|
|
135
|
+
exampleValues += `${prop.name} = ${example};\n`;
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case core_1.GetterPropType.BODY: {
|
|
140
|
+
// Generate example value from body schema
|
|
141
|
+
const requestBody = originalOperation.requestBody;
|
|
142
|
+
let requestBodyExample;
|
|
143
|
+
if (!requestBody) {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
let resolvedSchema;
|
|
147
|
+
if ('$ref' in requestBody) {
|
|
148
|
+
const { schema } = (0, core_1.resolveRef)(requestBody, context);
|
|
149
|
+
resolvedSchema = schema;
|
|
150
|
+
}
|
|
151
|
+
else if ('content' in requestBody) {
|
|
152
|
+
resolvedSchema = requestBody;
|
|
153
|
+
}
|
|
154
|
+
if (resolvedSchema && 'content' in resolvedSchema) {
|
|
155
|
+
// Get the first available content type
|
|
156
|
+
const contentType = Object.keys(resolvedSchema.content)[0];
|
|
157
|
+
if (contentType) {
|
|
158
|
+
const requestBodySchema = (_a = resolvedSchema.content[contentType]) === null || _a === void 0 ? void 0 : _a.schema;
|
|
159
|
+
if (requestBodySchema) {
|
|
160
|
+
requestBodyExample = getExampleValueForSchema(requestBodySchema, context);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (requestBodyExample) {
|
|
165
|
+
exampleValues += `${prop.name} = ${requestBodyExample};\n`;
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return exampleValues;
|
|
172
|
+
}
|
|
173
|
+
function getClientClassName(identifier) {
|
|
174
|
+
return (0, k6Client_1.generateTitle)((0, core_1.pascal)(identifier));
|
|
175
|
+
}
|
|
176
|
+
function getClientObjectName(identifier) {
|
|
177
|
+
return (0, core_1.camel)((0, k6Client_1.generateTitle)((0, core_1.pascal)(identifier)));
|
|
178
|
+
}
|
|
179
|
+
const k6ScriptBuilder = (verbOptions, output, context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
|
+
var _a;
|
|
181
|
+
const schemaTitle = ((_a = context.specs[context.specKey]) === null || _a === void 0 ? void 0 : _a.info.title) || constants_1.DEFAULT_SCHEMA_TITLE;
|
|
182
|
+
const { path: pathOfGeneratedClient, filename, extension, } = yield (0, helper_1.getGeneratedClientPath)(output.target, schemaTitle);
|
|
183
|
+
const directoryPath = (0, helper_1.getDirectoryForPath)(pathOfGeneratedClient);
|
|
184
|
+
const generateScriptPath = path_1.default.join(directoryPath, constants_1.SAMPLE_K6_SCRIPT_FILE_NAME);
|
|
185
|
+
logger_1.logger.debug(`k6ScriptBuilder ~ Generating sample K6 Script\n${JSON.stringify({
|
|
186
|
+
pathOfGeneratedClient,
|
|
187
|
+
filename,
|
|
188
|
+
extension,
|
|
189
|
+
schemaTitle,
|
|
190
|
+
directoryPath,
|
|
191
|
+
generateScriptPath,
|
|
192
|
+
}, null, 2)}`);
|
|
193
|
+
const clientFunctionsList = [];
|
|
194
|
+
const uniqueVariables = new Set(); // Track unique variable names
|
|
195
|
+
const allUniqueTags = new Set();
|
|
196
|
+
for (const verbOption of Object.values(verbOptions)) {
|
|
197
|
+
if (verbOption.tags && verbOption.tags.length > 0) {
|
|
198
|
+
verbOption.tags.forEach((tag) => allUniqueTags.add(tag));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
allUniqueTags.add('default');
|
|
202
|
+
}
|
|
203
|
+
let clientObjectName;
|
|
204
|
+
if (output.mode === 'tags') {
|
|
205
|
+
clientObjectName = getClientObjectName(verbOption.tags[0] || 'default');
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
clientObjectName = getClientObjectName(schemaTitle);
|
|
209
|
+
}
|
|
210
|
+
const { operationName, summary, props, originalOperation } = verbOption;
|
|
211
|
+
const requiredProps = props.filter((prop) => prop.required);
|
|
212
|
+
// Create example values object
|
|
213
|
+
const exampleValues = getExampleValues(requiredProps, originalOperation, context);
|
|
214
|
+
for (const prop of requiredProps) {
|
|
215
|
+
uniqueVariables.add(prop.name);
|
|
216
|
+
}
|
|
217
|
+
clientFunctionsList.push({
|
|
218
|
+
operationName,
|
|
219
|
+
summary,
|
|
220
|
+
exampleValues,
|
|
221
|
+
requiredParametersString: (0, core_1.toObjectString)(requiredProps, 'name'),
|
|
222
|
+
clientObjectName,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
let importStatements = '';
|
|
226
|
+
let clientInitializationStatement = '';
|
|
227
|
+
if (output.mode === 'tags') {
|
|
228
|
+
for (const tag of allUniqueTags) {
|
|
229
|
+
const { extension } = yield (0, helper_1.getGeneratedClientPath)(output.target, schemaTitle);
|
|
230
|
+
const clientName = getClientClassName(tag);
|
|
231
|
+
importStatements += `import { ${clientName} } from './${(0, core_1.kebab)(tag)}${extension}';\n`;
|
|
232
|
+
clientInitializationStatement += `const ${getClientObjectName(tag)} = new ${clientName}({ baseUrl });\n`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const clientName = getClientClassName(schemaTitle);
|
|
237
|
+
importStatements = `import { ${clientName} } from './${filename}${extension}';\n`;
|
|
238
|
+
clientInitializationStatement = `const ${getClientObjectName(schemaTitle)} = new ${clientName}({ baseUrl });\n`;
|
|
239
|
+
}
|
|
240
|
+
const scriptContentData = {
|
|
241
|
+
clientFunctionsList,
|
|
242
|
+
variableDefinition: uniqueVariables.size > 0
|
|
243
|
+
? `let ${Array.from(uniqueVariables).join(', ')};`
|
|
244
|
+
: '',
|
|
245
|
+
importStatements,
|
|
246
|
+
clientInitializationStatement,
|
|
247
|
+
};
|
|
248
|
+
const template = handlebars_1.default.compile(constants_1.K6_SCRIPT_TEMPLATE);
|
|
249
|
+
return [
|
|
250
|
+
{
|
|
251
|
+
path: generateScriptPath,
|
|
252
|
+
content: template(scriptContentData),
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
});
|
|
256
|
+
exports.k6ScriptBuilder = k6ScriptBuilder;
|
package/dist/helper.js
CHANGED
|
@@ -123,6 +123,22 @@ class OutputOverrider {
|
|
|
123
123
|
process.stdout.write = this.originalStdoutWrite;
|
|
124
124
|
process.stderr.write = this.originalStderrWrite;
|
|
125
125
|
}
|
|
126
|
+
// Method to temporarily write to stdout and stderr
|
|
127
|
+
temporarilyWriteToStdoutAndStderr(callback) {
|
|
128
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
129
|
+
const currentStdoutWrite = process.stdout.write;
|
|
130
|
+
const currentStderrWrite = process.stderr.write;
|
|
131
|
+
process.stdout.write = this.originalStdoutWrite;
|
|
132
|
+
process.stderr.write = this.originalStderrWrite;
|
|
133
|
+
try {
|
|
134
|
+
yield callback();
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
process.stdout.write = currentStdoutWrite;
|
|
138
|
+
process.stderr.write = currentStderrWrite;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
126
142
|
}
|
|
127
143
|
exports.OutputOverrider = OutputOverrider;
|
|
128
144
|
OutputOverrider.instance = null;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { SimpleAPIClient } from './simpleAPI.ts'
|
|
2
2
|
|
|
3
3
|
const baseUrl = '<BASE_URL>'
|
|
4
|
-
const
|
|
4
|
+
const simpleAPIClient = new SimpleAPIClient({ baseUrl })
|
|
5
5
|
|
|
6
6
|
export default function () {
|
|
7
7
|
/**
|
|
8
8
|
* Retrieve example data
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
const getExampleResponseData = simpleAPIClient.getExample()
|
|
11
12
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Automatically generated by @grafana/openapi-to-k6: 0.
|
|
2
|
+
* Automatically generated by @grafana/openapi-to-k6: 0.3.1
|
|
3
3
|
* Do not edit manually.
|
|
4
4
|
* Simple API
|
|
5
5
|
* Service version: 1.0.0
|
|
@@ -8,6 +8,7 @@ import { URL } from 'https://jslib.k6.io/url/1.0.0/index.js'
|
|
|
8
8
|
import http from 'k6/http'
|
|
9
9
|
import type { Params, Response } from 'k6/http'
|
|
10
10
|
export type GetExample200 = {
|
|
11
|
+
/** @pattern ^([a-zA-Z_][a-zA-Z0-9_-]*:)?([a-zA-Z_][a-zA-Z0-9_-]*\/)?([a-zA-Z_][.a-zA-Z0-9_-]*)$ */
|
|
11
12
|
message?: string
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -23,6 +24,8 @@ export class SimpleAPIClient {
|
|
|
23
24
|
commonRequestParameters?: Params
|
|
24
25
|
}) {
|
|
25
26
|
this.cleanBaseUrl = clientOptions.baseUrl.replace(/\/+$/, '')
|
|
27
|
+
|
|
28
|
+
this.commonRequestParameters = clientOptions.commonRequestParameters || {}
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { SimpleAPIClient } from './simpleAPI.ts'
|
|
2
2
|
|
|
3
3
|
const baseUrl = '<BASE_URL>'
|
|
4
|
-
const
|
|
4
|
+
const simpleAPIClient = new SimpleAPIClient({ baseUrl })
|
|
5
5
|
|
|
6
6
|
export default function () {
|
|
7
7
|
/**
|
|
8
8
|
* Retrieve example data
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
const getExampleResponseData = simpleAPIClient.getExample()
|
|
11
12
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Automatically generated by @grafana/openapi-to-k6: 0.
|
|
2
|
+
* Automatically generated by @grafana/openapi-to-k6: 0.3.1
|
|
3
3
|
* Do not edit manually.
|
|
4
4
|
* Simple API
|
|
5
5
|
* Service version: 1.0.0
|
|
6
6
|
*/
|
|
7
7
|
export type GetExample200 = {
|
|
8
|
+
/** @pattern ^([a-zA-Z_][a-zA-Z0-9_-]*:)?([a-zA-Z_][a-zA-Z0-9_-]*\/)?([a-zA-Z_][.a-zA-Z0-9_-]*)$ */
|
|
8
9
|
message?: string
|
|
9
10
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Automatically generated by @grafana/openapi-to-k6: 0.
|
|
2
|
+
* Automatically generated by @grafana/openapi-to-k6: 0.3.1
|
|
3
3
|
* Do not edit manually.
|
|
4
4
|
* Simple API
|
|
5
5
|
* Service version: 1.0.0
|
|
@@ -21,6 +21,8 @@ export class SimpleAPIClient {
|
|
|
21
21
|
commonRequestParameters?: Params
|
|
22
22
|
}) {
|
|
23
23
|
this.cleanBaseUrl = clientOptions.baseUrl.replace(/\/+$/, '')
|
|
24
|
+
|
|
25
|
+
this.commonRequestParameters = clientOptions.commonRequestParameters || {}
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Automatically generated by @grafana/openapi-to-k6: 0.
|
|
2
|
+
* Automatically generated by @grafana/openapi-to-k6: 0.3.1
|
|
3
3
|
* Do not edit manually.
|
|
4
4
|
* Simple API
|
|
5
5
|
* Service version: 1.0.0
|
|
@@ -21,6 +21,8 @@ export class DefaultClient {
|
|
|
21
21
|
commonRequestParameters?: Params
|
|
22
22
|
}) {
|
|
23
23
|
this.cleanBaseUrl = clientOptions.baseUrl.replace(/\/+$/, '')
|
|
24
|
+
|
|
25
|
+
this.commonRequestParameters = clientOptions.commonRequestParameters || {}
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DefaultClient } from './default.ts'
|
|
2
2
|
|
|
3
3
|
const baseUrl = '<BASE_URL>'
|
|
4
|
-
const
|
|
4
|
+
const defaultClient = new DefaultClient({ baseUrl })
|
|
5
5
|
|
|
6
6
|
export default function () {
|
|
7
7
|
/**
|
|
8
8
|
* Retrieve example data
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
const getExampleResponseData = defaultClient.getExample()
|
|
11
12
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Automatically generated by @grafana/openapi-to-k6: 0.
|
|
2
|
+
* Automatically generated by @grafana/openapi-to-k6: 0.3.1
|
|
3
3
|
* Do not edit manually.
|
|
4
4
|
* Simple API
|
|
5
5
|
* Service version: 1.0.0
|
|
6
6
|
*/
|
|
7
7
|
export type GetExample200 = {
|
|
8
|
+
/** @pattern ^([a-zA-Z_][a-zA-Z0-9_-]*:)?([a-zA-Z_][a-zA-Z0-9_-]*\/)?([a-zA-Z_][.a-zA-Z0-9_-]*)$ */
|
|
8
9
|
message?: string
|
|
9
10
|
}
|
|
@@ -19,15 +19,18 @@
|
|
|
19
19
|
"file": {
|
|
20
20
|
"type": "string",
|
|
21
21
|
"format": "binary",
|
|
22
|
-
"description": "File to upload"
|
|
22
|
+
"description": "File to upload",
|
|
23
|
+
"example": "example.pdf"
|
|
23
24
|
},
|
|
24
25
|
"description": {
|
|
25
26
|
"type": "string",
|
|
26
|
-
"description": "Description of the file"
|
|
27
|
+
"description": "Description of the file",
|
|
28
|
+
"example": "Monthly report document"
|
|
27
29
|
},
|
|
28
30
|
"userId": {
|
|
29
31
|
"type": "string",
|
|
30
|
-
"description": "User ID associated with the upload"
|
|
32
|
+
"description": "User ID associated with the upload",
|
|
33
|
+
"example": "user123"
|
|
31
34
|
}
|
|
32
35
|
},
|
|
33
36
|
"required": [
|