@redocly/cli 1.0.0 → 1.0.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/commands/build-docs/index.js +2 -4
  3. package/lib/commands/build-docs/utils.d.ts +1 -1
  4. package/lib/commands/build-docs/utils.js +3 -3
  5. package/package.json +2 -2
  6. package/src/__mocks__/@redocly/openapi-core.ts +80 -0
  7. package/src/__mocks__/documents.ts +63 -0
  8. package/src/__mocks__/fs.ts +6 -0
  9. package/src/__mocks__/perf_hooks.ts +3 -0
  10. package/src/__mocks__/redoc.ts +2 -0
  11. package/src/__mocks__/utils.ts +19 -0
  12. package/src/__tests__/commands/build-docs.test.ts +62 -0
  13. package/src/__tests__/commands/bundle.test.ts +150 -0
  14. package/src/__tests__/commands/join.test.ts +122 -0
  15. package/src/__tests__/commands/lint.test.ts +190 -0
  16. package/src/__tests__/commands/push-region.test.ts +58 -0
  17. package/src/__tests__/commands/push.test.ts +492 -0
  18. package/src/__tests__/fetch-with-timeout.test.ts +35 -0
  19. package/src/__tests__/fixtures/config.ts +21 -0
  20. package/src/__tests__/fixtures/openapi.json +0 -0
  21. package/src/__tests__/fixtures/openapi.yaml +0 -0
  22. package/src/__tests__/fixtures/redocly.yaml +0 -0
  23. package/src/__tests__/utils.test.ts +564 -0
  24. package/src/__tests__/wrapper.test.ts +57 -0
  25. package/src/assert-node-version.ts +8 -0
  26. package/src/commands/build-docs/index.ts +50 -0
  27. package/src/commands/build-docs/template.hbs +23 -0
  28. package/src/commands/build-docs/types.ts +24 -0
  29. package/src/commands/build-docs/utils.ts +110 -0
  30. package/src/commands/bundle.ts +177 -0
  31. package/src/commands/join.ts +811 -0
  32. package/src/commands/lint.ts +151 -0
  33. package/src/commands/login.ts +27 -0
  34. package/src/commands/preview-docs/index.ts +190 -0
  35. package/src/commands/preview-docs/preview-server/default.hbs +24 -0
  36. package/src/commands/preview-docs/preview-server/hot.js +42 -0
  37. package/src/commands/preview-docs/preview-server/oauth2-redirect.html +21 -0
  38. package/src/commands/preview-docs/preview-server/preview-server.ts +156 -0
  39. package/src/commands/preview-docs/preview-server/server.ts +91 -0
  40. package/src/commands/push.ts +441 -0
  41. package/src/commands/split/__tests__/fixtures/samples.json +61 -0
  42. package/src/commands/split/__tests__/fixtures/spec.json +70 -0
  43. package/src/commands/split/__tests__/fixtures/webhooks.json +85 -0
  44. package/src/commands/split/__tests__/index.test.ts +137 -0
  45. package/src/commands/split/index.ts +385 -0
  46. package/src/commands/split/types.ts +85 -0
  47. package/src/commands/stats.ts +119 -0
  48. package/src/custom.d.ts +1 -0
  49. package/src/fetch-with-timeout.ts +21 -0
  50. package/src/index.ts +484 -0
  51. package/src/js-utils.ts +17 -0
  52. package/src/types.ts +40 -0
  53. package/src/update-version-notifier.ts +106 -0
  54. package/src/utils.ts +590 -0
  55. package/src/wrapper.ts +42 -0
  56. package/tsconfig.json +9 -0
  57. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,441 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import fetch from 'node-fetch';
4
+ import { performance } from 'perf_hooks';
5
+ import { yellow, green, blue, red } from 'colorette';
6
+ import { createHash } from 'crypto';
7
+ import {
8
+ bundle,
9
+ Config,
10
+ RedoclyClient,
11
+ IGNORE_FILE,
12
+ BundleOutputFormat,
13
+ getTotals,
14
+ slash,
15
+ Region,
16
+ getMergedConfig,
17
+ } from '@redocly/openapi-core';
18
+ import {
19
+ exitWithError,
20
+ printExecutionTime,
21
+ getFallbackApisOrExit,
22
+ pluralize,
23
+ dumpBundle,
24
+ } from '../utils';
25
+ import { promptClientToken } from './login';
26
+
27
+ const DEFAULT_VERSION = 'latest';
28
+
29
+ export const DESTINATION_REGEX =
30
+ /^(@(?<organizationId>[\w\-\s]+)\/)?(?<name>[^@]*)@(?<version>[\w\.\-]+)$/;
31
+
32
+ export type PushOptions = {
33
+ api?: string;
34
+ destination?: string;
35
+ branchName?: string;
36
+ upsert?: boolean;
37
+ 'job-id'?: string;
38
+ 'batch-size'?: number;
39
+ region?: Region;
40
+ 'skip-decorator'?: string[];
41
+ public?: boolean;
42
+ files?: string[];
43
+ organization?: string;
44
+ config?: string;
45
+ };
46
+
47
+ export async function handlePush(argv: PushOptions, config: Config): Promise<void> {
48
+ const client = new RedoclyClient(config.region);
49
+ const isAuthorized = await client.isAuthorizedWithRedoclyByRegion();
50
+ if (!isAuthorized) {
51
+ const clientToken = await promptClientToken(client.domain);
52
+ await client.login(clientToken);
53
+ }
54
+
55
+ const startedAt = performance.now();
56
+ const { destination, branchName, upsert } = argv;
57
+
58
+ const jobId = argv['job-id'];
59
+ const batchSize = argv['batch-size'];
60
+
61
+ if (destination && !DESTINATION_REGEX.test(destination)) {
62
+ exitWithError(
63
+ `Destination argument value is not valid, please use the right format: ${yellow(
64
+ '<api-name@api-version>'
65
+ )}`
66
+ );
67
+ }
68
+
69
+ const destinationProps = getDestinationProps(destination, config.organization);
70
+
71
+ const organizationId = argv.organization || destinationProps.organizationId;
72
+ const { name, version } = destinationProps;
73
+
74
+ if (!organizationId) {
75
+ return exitWithError(
76
+ `No organization provided, please use --organization option or specify the 'organization' field in the config file.`
77
+ );
78
+ }
79
+
80
+ const api = argv.api || (name && version && getApiRoot({ name, version, config }));
81
+
82
+ if (name && version && !api) {
83
+ exitWithError(
84
+ `No api found that matches ${blue(
85
+ `${name}@${version}`
86
+ )}. Please make sure you have provided the correct data in the config file.`
87
+ );
88
+ }
89
+
90
+ // Ensure that a destination for the api is provided.
91
+ if (!name && api) {
92
+ return exitWithError(
93
+ `No destination provided, please use --destination option to provide destination.`
94
+ );
95
+ }
96
+
97
+ if (jobId && !jobId.trim()) {
98
+ exitWithError(
99
+ `The ${blue(`job-id`)} option value is not valid, please avoid using an empty string.`
100
+ );
101
+ }
102
+
103
+ if (batchSize && batchSize < 2) {
104
+ exitWithError(
105
+ `The ${blue(`batch-size`)} option value is not valid, please use the integer bigger than 1.`
106
+ );
107
+ }
108
+
109
+ const apis = api ? { [`${name}@${version}`]: { root: api } } : config.apis;
110
+ if (!Object.keys(apis).length) {
111
+ exitWithError(
112
+ `Api not found. Please make sure you have provided the correct data in the config file.`
113
+ );
114
+ }
115
+
116
+ for (const [apiNameAndVersion, { root: api }] of Object.entries(apis)) {
117
+ const resolvedConfig = getMergedConfig(config, apiNameAndVersion);
118
+ resolvedConfig.styleguide.skipDecorators(argv['skip-decorator']);
119
+
120
+ const [name, version = DEFAULT_VERSION] = apiNameAndVersion.split('@');
121
+ const encodedName = encodeURIComponent(name);
122
+ try {
123
+ let rootFilePath = '';
124
+ const filePaths: string[] = [];
125
+ const filesToUpload = await collectFilesToUpload(api, resolvedConfig);
126
+ const filesHash = hashFiles(filesToUpload.files);
127
+
128
+ process.stdout.write(
129
+ `Uploading ${filesToUpload.files.length} ${pluralize(
130
+ 'file',
131
+ filesToUpload.files.length
132
+ )}:\n`
133
+ );
134
+
135
+ let uploaded = 0;
136
+
137
+ for (const file of filesToUpload.files) {
138
+ const { signedUploadUrl, filePath } = await client.registryApi.prepareFileUpload({
139
+ organizationId,
140
+ name: encodedName,
141
+ version,
142
+ filesHash,
143
+ filename: file.keyOnS3,
144
+ isUpsert: upsert,
145
+ });
146
+
147
+ if (file.filePath === filesToUpload.root) {
148
+ rootFilePath = filePath;
149
+ }
150
+
151
+ filePaths.push(filePath);
152
+
153
+ process.stdout.write(
154
+ `Uploading ${file.contents ? 'bundle for ' : ''}${blue(file.filePath)}...`
155
+ );
156
+
157
+ const uploadResponse = await uploadFileToS3(
158
+ signedUploadUrl,
159
+ file.contents || file.filePath
160
+ );
161
+
162
+ const fileCounter = `(${++uploaded}/${filesToUpload.files.length})`;
163
+
164
+ if (!uploadResponse.ok) {
165
+ exitWithError(`✗ ${fileCounter}\nFile upload failed\n`);
166
+ }
167
+
168
+ process.stdout.write(green(`✓ ${fileCounter}\n`));
169
+ }
170
+
171
+ process.stdout.write('\n');
172
+
173
+ await client.registryApi.pushApi({
174
+ organizationId,
175
+ name: encodedName,
176
+ version,
177
+ rootFilePath,
178
+ filePaths,
179
+ branch: branchName,
180
+ isUpsert: upsert,
181
+ isPublic: argv['public'],
182
+ batchId: jobId,
183
+ batchSize: batchSize,
184
+ });
185
+ } catch (error) {
186
+ if (error.message === 'ORGANIZATION_NOT_FOUND') {
187
+ exitWithError(`Organization ${blue(organizationId)} not found`);
188
+ }
189
+
190
+ if (error.message === 'API_VERSION_NOT_FOUND') {
191
+ exitWithError(
192
+ `The definition version ${blue(
193
+ `${name}@${version}`
194
+ )} does not exist in organization ${blue(organizationId)}!\n${yellow(
195
+ 'Suggestion:'
196
+ )} please use ${blue('-u')} or ${blue('--upsert')} to create definition.\n\n`
197
+ );
198
+ }
199
+
200
+ throw error;
201
+ }
202
+
203
+ process.stdout.write(
204
+ `Definition: ${blue(api!)} is successfully pushed to Redocly API Registry \n`
205
+ );
206
+ }
207
+ printExecutionTime('push', startedAt, api || `apis in organization ${organizationId}`);
208
+ }
209
+
210
+ function getFilesList(dir: string, files?: any): string[] {
211
+ files = files || [];
212
+ const filesAndDirs = fs.readdirSync(dir);
213
+ for (const name of filesAndDirs) {
214
+ if (fs.statSync(path.join(dir, name)).isDirectory()) {
215
+ files = getFilesList(path.join(dir, name), files);
216
+ } else {
217
+ const currentPath = dir + '/' + name;
218
+ files.push(currentPath);
219
+ }
220
+ }
221
+ return files;
222
+ }
223
+
224
+ async function collectFilesToUpload(api: string, config: Config) {
225
+ const files: { filePath: string; keyOnS3: string; contents?: Buffer }[] = [];
226
+ const [{ path: apiPath }] = await getFallbackApisOrExit([api], config);
227
+
228
+ process.stdout.write('Bundling definition\n');
229
+
230
+ const { bundle: openapiBundle, problems } = await bundle({
231
+ config,
232
+ ref: apiPath,
233
+ skipRedoclyRegistryRefs: true,
234
+ });
235
+
236
+ const fileTotals = getTotals(problems);
237
+
238
+ if (fileTotals.errors === 0) {
239
+ process.stdout.write(
240
+ `Created a bundle for ${blue(api)} ${fileTotals.warnings > 0 ? 'with warnings' : ''}\n`
241
+ );
242
+ } else {
243
+ exitWithError(`Failed to create a bundle for ${blue(api)}\n`);
244
+ }
245
+
246
+ const fileExt = path.extname(apiPath).split('.').pop();
247
+ files.push(
248
+ getFileEntry(apiPath, dumpBundle(openapiBundle.parsed, fileExt as BundleOutputFormat))
249
+ );
250
+
251
+ if (fs.existsSync('package.json')) {
252
+ files.push(getFileEntry('package.json'));
253
+ }
254
+ if (fs.existsSync(IGNORE_FILE)) {
255
+ files.push(getFileEntry(IGNORE_FILE));
256
+ }
257
+ if (config.configFile) {
258
+ // All config file paths including the root one
259
+ files.push(...[...new Set(config.styleguide.extendPaths)].map((f) => getFileEntry(f)));
260
+ if (config.theme?.openapi?.htmlTemplate) {
261
+ const dir = getFolder(config.theme.openapi.htmlTemplate);
262
+ const fileList = getFilesList(dir, []);
263
+ files.push(...fileList.map((f) => getFileEntry(f)));
264
+ }
265
+ const pluginFiles = new Set<string>();
266
+ for (const plugin of config.styleguide.pluginPaths) {
267
+ if (typeof plugin !== 'string') continue;
268
+ const fileList = getFilesList(getFolder(plugin), []);
269
+ fileList.forEach((f) => pluginFiles.add(f));
270
+ }
271
+ files.push(...filterPluginFilesByExt(Array.from(pluginFiles)).map((f) => getFileEntry(f)));
272
+ }
273
+
274
+ if (config.files) {
275
+ const otherFiles = new Set<string>();
276
+ for (const file of config.files) {
277
+ if (fs.statSync(file).isDirectory()) {
278
+ const fileList = getFilesList(file, []);
279
+ fileList.forEach((f) => otherFiles.add(f));
280
+ } else {
281
+ otherFiles.add(file);
282
+ }
283
+ }
284
+ files.push(...Array.from(otherFiles).map((f) => getFileEntry(f)));
285
+ }
286
+
287
+ return {
288
+ files,
289
+ root: path.resolve(apiPath),
290
+ };
291
+
292
+ function filterPluginFilesByExt(files: string[]) {
293
+ return files.filter((file: string) => {
294
+ const fileExt = path.extname(file).toLowerCase();
295
+ return fileExt === '.js' || fileExt === '.ts' || fileExt === '.mjs' || fileExt === 'json';
296
+ });
297
+ }
298
+
299
+ function getFileEntry(filename: string, contents?: string) {
300
+ return {
301
+ filePath: path.resolve(filename),
302
+ keyOnS3: config.configFile
303
+ ? slash(path.relative(path.dirname(config.configFile), filename))
304
+ : slash(path.basename(filename)),
305
+ contents: (contents && Buffer.from(contents, 'utf-8')) || undefined,
306
+ };
307
+ }
308
+ }
309
+
310
+ function getFolder(filePath: string) {
311
+ return path.resolve(path.dirname(filePath));
312
+ }
313
+
314
+ function hashFiles(filePaths: { filePath: string }[]) {
315
+ const sum = createHash('sha256');
316
+ filePaths.forEach((file) => sum.update(fs.readFileSync(file.filePath)));
317
+ return sum.digest('hex');
318
+ }
319
+
320
+ function parseDestination(
321
+ destination?: string
322
+ ): { organizationId?: string; name?: string; version?: string } | undefined {
323
+ return destination?.match(DESTINATION_REGEX)?.groups;
324
+ }
325
+
326
+ export function getDestinationProps(
327
+ destination: string | undefined,
328
+ organization: string | undefined
329
+ ) {
330
+ const groups = destination && parseDestination(destination);
331
+ if (groups) {
332
+ return {
333
+ organizationId: groups.organizationId || organization,
334
+ name: groups.name,
335
+ version: groups.version,
336
+ };
337
+ } else {
338
+ return { organizationId: organization, name: undefined, version: undefined };
339
+ }
340
+ }
341
+
342
+ type BarePushArgs = Omit<PushOptions, 'destination' | 'branchName'> & {
343
+ maybeDestination?: string;
344
+ maybeBranchName?: string;
345
+ branch?: string;
346
+ destination?: string;
347
+ };
348
+
349
+ export const transformPush =
350
+ (callback: typeof handlePush) =>
351
+ (
352
+ {
353
+ api: maybeApiOrDestination,
354
+ maybeDestination,
355
+ maybeBranchName,
356
+ branch,
357
+ 'batch-id': batchId,
358
+ 'job-id': jobId,
359
+ ...rest
360
+ }: BarePushArgs & { 'batch-id'?: string },
361
+ config: Config
362
+ ) => {
363
+ if (batchId) {
364
+ process.stderr.write(
365
+ yellow(
366
+ `The ${red('batch-id')} option is deprecated. Please use ${green('job-id')} instead.\n\n`
367
+ )
368
+ );
369
+ }
370
+
371
+ if (maybeBranchName) {
372
+ process.stderr.write(
373
+ yellow(
374
+ 'Deprecation warning: Do not use the third parameter as a branch name. Please use a separate --branch option instead.\n\n'
375
+ )
376
+ );
377
+ }
378
+
379
+ let apiFile, destination;
380
+ if (maybeDestination) {
381
+ process.stderr.write(
382
+ yellow(
383
+ 'Deprecation warning: Do not use the second parameter as a destination. Please use a separate --destination and --organization instead.\n\n'
384
+ )
385
+ );
386
+ apiFile = maybeApiOrDestination;
387
+ destination = maybeDestination;
388
+ } else if (maybeApiOrDestination && DESTINATION_REGEX.test(maybeApiOrDestination)) {
389
+ process.stderr.write(
390
+ yellow(
391
+ 'Deprecation warning: Do not use the first parameter as a destination. Please use a separate --destination and --organization options instead.\n\n'
392
+ )
393
+ );
394
+ destination = maybeApiOrDestination;
395
+ } else if (maybeApiOrDestination && !DESTINATION_REGEX.test(maybeApiOrDestination)) {
396
+ apiFile = maybeApiOrDestination;
397
+ }
398
+
399
+ destination = rest.destination || destination;
400
+
401
+ return callback(
402
+ {
403
+ ...rest,
404
+ destination,
405
+ api: apiFile,
406
+ branchName: branch ?? maybeBranchName,
407
+ 'job-id': jobId || batchId,
408
+ },
409
+ config
410
+ );
411
+ };
412
+
413
+ export function getApiRoot({
414
+ name,
415
+ version,
416
+ config: { apis },
417
+ }: {
418
+ name: string;
419
+ version: string;
420
+ config: Config;
421
+ }): string {
422
+ const api = apis?.[`${name}@${version}`] || (version === DEFAULT_VERSION && apis?.[name]);
423
+ return api?.root;
424
+ }
425
+
426
+ function uploadFileToS3(url: string, filePathOrBuffer: string | Buffer) {
427
+ const fileSizeInBytes =
428
+ typeof filePathOrBuffer === 'string'
429
+ ? fs.statSync(filePathOrBuffer).size
430
+ : filePathOrBuffer.byteLength;
431
+ const readStream =
432
+ typeof filePathOrBuffer === 'string' ? fs.createReadStream(filePathOrBuffer) : filePathOrBuffer;
433
+
434
+ return fetch(url, {
435
+ method: 'PUT',
436
+ headers: {
437
+ 'Content-Length': fileSizeInBytes.toString(),
438
+ },
439
+ body: readStream,
440
+ });
441
+ }
@@ -0,0 +1,61 @@
1
+ {
2
+ "openapi": "3.0.1",
3
+ "info": {
4
+ "title": "TEST",
5
+ "description": "TEST",
6
+ "version": "v1",
7
+ "license": {
8
+ "name": "Apache 2.0",
9
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
10
+ }
11
+ },
12
+ "servers": [
13
+ {
14
+ "url": "http://petstore.swagger.io/v1"
15
+ }
16
+ ],
17
+ "components": {
18
+ "schemas": {
19
+ "Test": {
20
+ "nullable": true
21
+ }
22
+ }
23
+ },
24
+ "paths": {
25
+ "/test": {
26
+ "get": {
27
+ "summary": "test",
28
+ "operationId": "test",
29
+ "responses": {
30
+ "202": {
31
+ "description": "Test",
32
+ "content": {
33
+ "application/json": {
34
+ "schema": {
35
+ "$ref": "#/components/schemas/Test"
36
+ }
37
+ }
38
+ }
39
+ },
40
+ "400": {
41
+ "description": "An error response"
42
+ }
43
+ },
44
+ "x-codeSamples": [
45
+ {
46
+ "lang": "C#",
47
+ "source": "PetStore.v1.Pet pet = new PetStore.v1.Pet();"
48
+ },
49
+ {
50
+ "lang": "C/AL",
51
+ "source": "PetStore.v1.Pet pet = new PetStore.v1.Pet();"
52
+ },
53
+ {
54
+ "lang": "Visual Basic",
55
+ "source": "PetStore.v1.Pet pet = new PetStore.v1.Pet();"
56
+ }
57
+ ]
58
+ }
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "openapi": "3.0.1",
3
+ "info": {
4
+ "title": "TEST",
5
+ "description": "TEST",
6
+ "version": "v1",
7
+ "license": {
8
+ "name": "Apache 2.0",
9
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
10
+ }
11
+ },
12
+ "servers": [
13
+ {
14
+ "url": "http://petstore.swagger.io/v1"
15
+ }
16
+ ],
17
+ "components": {
18
+ "schemas": {
19
+ "Test": {
20
+ "nullable": true
21
+ }
22
+ }
23
+ },
24
+ "paths": {
25
+ "/test": {
26
+ "get": {
27
+ "summary": "test",
28
+ "operationId": "test",
29
+ "responses": {
30
+ "202": {
31
+ "description": "Test",
32
+ "content": {
33
+ "application/json": {
34
+ "schema": {
35
+ "$ref": "#/components/schemas/Test"
36
+ },
37
+ "example": {}
38
+ }
39
+ }
40
+ },
41
+ "400": {
42
+ "description": "An error response"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ },
48
+ "x-webhooks": {
49
+ "test": {
50
+ "post": {
51
+ "summary": "New pet",
52
+ "description": "Information about a new pet in the systems",
53
+ "operationId": "newPet",
54
+ "tags": ["pet"],
55
+ "requestBody": {
56
+ "content": {
57
+ "application/json": {
58
+ "schema": { "$ref": "#/components/schemas/Test" }
59
+ }
60
+ }
61
+ },
62
+ "responses": {
63
+ "200": {
64
+ "description": "Return a 200 status to indicate that the data was received successfully"
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "openapi": "3.1.0",
3
+ "info": {
4
+ "title": "Webhook Example",
5
+ "version": "1.0.0"
6
+ },
7
+ "paths": {
8
+ "/pets": {
9
+ "get": {
10
+ "summary": "List all pets",
11
+ "operationId": "listPets",
12
+ "parameters": [
13
+ {
14
+ "name": "limit",
15
+ "in": "query",
16
+ "description": "How many items to return at one time (max 100)",
17
+ "required": false,
18
+ "schema": {
19
+ "type": "integer",
20
+ "format": "int32"
21
+ }
22
+ }
23
+ ],
24
+ "responses": {
25
+ "200": {
26
+ "description": "A paged array of pets",
27
+ "content": {
28
+ "application/json": {
29
+ "schema": {
30
+ "$ref": "#/components/schemas/Pets"
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ },
39
+ "webhooks": {
40
+ "test": {
41
+ "post": {
42
+ "requestBody": {
43
+ "description": "Information about a new pet in the system",
44
+ "content": {
45
+ "application/json": {
46
+ "schema": {
47
+ "$ref": "#/components/schemas/Pet"
48
+ }
49
+ }
50
+ }
51
+ },
52
+ "responses": {
53
+ "200": {
54
+ "description": "Return a 200 status to indicate that the data was received successfully"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ },
60
+ "components": {
61
+ "schemas": {
62
+ "Pet": {
63
+ "required": ["id", "name"],
64
+ "properties": {
65
+ "id": {
66
+ "type": "integer",
67
+ "format": "int64"
68
+ },
69
+ "name": {
70
+ "type": "string"
71
+ },
72
+ "tag": {
73
+ "type": "string"
74
+ }
75
+ }
76
+ },
77
+ "Pets": {
78
+ "type": "array",
79
+ "items": {
80
+ "$ref": "#/components/schemas/Pet"
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }