@grafana/openapi-to-k6 0.2.5 → 0.2.6
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/.github/workflows/tests.yaml +1 -0
- package/README.md +6 -4
- package/dist/cli.js +27 -12
- package/dist/errors.js +10 -0
- package/dist/generator/index.js +50 -3
- package/dist/helper.js +32 -0
- package/examples/basic_schema/single/simpleAPI.ts +1 -1
- package/examples/basic_schema/split/simpleAPI.schemas.ts +1 -1
- package/examples/basic_schema/split/simpleAPI.ts +1 -1
- package/examples/basic_schema/tags/default.ts +1 -1
- package/examples/basic_schema/tags/simpleAPI.schemas.ts +1 -1
- package/examples/form_data_schema/single/formDataAPI.ts +1 -1
- package/examples/form_data_schema/split/formDataAPI.schemas.ts +1 -1
- package/examples/form_data_schema/split/formDataAPI.ts +1 -1
- package/examples/form_data_schema/tags/default.ts +1 -1
- package/examples/form_data_schema/tags/formDataAPI.schemas.ts +1 -1
- package/examples/form_url_encoded_data_schema/single/formURLEncodedAPI.ts +1 -1
- package/examples/form_url_encoded_data_schema/split/formURLEncodedAPI.schemas.ts +1 -1
- package/examples/form_url_encoded_data_schema/split/formURLEncodedAPI.ts +1 -1
- package/examples/form_url_encoded_data_schema/tags/default.ts +1 -1
- package/examples/form_url_encoded_data_schema/tags/formURLEncodedAPI.schemas.ts +1 -1
- package/examples/form_url_encoded_data_with_query_params_schema/single/formURLEncodedAPIWithQueryParameters.ts +1 -1
- package/examples/form_url_encoded_data_with_query_params_schema/split/formURLEncodedAPIWithQueryParameters.schemas.ts +1 -1
- package/examples/form_url_encoded_data_with_query_params_schema/split/formURLEncodedAPIWithQueryParameters.ts +1 -1
- package/examples/form_url_encoded_data_with_query_params_schema/tags/default.ts +1 -1
- package/examples/form_url_encoded_data_with_query_params_schema/tags/formURLEncodedAPIWithQueryParameters.schemas.ts +1 -1
- package/examples/get_request_with_path_parameters_schema/single/simpleAPI.ts +1 -1
- package/examples/get_request_with_path_parameters_schema/split/simpleAPI.schemas.ts +1 -1
- package/examples/get_request_with_path_parameters_schema/split/simpleAPI.ts +1 -1
- package/examples/get_request_with_path_parameters_schema/tags/default.ts +1 -1
- package/examples/get_request_with_path_parameters_schema/tags/simpleAPI.schemas.ts +1 -1
- package/examples/headers_schema/single/headerDemoAPI.ts +1 -1
- package/examples/headers_schema/split/headerDemoAPI.schemas.ts +1 -1
- package/examples/headers_schema/split/headerDemoAPI.ts +1 -1
- package/examples/headers_schema/tags/default.ts +1 -1
- package/examples/headers_schema/tags/headerDemoAPI.schemas.ts +1 -1
- package/examples/no_title_schema/single/k6Client.ts +1 -1
- package/examples/no_title_schema/split/k6Client.schemas.ts +1 -1
- package/examples/no_title_schema/split/k6Client.ts +1 -1
- package/examples/no_title_schema/tags/default.ts +1 -1
- package/examples/no_title_schema/tags/k6Client.schemas.ts +1 -1
- package/examples/post_request_with_query_params/single/exampleAPI.ts +1 -1
- package/examples/post_request_with_query_params/split/exampleAPI.schemas.ts +1 -1
- package/examples/post_request_with_query_params/split/exampleAPI.ts +1 -1
- package/examples/post_request_with_query_params/tags/default.ts +1 -1
- package/examples/post_request_with_query_params/tags/exampleAPI.schemas.ts +1 -1
- package/examples/query_params_schema/single/exampleAPI.ts +1 -1
- package/examples/query_params_schema/split/exampleAPI.schemas.ts +1 -1
- package/examples/query_params_schema/split/exampleAPI.ts +1 -1
- package/examples/query_params_schema/tags/default.ts +1 -1
- package/examples/query_params_schema/tags/exampleAPI.schemas.ts +1 -1
- package/examples/simple_post_request_schema/single/exampleAPI.ts +1 -1
- package/examples/simple_post_request_schema/split/exampleAPI.schemas.ts +1 -1
- package/examples/simple_post_request_schema/split/exampleAPI.ts +1 -1
- package/examples/simple_post_request_schema/tags/default.ts +1 -1
- package/examples/simple_post_request_schema/tags/exampleAPI.schemas.ts +1 -1
- package/package.json +1 -1
- package/src/cli.ts +32 -14
- package/src/errors.ts +6 -0
- package/src/generator/index.ts +67 -2
- package/src/helper.ts +40 -0
- package/src/type.d.ts +1 -0
- package/tests/e2e/schema.json +1 -0
- package/tests/e2e/single-tag-filter/k6Script.ts +50 -0
- package/tests/e2e/tags/k6Script.ts +3 -3
- package/tests/functional-tests/fixtures/tags_filtering.json +141 -0
- package/tests/functional-tests/generator.test.ts +159 -3
- package/tests/helper.test.ts +59 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import chalk from 'chalk'
|
|
|
4
4
|
import { Command, InvalidArgumentError } from 'commander'
|
|
5
5
|
import { generateDefaultAnalyticsData, reportUsageAnalytics } from './analytics'
|
|
6
6
|
import { Mode } from './constants'
|
|
7
|
+
import { NoFilesGeneratedError } from './errors'
|
|
7
8
|
import generateK6SDK from './generator'
|
|
8
9
|
import { getPackageDetails } from './helper'
|
|
9
10
|
import { logger } from './logger'
|
|
@@ -33,10 +34,21 @@ async function generateSDK({
|
|
|
33
34
|
shouldGenerateSampleK6Script,
|
|
34
35
|
analyticsData,
|
|
35
36
|
mode,
|
|
37
|
+
tags,
|
|
36
38
|
}: GenerateK6SDKOptions) {
|
|
37
|
-
logger.logMessage(
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
logger.logMessage(
|
|
40
|
+
'Generating TypeScript client for k6...\n' +
|
|
41
|
+
'OpenAPI schema: ' +
|
|
42
|
+
chalk.cyan(openApiPath) +
|
|
43
|
+
'\n' +
|
|
44
|
+
'Output: ' +
|
|
45
|
+
chalk.cyan(outputDir) +
|
|
46
|
+
'\n' +
|
|
47
|
+
(tags?.length
|
|
48
|
+
? 'Filtering by tag(s): ' + chalk.cyan(tags.join(', '))
|
|
49
|
+
: '') +
|
|
50
|
+
'\n'
|
|
51
|
+
)
|
|
40
52
|
|
|
41
53
|
await generateK6SDK({
|
|
42
54
|
openApiPath,
|
|
@@ -44,16 +56,13 @@ async function generateSDK({
|
|
|
44
56
|
shouldGenerateSampleK6Script,
|
|
45
57
|
analyticsData,
|
|
46
58
|
mode,
|
|
59
|
+
tags,
|
|
47
60
|
})
|
|
48
61
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
} else {
|
|
55
|
-
logger.logMessage(`TypeScript client generated successfully.`, chalk.green)
|
|
56
|
-
}
|
|
62
|
+
const message = shouldGenerateSampleK6Script
|
|
63
|
+
? 'TypeScript client and sample k6 script generated successfully.'
|
|
64
|
+
: 'TypeScript client generated successfully.'
|
|
65
|
+
logger.logMessage(message, chalk.green)
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
program
|
|
@@ -68,6 +77,10 @@ program
|
|
|
68
77
|
validateMode,
|
|
69
78
|
Mode.SINGLE
|
|
70
79
|
)
|
|
80
|
+
.option(
|
|
81
|
+
'--only-tags <filters...>',
|
|
82
|
+
'list of tags to filter on. Generated client will only include operations with these tags'
|
|
83
|
+
)
|
|
71
84
|
.option('-v, --verbose', 'enable verbose mode to show debug logs')
|
|
72
85
|
.option('--include-sample-script', 'generate a sample k6 script')
|
|
73
86
|
.option('--disable-analytics', 'disable anonymous usage data collection')
|
|
@@ -78,6 +91,7 @@ program
|
|
|
78
91
|
options: {
|
|
79
92
|
verbose?: boolean
|
|
80
93
|
mode: Mode
|
|
94
|
+
onlyTags?: (string | RegExp)[]
|
|
81
95
|
disableAnalytics?: boolean
|
|
82
96
|
includeSampleScript?: boolean
|
|
83
97
|
}
|
|
@@ -112,16 +126,20 @@ program
|
|
|
112
126
|
shouldGenerateSampleK6Script: !!options.includeSampleScript,
|
|
113
127
|
analyticsData,
|
|
114
128
|
mode: options.mode,
|
|
129
|
+
tags: options.onlyTags,
|
|
115
130
|
})
|
|
116
131
|
} catch (error) {
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
if (error instanceof NoFilesGeneratedError) {
|
|
133
|
+
logger.logMessage(error.message, chalk.yellow)
|
|
134
|
+
} else {
|
|
135
|
+
logger.error('Failed to generate SDK:')
|
|
136
|
+
console.error(error)
|
|
137
|
+
}
|
|
119
138
|
}
|
|
120
139
|
|
|
121
140
|
if (!shouldDisableAnalytics && analyticsData) {
|
|
122
141
|
logger.debug('Reporting following usage analytics data:')
|
|
123
142
|
logger.debug(JSON.stringify(analyticsData, null, 2))
|
|
124
|
-
|
|
125
143
|
await reportUsageAnalytics(analyticsData)
|
|
126
144
|
}
|
|
127
145
|
}
|
package/src/errors.ts
ADDED
package/src/generator/index.ts
CHANGED
|
@@ -3,9 +3,11 @@ import { InfoObject } from 'openapi3-ts/oas30'
|
|
|
3
3
|
import orval from 'orval'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import { DEFAULT_SCHEMA_TITLE } from '../constants'
|
|
6
|
+
import { NoFilesGeneratedError } from '../errors'
|
|
6
7
|
import {
|
|
7
8
|
formatFileWithPrettier,
|
|
8
9
|
getPackageDetails,
|
|
10
|
+
hasOnlyComments,
|
|
9
11
|
OutputOverrider,
|
|
10
12
|
} from '../helper'
|
|
11
13
|
import { logger } from '../logger'
|
|
@@ -26,7 +28,32 @@ const generatedFileHeaderGenerator = (info: InfoObject) => {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const afterAllFilesWriteHandler = async (filePaths: string[]) => {
|
|
31
|
+
const removeSingleFile = (filePath: string) => {
|
|
32
|
+
try {
|
|
33
|
+
fs.unlinkSync(filePath)
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// This is non-critical, so we just log it
|
|
36
|
+
logger.debug(
|
|
37
|
+
`afterAllFilesWriteHandler ~ Error deleting file ${filePath}: ${error}`
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const emptyFileList: string[] = []
|
|
42
|
+
|
|
29
43
|
for (const filePath of filePaths) {
|
|
44
|
+
// There is a bug in the orval library which generates empty files when tags filter is applied
|
|
45
|
+
// and no matching endpoints are found and used mode is split or single.
|
|
46
|
+
// It generates the file with only the header comment.
|
|
47
|
+
// Hence, we manually remove those empty files.
|
|
48
|
+
// Issue link - https://github.com/orval-labs/orval/issues/1691
|
|
49
|
+
|
|
50
|
+
if (hasOnlyComments(fs.readFileSync(filePath, 'utf-8'))) {
|
|
51
|
+
emptyFileList.push(filePath)
|
|
52
|
+
// Delete the file
|
|
53
|
+
removeSingleFile(filePath)
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
await formatFileWithPrettier(filePath)
|
|
31
58
|
|
|
32
59
|
const fileName = path.basename(filePath)
|
|
@@ -42,6 +69,28 @@ const afterAllFilesWriteHandler = async (filePaths: string[]) => {
|
|
|
42
69
|
fs.renameSync(filePath, newPath)
|
|
43
70
|
}
|
|
44
71
|
}
|
|
72
|
+
|
|
73
|
+
if (emptyFileList.length > 0) {
|
|
74
|
+
logger.debug(
|
|
75
|
+
`afterAllFilesWriteHandler ~ The following files were empty and removed: ${emptyFileList.join(
|
|
76
|
+
', '
|
|
77
|
+
)}`
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Return the list of generated file paths excluding the empty files
|
|
82
|
+
const filteredFilePaths = filePaths.filter(
|
|
83
|
+
(filePath) => !emptyFileList.includes(filePath)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// Check if all the filtered file path end with `.schemas.ts`
|
|
87
|
+
if (filteredFilePaths.every((filePath) => filePath.endsWith('.schemas.ts'))) {
|
|
88
|
+
// If yes we should remove them as only schemas files is not needed
|
|
89
|
+
filteredFilePaths.map(removeSingleFile)
|
|
90
|
+
return []
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return filteredFilePaths
|
|
45
94
|
}
|
|
46
95
|
|
|
47
96
|
export default async ({
|
|
@@ -50,15 +99,20 @@ export default async ({
|
|
|
50
99
|
shouldGenerateSampleK6Script,
|
|
51
100
|
analyticsData,
|
|
52
101
|
mode,
|
|
102
|
+
tags,
|
|
53
103
|
}: GenerateK6SDKOptions) => {
|
|
54
104
|
/**
|
|
55
105
|
* Note!
|
|
56
106
|
* 1. override.requestOptions is not supported for the custom K6 client
|
|
57
107
|
* 2. override.mutator is not supported for the custom K6 client
|
|
58
108
|
*/
|
|
109
|
+
const generatedFilePaths: string[] = []
|
|
59
110
|
await outputOverrider.redirectOutputToNullStream(async () => {
|
|
60
111
|
await orval({
|
|
61
|
-
input:
|
|
112
|
+
input: {
|
|
113
|
+
target: openApiPath,
|
|
114
|
+
filters: { tags: tags && tags.length > 0 ? tags : undefined },
|
|
115
|
+
},
|
|
62
116
|
output: {
|
|
63
117
|
target: outputDir,
|
|
64
118
|
mode: mode,
|
|
@@ -70,8 +124,19 @@ export default async ({
|
|
|
70
124
|
headers: true,
|
|
71
125
|
},
|
|
72
126
|
hooks: {
|
|
73
|
-
afterAllFilesWrite:
|
|
127
|
+
afterAllFilesWrite: async (filePaths: string[]) => {
|
|
128
|
+
const filteredFilePaths = await afterAllFilesWriteHandler(filePaths)
|
|
129
|
+
generatedFilePaths.push(...filteredFilePaths)
|
|
130
|
+
},
|
|
74
131
|
},
|
|
75
132
|
})
|
|
76
133
|
})
|
|
134
|
+
|
|
135
|
+
if (generatedFilePaths.length === 0) {
|
|
136
|
+
const tagsMessage =
|
|
137
|
+
tags?.length && tags.length > 0
|
|
138
|
+
? ` Applied tag filter(s): ${tags.join(', ')}`
|
|
139
|
+
: ''
|
|
140
|
+
throw new NoFilesGeneratedError(`No files were generated.${tagsMessage}`)
|
|
141
|
+
}
|
|
77
142
|
}
|
package/src/helper.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { camel, getFileInfo } from '@orval/core'
|
|
2
2
|
import fs from 'fs'
|
|
3
|
+
import { createSourceFile, ScriptTarget } from 'typescript'
|
|
4
|
+
|
|
3
5
|
import path from 'path'
|
|
4
6
|
import { format, resolveConfig } from 'prettier'
|
|
5
7
|
import packageJson from '../package.json'
|
|
@@ -158,3 +160,41 @@ export function getDirectoryForPath(pathString: string): string {
|
|
|
158
160
|
// If the path has an extension, it is a file
|
|
159
161
|
return path.dirname(pathString)
|
|
160
162
|
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Checks if a file contains only comments and whitespace, with no actual code.
|
|
166
|
+
*
|
|
167
|
+
* This function uses TypeScript's parser to accurately detect and remove all types
|
|
168
|
+
* of comments (single-line, multi-line, JSDoc) and whitespace from the input text.
|
|
169
|
+
* If nothing remains after removing comments and whitespace, then the file is
|
|
170
|
+
* considered to contain only comments.
|
|
171
|
+
*
|
|
172
|
+
* @param fileContent - The string content of the file to check
|
|
173
|
+
* @returns True if the file contains only comments and whitespace, false if it contains any actual code
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* // Returns true
|
|
177
|
+
* hasOnlyComments('// Just a comment');
|
|
178
|
+
*
|
|
179
|
+
* // Returns false
|
|
180
|
+
* hasOnlyComments('// A comment\nconst x = 1;');
|
|
181
|
+
*/
|
|
182
|
+
export function hasOnlyComments(fileContent: string): boolean {
|
|
183
|
+
// Create a source file
|
|
184
|
+
const sourceFile = createSourceFile(
|
|
185
|
+
'temp.ts',
|
|
186
|
+
fileContent,
|
|
187
|
+
ScriptTarget.Latest,
|
|
188
|
+
true
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
// Remove all comments and whitespace
|
|
192
|
+
const textWithoutComments = sourceFile
|
|
193
|
+
.getFullText()
|
|
194
|
+
.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '') // Remove comments
|
|
195
|
+
.replace(/\s+/g, '') // Remove whitespace
|
|
196
|
+
|
|
197
|
+
// If there's any content left after removing comments and whitespace,
|
|
198
|
+
// then there's actual code
|
|
199
|
+
return textWithoutComments.length === 0
|
|
200
|
+
}
|
package/src/type.d.ts
CHANGED
package/tests/e2e/schema.json
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* eslint-disable import/no-unresolved */
|
|
2
|
+
import { check } from 'k6'
|
|
3
|
+
import { Counter } from 'k6/metrics'
|
|
4
|
+
import { ComprehensiveAPIClient } from './sdk.ts'
|
|
5
|
+
/* eslint-enable import/no-unresolved */
|
|
6
|
+
|
|
7
|
+
const CounterErrors = new Counter('Errors')
|
|
8
|
+
const CounterSuccess = new Counter('Success')
|
|
9
|
+
const baseUrl = 'http://localhost:3000'
|
|
10
|
+
const client = new ComprehensiveAPIClient({ baseUrl })
|
|
11
|
+
|
|
12
|
+
export const options = {
|
|
13
|
+
thresholds: {
|
|
14
|
+
Errors: ['count>=1'],
|
|
15
|
+
Success: ['count>=1'],
|
|
16
|
+
// the rate of successful checks should be higher than 90%
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function checkResponseStatus(response, expectedStatus) {
|
|
21
|
+
const result = check(response, {
|
|
22
|
+
[`status is ${expectedStatus}`]: (r) => r.status === expectedStatus,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (!result) {
|
|
26
|
+
console.error(
|
|
27
|
+
`Check failed! Expected status ${expectedStatus} but got ${response.status}. Following is the response:`
|
|
28
|
+
)
|
|
29
|
+
console.log(JSON.stringify(response, null, 2))
|
|
30
|
+
} else {
|
|
31
|
+
CounterSuccess.add(1)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function () {
|
|
36
|
+
try {
|
|
37
|
+
const getResponseData = client.getItemsId('1')
|
|
38
|
+
checkResponseStatus(getResponseData.response, 200)
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof TypeError) {
|
|
41
|
+
// Add a k6 check to verify if the error is thrown
|
|
42
|
+
CounterErrors.add(1)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const getItemsHeaderResponseData = client.getItemsHeader({
|
|
47
|
+
id: 'test',
|
|
48
|
+
})
|
|
49
|
+
checkResponseStatus(getItemsHeaderResponseData.response, 200)
|
|
50
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable import/no-unresolved */
|
|
2
2
|
import { check } from 'k6'
|
|
3
|
-
import { DefaultClient } from './default.ts'
|
|
4
3
|
import { ItemsFormClient } from './items-form.ts'
|
|
4
|
+
import { ItemsHeaderClient } from './items-header.ts'
|
|
5
5
|
import { ItemsClient } from './items.ts'
|
|
6
6
|
|
|
7
7
|
/* eslint-enable import/no-unresolved */
|
|
@@ -9,7 +9,7 @@ import { ItemsClient } from './items.ts'
|
|
|
9
9
|
const baseUrl = 'http://localhost:3000'
|
|
10
10
|
const itemsClient = new ItemsClient({ baseUrl })
|
|
11
11
|
const itemFormClient = new ItemsFormClient({ baseUrl })
|
|
12
|
-
const
|
|
12
|
+
const itemsHeaderClient = new ItemsHeaderClient({ baseUrl })
|
|
13
13
|
|
|
14
14
|
export const options = {
|
|
15
15
|
thresholds: {
|
|
@@ -98,7 +98,7 @@ export default function () {
|
|
|
98
98
|
// Items form client call end
|
|
99
99
|
|
|
100
100
|
// Default client call start
|
|
101
|
-
const getItemsHeaderResponseData =
|
|
101
|
+
const getItemsHeaderResponseData = itemsHeaderClient.getItemsHeader({
|
|
102
102
|
id: 'test',
|
|
103
103
|
})
|
|
104
104
|
checkResponseStatus(getItemsHeaderResponseData.response, 200)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.0",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Sample API",
|
|
5
|
+
"version": "1.0.0"
|
|
6
|
+
},
|
|
7
|
+
"paths": {
|
|
8
|
+
"/users": {
|
|
9
|
+
"get": {
|
|
10
|
+
"tags": ["users"],
|
|
11
|
+
"responses": {
|
|
12
|
+
"200": {
|
|
13
|
+
"description": "Success"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"/user-profiles": {
|
|
19
|
+
"get": {
|
|
20
|
+
"tags": ["userProfiles"],
|
|
21
|
+
"responses": {
|
|
22
|
+
"200": {
|
|
23
|
+
"description": "Success"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"/pets": {
|
|
29
|
+
"get": {
|
|
30
|
+
"tags": ["pets"],
|
|
31
|
+
"responses": {
|
|
32
|
+
"200": {
|
|
33
|
+
"description": "Success"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"/auth": {
|
|
39
|
+
"post": {
|
|
40
|
+
"tags": ["auth"],
|
|
41
|
+
"requestBody": {
|
|
42
|
+
"content": {
|
|
43
|
+
"application/json": {
|
|
44
|
+
"schema": {
|
|
45
|
+
"$ref": "#/components/schemas/AuthRequest"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"responses": {
|
|
51
|
+
"200": {
|
|
52
|
+
"description": "Success"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"components": {
|
|
59
|
+
"schemas": {
|
|
60
|
+
"AuthRequest": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"username": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"example": "user123"
|
|
66
|
+
},
|
|
67
|
+
"password": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"format": "password",
|
|
70
|
+
"example": "mypassword"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"required": ["username", "password"]
|
|
74
|
+
},
|
|
75
|
+
"User": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"properties": {
|
|
78
|
+
"id": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"example": "abc123"
|
|
81
|
+
},
|
|
82
|
+
"name": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"example": "John Doe"
|
|
85
|
+
},
|
|
86
|
+
"email": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"format": "email",
|
|
89
|
+
"example": "john.doe@example.com"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"required": ["id", "name", "email"]
|
|
93
|
+
},
|
|
94
|
+
"UserProfile": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"properties": {
|
|
97
|
+
"id": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"example": "profile123"
|
|
100
|
+
},
|
|
101
|
+
"userId": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"example": "abc123"
|
|
104
|
+
},
|
|
105
|
+
"bio": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"example": "Software developer with 10 years of experience"
|
|
108
|
+
},
|
|
109
|
+
"avatarUrl": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"format": "uri",
|
|
112
|
+
"example": "https://example.com/avatar.jpg"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"required": ["id", "userId", "bio"]
|
|
116
|
+
},
|
|
117
|
+
"Pet": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"properties": {
|
|
120
|
+
"id": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"example": "pet123"
|
|
123
|
+
},
|
|
124
|
+
"name": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"example": "Buddy"
|
|
127
|
+
},
|
|
128
|
+
"species": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"example": "Dog"
|
|
131
|
+
},
|
|
132
|
+
"age": {
|
|
133
|
+
"type": "integer",
|
|
134
|
+
"example": 3
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"required": ["id", "name", "species"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|