@grafana/openapi-to-k6 0.1.0
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/.eslintrc.json +31 -0
- package/.github/workflows/publish.yaml +37 -0
- package/.github/workflows/tests.yaml +43 -0
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/.prettierrc +10 -0
- package/LICENSE.md +660 -0
- package/README.md +80 -0
- package/dist/analytics.js +80 -0
- package/dist/cli.js +84 -0
- package/dist/constants.js +21 -0
- package/dist/generator.js +57 -0
- package/dist/helper.js +198 -0
- package/dist/k6SdkClient.js +307 -0
- package/dist/logger.js +50 -0
- package/jest.config.js +15 -0
- package/package.json +56 -0
- package/src/analytics.ts +71 -0
- package/src/cli.ts +96 -0
- package/src/constants.ts +19 -0
- package/src/generator.ts +71 -0
- package/src/helper.ts +210 -0
- package/src/k6SdkClient.ts +434 -0
- package/src/logger.ts +61 -0
- package/src/type.d.ts +34 -0
- package/tests/e2e/k6Script.ts +77 -0
- package/tests/e2e/schema.json +289 -0
- package/tests/functional-tests/fixtures/schemas/basic_schema.json +41 -0
- package/tests/functional-tests/fixtures/schemas/form_data_schema.json +94 -0
- package/tests/functional-tests/fixtures/schemas/form_url_encoded_data_schema.json +93 -0
- package/tests/functional-tests/fixtures/schemas/form_url_encoded_data_with_query_params_schema.json +113 -0
- package/tests/functional-tests/fixtures/schemas/get_request_with_path_parameters_schema.json +66 -0
- package/tests/functional-tests/fixtures/schemas/headers_schema.json +163 -0
- package/tests/functional-tests/fixtures/schemas/no_title_schema.json +40 -0
- package/tests/functional-tests/fixtures/schemas/post_request_with_query_params.json +95 -0
- package/tests/functional-tests/fixtures/schemas/query_params_schema.json +115 -0
- package/tests/functional-tests/fixtures/schemas/simple_post_request_schema.json +132 -0
- package/tests/functional-tests/generator.test.ts +73 -0
- package/tests/helper.test.ts +203 -0
- package/tsconfig.json +17 -0
package/src/helper.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { camel, getFileInfo } from '@orval/core'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { format, resolveConfig } from 'prettier'
|
|
5
|
+
import packageJson from '../package.json'
|
|
6
|
+
import { SAMPLE_K6_SCRIPT_FILE_NAME } from './constants'
|
|
7
|
+
import { logger } from './logger'
|
|
8
|
+
import { PackageDetails } from './type'
|
|
9
|
+
|
|
10
|
+
export const getPackageDetails = (): PackageDetails => {
|
|
11
|
+
const commandName = Object.keys(packageJson.bin)[0] || 'openapi-to-k6'
|
|
12
|
+
return {
|
|
13
|
+
name: packageJson.name,
|
|
14
|
+
commandName,
|
|
15
|
+
description: packageJson.description,
|
|
16
|
+
version: packageJson.version,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format the given file using Prettier.
|
|
22
|
+
*
|
|
23
|
+
* @param filePath - Path to the file to format.
|
|
24
|
+
*/
|
|
25
|
+
export async function formatFileWithPrettier(filePath: string) {
|
|
26
|
+
// Read file contents
|
|
27
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
28
|
+
// Format using Prettier
|
|
29
|
+
const options = await resolveConfig(filePath)
|
|
30
|
+
const formatted = await format(content, {
|
|
31
|
+
...options,
|
|
32
|
+
filepath: filePath,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Write formatted content back to the file
|
|
36
|
+
fs.writeFileSync(filePath, formatted)
|
|
37
|
+
logger.debug(`Formatted: ${filePath}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format the generated files using Prettier.
|
|
42
|
+
*
|
|
43
|
+
* @param outputTarget - Path to the generated files.
|
|
44
|
+
* @param schemaTitle - Title of the schema.
|
|
45
|
+
*/
|
|
46
|
+
export async function formatGeneratedFiles(
|
|
47
|
+
outputTarget: string,
|
|
48
|
+
schemaTitle: string,
|
|
49
|
+
isSampleK6ScriptGenerated: boolean
|
|
50
|
+
) {
|
|
51
|
+
// Here we call the original function from @orval/core used by the library to generate the
|
|
52
|
+
// file name with same defaults.
|
|
53
|
+
const { path: clientPath } = await getGeneratedClientPath(
|
|
54
|
+
outputTarget,
|
|
55
|
+
schemaTitle
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
logger.debug('Following are the details for formatting generated files:')
|
|
59
|
+
logger.debug(`Path: ${path}`)
|
|
60
|
+
logger.debug(`Schema Title: ${schemaTitle}`)
|
|
61
|
+
logger.debug(`Output Target: ${outputTarget}`)
|
|
62
|
+
|
|
63
|
+
await exports.formatFileWithPrettier(clientPath)
|
|
64
|
+
|
|
65
|
+
if (isSampleK6ScriptGenerated) {
|
|
66
|
+
const k6ScriptPath = path.join(
|
|
67
|
+
getDirectoryForPath(clientPath),
|
|
68
|
+
SAMPLE_K6_SCRIPT_FILE_NAME
|
|
69
|
+
)
|
|
70
|
+
logger.debug(`Generated sample K6 Script Path: ${k6ScriptPath}`)
|
|
71
|
+
|
|
72
|
+
if (fs.existsSync(k6ScriptPath)) {
|
|
73
|
+
logger.debug('Formatting sample k6 script file')
|
|
74
|
+
await exports.formatFileWithPrettier(k6ScriptPath)
|
|
75
|
+
} else {
|
|
76
|
+
logger.error(
|
|
77
|
+
'Unable to format sample K6 script file as it does not exist!'
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the path for the generated client file.
|
|
85
|
+
*
|
|
86
|
+
* @param outputTarget - Path to the generated files.
|
|
87
|
+
* @param schemaTitle - Title of the schema.
|
|
88
|
+
*
|
|
89
|
+
* @returns Path to the generated client file.
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
export async function getGeneratedClientPath(
|
|
93
|
+
outputTarget: string,
|
|
94
|
+
schemaTitle: string
|
|
95
|
+
): Promise<{
|
|
96
|
+
path: string
|
|
97
|
+
filename: string
|
|
98
|
+
extension: string
|
|
99
|
+
}> {
|
|
100
|
+
const { path, filename, extension } = getFileInfo(outputTarget, {
|
|
101
|
+
backupFilename: camel(schemaTitle),
|
|
102
|
+
extension: '.ts',
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return { path, filename, extension }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A singleton Class to allow redirecting stdout and stderr to a null stream.
|
|
110
|
+
* This is used to supress the output from third-party libraries.
|
|
111
|
+
*
|
|
112
|
+
* Note: Make sure to call restoreOutput() after the third-party library call to restore the output.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* const outputOverrider = OutputOverrider.getInstance();
|
|
117
|
+
* outputOverrider.redirectOutputToNullStream();
|
|
118
|
+
* // Call the third-party library
|
|
119
|
+
* outputOverrider.restoreOutput();
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @export
|
|
123
|
+
* @class OutputOverrider
|
|
124
|
+
*/
|
|
125
|
+
export class OutputOverrider {
|
|
126
|
+
private static instance: OutputOverrider | null = null
|
|
127
|
+
private originalStdoutWrite: any // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
128
|
+
private originalStderrWrite: any // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
129
|
+
|
|
130
|
+
// Making the constructor private to prevent direct instantiation
|
|
131
|
+
private constructor() {
|
|
132
|
+
this.originalStdoutWrite = process.stdout.write
|
|
133
|
+
this.originalStderrWrite = process.stderr.write
|
|
134
|
+
}
|
|
135
|
+
// Static method to get the single instance of the class
|
|
136
|
+
public static getInstance() {
|
|
137
|
+
if (OutputOverrider.instance === null) {
|
|
138
|
+
OutputOverrider.instance = new OutputOverrider()
|
|
139
|
+
}
|
|
140
|
+
return OutputOverrider.instance
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public async redirectOutputToNullStream(callback?: () => Promise<void>) {
|
|
144
|
+
process.stdout.write = process.stderr.write = () => true
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
if (callback) {
|
|
148
|
+
await callback()
|
|
149
|
+
}
|
|
150
|
+
} finally {
|
|
151
|
+
this._restoreOutput()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private _restoreOutput() {
|
|
156
|
+
process.stdout.write = this.originalStdoutWrite
|
|
157
|
+
process.stderr.write = this.originalStderrWrite
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if the current script is running with ts-node. i.e. directly from source.
|
|
163
|
+
*
|
|
164
|
+
* @export
|
|
165
|
+
* @returns {boolean}
|
|
166
|
+
*/
|
|
167
|
+
export function isTsNode(): boolean {
|
|
168
|
+
const scriptPath = process.argv[1]
|
|
169
|
+
if (scriptPath) {
|
|
170
|
+
return scriptPath.endsWith('.ts') || scriptPath.includes('ts-node')
|
|
171
|
+
} else {
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Create a hash from the given string using the djb2 algorithm.
|
|
178
|
+
*
|
|
179
|
+
* @param str
|
|
180
|
+
* @returns generated hash
|
|
181
|
+
*/
|
|
182
|
+
export function djb2Hash(str: string): number {
|
|
183
|
+
let hash = 5381
|
|
184
|
+
for (let i = 0; i < str.length; i++) {
|
|
185
|
+
hash = (hash * 33) ^ str.charCodeAt(i)
|
|
186
|
+
}
|
|
187
|
+
return hash >>> 0 // Ensure the hash is a positive integer
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Returns the directory for the given path.
|
|
192
|
+
*
|
|
193
|
+
* If the path is a file, the directory of the file is returned.
|
|
194
|
+
* If the path is a directory, the path itself is returned.
|
|
195
|
+
*
|
|
196
|
+
* @param path - The path to get the directory for.
|
|
197
|
+
*
|
|
198
|
+
* @returns The directory for the given path.
|
|
199
|
+
*/
|
|
200
|
+
export function getDirectoryForPath(pathString: string): string {
|
|
201
|
+
const extensionName = path.extname(pathString)
|
|
202
|
+
|
|
203
|
+
if (!extensionName) {
|
|
204
|
+
// If the path does not have an extension, it is a directory
|
|
205
|
+
return pathString
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// If the path has an extension, it is a file
|
|
209
|
+
return path.dirname(pathString)
|
|
210
|
+
}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClientDependenciesBuilder,
|
|
3
|
+
ClientExtraFilesBuilder,
|
|
4
|
+
ClientFooterBuilder,
|
|
5
|
+
ClientGeneratorsBuilder,
|
|
6
|
+
ClientHeaderBuilder,
|
|
7
|
+
ClientTitleBuilder,
|
|
8
|
+
ContextSpecs,
|
|
9
|
+
generateFormDataAndUrlEncodedFunction,
|
|
10
|
+
generateVerbImports,
|
|
11
|
+
GeneratorMutator,
|
|
12
|
+
GeneratorOptions,
|
|
13
|
+
GeneratorSchema,
|
|
14
|
+
GeneratorVerbOptions,
|
|
15
|
+
GetterBody,
|
|
16
|
+
GetterQueryParam,
|
|
17
|
+
GetterResponse,
|
|
18
|
+
ParamsSerializerOptions,
|
|
19
|
+
pascal,
|
|
20
|
+
sanitize,
|
|
21
|
+
toObjectString,
|
|
22
|
+
Verbs,
|
|
23
|
+
} from '@orval/core'
|
|
24
|
+
import Handlebars from 'handlebars'
|
|
25
|
+
import path from 'path'
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_SCHEMA_TITLE,
|
|
28
|
+
K6_SCRIPT_TEMPLATE,
|
|
29
|
+
SAMPLE_K6_SCRIPT_FILE_NAME,
|
|
30
|
+
} from './constants'
|
|
31
|
+
import { getDirectoryForPath, getGeneratedClientPath } from './helper'
|
|
32
|
+
import { AnalyticsData, SchemaDetails } from './type'
|
|
33
|
+
|
|
34
|
+
// A map to store the operationNames for which a return type is to be written at the end to export
|
|
35
|
+
// and the return type definition
|
|
36
|
+
const returnTypesToWrite: Map<string, string> = new Map()
|
|
37
|
+
|
|
38
|
+
export const getK6Dependencies: ClientDependenciesBuilder = () => [
|
|
39
|
+
{
|
|
40
|
+
exports: [
|
|
41
|
+
{
|
|
42
|
+
name: 'http',
|
|
43
|
+
default: true,
|
|
44
|
+
values: true,
|
|
45
|
+
syntheticDefaultImport: true,
|
|
46
|
+
},
|
|
47
|
+
{ name: 'Response' },
|
|
48
|
+
{ name: 'ResponseBody' },
|
|
49
|
+
{ name: 'Params' },
|
|
50
|
+
],
|
|
51
|
+
dependency: 'k6/http',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
exports: [
|
|
55
|
+
{
|
|
56
|
+
name: 'URLSearchParams',
|
|
57
|
+
default: false,
|
|
58
|
+
values: true,
|
|
59
|
+
// syntheticDefaultImport: true,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'URL',
|
|
63
|
+
default: false,
|
|
64
|
+
values: true,
|
|
65
|
+
// syntheticDefaultImport: true,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
dependency: 'https://jslib.k6.io/url/1.0.0/index.js',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
exports: [
|
|
72
|
+
{
|
|
73
|
+
name: 'FormData',
|
|
74
|
+
default: false,
|
|
75
|
+
values: true,
|
|
76
|
+
// syntheticDefaultImport: true,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
dependency: 'https://jslib.k6.io/formdata/0.0.2/index.js',
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
function getSchemaTitleFromContext(context: ContextSpecs) {
|
|
84
|
+
const specData = Object.values(context.specs)
|
|
85
|
+
|
|
86
|
+
if (specData[0]) {
|
|
87
|
+
return specData[0].info.title
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return DEFAULT_SCHEMA_TITLE
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _generateResponseTypeName(operationName: string): string {
|
|
94
|
+
return `${pascal(operationName)}Response`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _generateResponseTypeDefinition(
|
|
98
|
+
operationName: string,
|
|
99
|
+
response: GetterResponse
|
|
100
|
+
): string {
|
|
101
|
+
const typeName = _generateResponseTypeName(operationName)
|
|
102
|
+
let responseDataType = ''
|
|
103
|
+
|
|
104
|
+
if (response.definition.success) {
|
|
105
|
+
responseDataType += response.definition.success + ' | '
|
|
106
|
+
}
|
|
107
|
+
responseDataType += 'ResponseBody'
|
|
108
|
+
|
|
109
|
+
return `export type ${typeName} = {
|
|
110
|
+
response: Response
|
|
111
|
+
data: ${responseDataType}
|
|
112
|
+
};`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const generateK6Implementation = (
|
|
116
|
+
{
|
|
117
|
+
headers,
|
|
118
|
+
queryParams,
|
|
119
|
+
operationName,
|
|
120
|
+
response,
|
|
121
|
+
body,
|
|
122
|
+
props,
|
|
123
|
+
verb,
|
|
124
|
+
override,
|
|
125
|
+
formData,
|
|
126
|
+
formUrlEncoded,
|
|
127
|
+
paramsSerializer,
|
|
128
|
+
}: GeneratorVerbOptions,
|
|
129
|
+
{ route }: GeneratorOptions,
|
|
130
|
+
analyticsData?: AnalyticsData
|
|
131
|
+
) => {
|
|
132
|
+
if (analyticsData) {
|
|
133
|
+
analyticsData.generatedRequestsCount[verb] += 1
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const bodyForm = generateFormDataAndUrlEncodedFunction({
|
|
137
|
+
formData,
|
|
138
|
+
formUrlEncoded,
|
|
139
|
+
body,
|
|
140
|
+
isFormData: true,
|
|
141
|
+
isFormUrlEncoded: false,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Generate response return types
|
|
145
|
+
returnTypesToWrite.set(
|
|
146
|
+
operationName,
|
|
147
|
+
_generateResponseTypeDefinition(operationName, response)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
let url = `cleanBaseUrl + \`${route}\``
|
|
151
|
+
|
|
152
|
+
if (queryParams) {
|
|
153
|
+
url += '+`?${new URLSearchParams(params).toString()}`'
|
|
154
|
+
}
|
|
155
|
+
const urlGeneration = `const url = new URL(${url});`
|
|
156
|
+
|
|
157
|
+
const options = getK6RequestOptions({
|
|
158
|
+
route,
|
|
159
|
+
body,
|
|
160
|
+
headers,
|
|
161
|
+
queryParams,
|
|
162
|
+
response,
|
|
163
|
+
verb,
|
|
164
|
+
requestOptions: override?.requestOptions,
|
|
165
|
+
paramsSerializer,
|
|
166
|
+
paramsSerializerOptions: override?.paramsSerializerOptions,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
return `const ${operationName} = (\n ${toObjectString(props, 'implementation')} requestParameters?: Params): ${_generateResponseTypeName(operationName)} => {${bodyForm}
|
|
170
|
+
${urlGeneration}
|
|
171
|
+
const mergedRequestParameters = _mergeRequestParameters(requestParameters || {}, clientOptions.commonRequestParameters);
|
|
172
|
+
const response = http.request(${options});
|
|
173
|
+
let data;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
data = response.json();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
data = response.body;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
response,
|
|
182
|
+
data
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
`
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type OptionsInput = {
|
|
189
|
+
route: string
|
|
190
|
+
body: GetterBody
|
|
191
|
+
headers?: GetterQueryParam
|
|
192
|
+
queryParams?: GetterQueryParam
|
|
193
|
+
response: GetterResponse
|
|
194
|
+
verb: Verbs
|
|
195
|
+
requestOptions?: object | boolean
|
|
196
|
+
isVue?: boolean
|
|
197
|
+
paramsSerializer?: GeneratorMutator
|
|
198
|
+
paramsSerializerOptions?: ParamsSerializerOptions
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const getParamsInputValue = ({
|
|
202
|
+
response,
|
|
203
|
+
queryParams,
|
|
204
|
+
headers,
|
|
205
|
+
body,
|
|
206
|
+
}: {
|
|
207
|
+
response: GetterResponse
|
|
208
|
+
body: GetterBody
|
|
209
|
+
queryParams?: GeneratorSchema
|
|
210
|
+
headers?: GeneratorSchema
|
|
211
|
+
}) => {
|
|
212
|
+
if (!queryParams && !headers && !response.isBlob && !body.contentType) {
|
|
213
|
+
// No parameters to merge, return the request parameters directly
|
|
214
|
+
return 'mergedRequestParameters'
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let value = '\n ...mergedRequestParameters,'
|
|
218
|
+
|
|
219
|
+
if (response.isBlob) {
|
|
220
|
+
value += `\n responseType: 'binary',`
|
|
221
|
+
}
|
|
222
|
+
// Expand the headers
|
|
223
|
+
if (body.contentType || headers) {
|
|
224
|
+
let headersValue = `\n headers: {`
|
|
225
|
+
if (body.contentType) {
|
|
226
|
+
if (body.formData) {
|
|
227
|
+
headersValue += `\n'Content-Type': '${body.contentType}; boundary=' + formData.boundary,`
|
|
228
|
+
} else {
|
|
229
|
+
headersValue += `\n'Content-Type': '${body.contentType}',`
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (headers) {
|
|
234
|
+
headersValue += `\n...headers,`
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
headersValue += `\n...mergedRequestParameters?.headers},`
|
|
238
|
+
value += headersValue
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return `{${value}}`
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const getK6RequestOptions = (options: OptionsInput) => {
|
|
245
|
+
const { body, headers, queryParams, response, verb } = options
|
|
246
|
+
|
|
247
|
+
let fetchBodyOption = 'undefined'
|
|
248
|
+
|
|
249
|
+
if (body.formData) {
|
|
250
|
+
// Use the FormData.body() method to get the body of the request
|
|
251
|
+
fetchBodyOption = 'formData.body()'
|
|
252
|
+
} else if (body.formUrlEncoded || body.implementation) {
|
|
253
|
+
fetchBodyOption = `JSON.stringify(${body.implementation})`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Generate the params input for the call
|
|
257
|
+
|
|
258
|
+
const paramsValue = getParamsInputValue({
|
|
259
|
+
response,
|
|
260
|
+
body,
|
|
261
|
+
headers: headers?.schema,
|
|
262
|
+
queryParams: queryParams?.schema,
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// Sample output
|
|
266
|
+
// 'GET', 'http://test.com/route', <body>, <options>
|
|
267
|
+
|
|
268
|
+
return `"${verb.toUpperCase()}",
|
|
269
|
+
url.toString(),
|
|
270
|
+
${fetchBodyOption},
|
|
271
|
+
${paramsValue}`
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function _getRequestParametersMergerFunctionImplementation() {
|
|
275
|
+
return `/**
|
|
276
|
+
* Merges the provided request parameters with default parameters for the client.
|
|
277
|
+
*
|
|
278
|
+
* @param {Params} requestParameters - The parameters provided specifically for the request
|
|
279
|
+
* @param {Params} commonRequestParameters - Common parameters for all requests
|
|
280
|
+
* @returns {Params} - The merged parameters
|
|
281
|
+
*/
|
|
282
|
+
const _mergeRequestParameters = (requestParameters?: Params, commonRequestParameters?: Params): Params => {
|
|
283
|
+
return {
|
|
284
|
+
...commonRequestParameters, // Default to common parameters
|
|
285
|
+
...requestParameters, // Override with request-specific parameters
|
|
286
|
+
headers: {
|
|
287
|
+
...commonRequestParameters?.headers || {}, // Ensure headers are defined
|
|
288
|
+
...requestParameters?.headers || {},
|
|
289
|
+
},
|
|
290
|
+
cookies: {
|
|
291
|
+
...commonRequestParameters?.cookies || {}, // Ensure cookies are defined
|
|
292
|
+
...requestParameters?.cookies || {},
|
|
293
|
+
},
|
|
294
|
+
tags: {
|
|
295
|
+
...commonRequestParameters?.tags || {}, // Ensure tags are defined
|
|
296
|
+
...requestParameters?.tags || {},
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
};`
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const generateTitle: ClientTitleBuilder = (title) => {
|
|
303
|
+
const sanTitle = sanitize(title || DEFAULT_SCHEMA_TITLE)
|
|
304
|
+
return `create${pascal(sanTitle)}`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export const generateK6Header: ClientHeaderBuilder = ({ title }) => {
|
|
308
|
+
const clientOptionsTypeName = `${pascal(title)}Options`
|
|
309
|
+
return `
|
|
310
|
+
export type ${clientOptionsTypeName} = {
|
|
311
|
+
baseUrl: string,
|
|
312
|
+
commonRequestParameters?: Params
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* This is the base client to use for interacting with the API.
|
|
317
|
+
*/
|
|
318
|
+
export const ${title} = (clientOptions: ${clientOptionsTypeName}) => {\n
|
|
319
|
+
const cleanBaseUrl = clientOptions.baseUrl.replace(/\\/+$/, '');\n`
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export const generateFooter: ClientFooterBuilder = ({ operationNames }) => {
|
|
323
|
+
let footer = ''
|
|
324
|
+
|
|
325
|
+
footer += `return {${operationNames.join(',')}}};\n\n`
|
|
326
|
+
|
|
327
|
+
operationNames.forEach((operationName) => {
|
|
328
|
+
if (returnTypesToWrite.has(operationName)) {
|
|
329
|
+
footer += returnTypesToWrite.get(operationName) + '\n'
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
// Add function definition for merging request parameters
|
|
334
|
+
footer += `\n\n${_getRequestParametersMergerFunctionImplementation()}\n`
|
|
335
|
+
|
|
336
|
+
return footer
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const k6ScriptBuilder: ClientExtraFilesBuilder = async (
|
|
340
|
+
verbOptions,
|
|
341
|
+
output,
|
|
342
|
+
context
|
|
343
|
+
) => {
|
|
344
|
+
console.log(
|
|
345
|
+
JSON.stringify(
|
|
346
|
+
{
|
|
347
|
+
verbOptions,
|
|
348
|
+
output,
|
|
349
|
+
context,
|
|
350
|
+
},
|
|
351
|
+
null,
|
|
352
|
+
2
|
|
353
|
+
)
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
const schemaTitle = getSchemaTitleFromContext(context)
|
|
357
|
+
const {
|
|
358
|
+
path: pathOfGeneratedClient,
|
|
359
|
+
filename,
|
|
360
|
+
extension,
|
|
361
|
+
} = await getGeneratedClientPath(output.target!, schemaTitle)
|
|
362
|
+
const directoryPath = getDirectoryForPath(pathOfGeneratedClient)
|
|
363
|
+
const generateScriptPath = path.join(
|
|
364
|
+
directoryPath,
|
|
365
|
+
SAMPLE_K6_SCRIPT_FILE_NAME
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
const clientFunctionsList = []
|
|
369
|
+
|
|
370
|
+
for (const verbOption of Object.values(verbOptions)) {
|
|
371
|
+
const { operationName, summary, props } = verbOption
|
|
372
|
+
const requiredProps = props.filter((prop) => prop.required)
|
|
373
|
+
clientFunctionsList.push({
|
|
374
|
+
operationName,
|
|
375
|
+
summary,
|
|
376
|
+
requiredParametersString: toObjectString(requiredProps, 'name'),
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const scriptContentData = {
|
|
381
|
+
clientFunctionName: generateTitle(schemaTitle),
|
|
382
|
+
clientPath: `./${filename}${extension}`,
|
|
383
|
+
clientFunctionsList,
|
|
384
|
+
}
|
|
385
|
+
const template = Handlebars.compile(K6_SCRIPT_TEMPLATE)
|
|
386
|
+
|
|
387
|
+
return [
|
|
388
|
+
{
|
|
389
|
+
path: generateScriptPath,
|
|
390
|
+
content: template(scriptContentData),
|
|
391
|
+
},
|
|
392
|
+
]
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function getK6Client(
|
|
396
|
+
schemaDetails: SchemaDetails,
|
|
397
|
+
analyticsData?: AnalyticsData
|
|
398
|
+
) {
|
|
399
|
+
return function (
|
|
400
|
+
verbOptions: GeneratorVerbOptions,
|
|
401
|
+
options: GeneratorOptions
|
|
402
|
+
) {
|
|
403
|
+
const imports = generateVerbImports(verbOptions)
|
|
404
|
+
const implementation = generateK6Implementation(
|
|
405
|
+
verbOptions,
|
|
406
|
+
options,
|
|
407
|
+
analyticsData
|
|
408
|
+
)
|
|
409
|
+
schemaDetails.title = getSchemaTitleFromContext(options.context)
|
|
410
|
+
const specData = Object.values(options.context.specs)
|
|
411
|
+
if (specData[0]) {
|
|
412
|
+
if (analyticsData) {
|
|
413
|
+
analyticsData.openApiSpecVersion = specData[0].openapi
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { implementation, imports }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function getK6ClientBuilder(
|
|
422
|
+
schemaDetails: SchemaDetails,
|
|
423
|
+
shouldGenerateSampleK6Script?: boolean,
|
|
424
|
+
analyticsData?: AnalyticsData
|
|
425
|
+
): ClientGeneratorsBuilder {
|
|
426
|
+
return {
|
|
427
|
+
client: getK6Client(schemaDetails, analyticsData),
|
|
428
|
+
header: generateK6Header,
|
|
429
|
+
dependencies: getK6Dependencies,
|
|
430
|
+
footer: generateFooter,
|
|
431
|
+
title: generateTitle,
|
|
432
|
+
extraFiles: shouldGenerateSampleK6Script ? k6ScriptBuilder : undefined,
|
|
433
|
+
}
|
|
434
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
enum LogLevel {
|
|
4
|
+
INFO = 'INFO',
|
|
5
|
+
WARNING = 'WARNING',
|
|
6
|
+
ERROR = 'ERROR',
|
|
7
|
+
DEBUG = 'DEBUG',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class Logger {
|
|
11
|
+
private static instance: Logger
|
|
12
|
+
private isVerbose: boolean = false
|
|
13
|
+
|
|
14
|
+
private constructor() {}
|
|
15
|
+
|
|
16
|
+
public static getInstance(): Logger {
|
|
17
|
+
if (!Logger.instance) {
|
|
18
|
+
Logger.instance = new Logger()
|
|
19
|
+
}
|
|
20
|
+
return Logger.instance
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public setVerbose(verbose: boolean): void {
|
|
24
|
+
this.isVerbose = verbose
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private logWithColor(
|
|
28
|
+
message: string,
|
|
29
|
+
level: LogLevel,
|
|
30
|
+
color: chalk.Chalk
|
|
31
|
+
): void {
|
|
32
|
+
const timestamp = new Date().toISOString()
|
|
33
|
+
console.log(
|
|
34
|
+
`${color(`[${level}]`)} ${chalk.gray(`[${timestamp}]`)} ${message}`
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public info(message: string): void {
|
|
39
|
+
this.logWithColor(message, LogLevel.INFO, chalk.blue)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public logMessage(message: string): void {
|
|
43
|
+
console.log(message)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public warning(message: string): void {
|
|
47
|
+
this.logWithColor(message, LogLevel.WARNING, chalk.yellow)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public error(message: string): void {
|
|
51
|
+
this.logWithColor(message, LogLevel.ERROR, chalk.red)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public debug(message: string): void {
|
|
55
|
+
if (this.isVerbose) {
|
|
56
|
+
this.logWithColor(message, LogLevel.DEBUG, chalk.green)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const logger = Logger.getInstance()
|