diff --git a/generator/konfig-next-app/package.json b/generator/konfig-next-app/package.json
index 316f01cac..ebb68b123 100644
--- a/generator/konfig-next-app/package.json
+++ b/generator/konfig-next-app/package.json
@@ -41,6 +41,7 @@
"axios": "^1.4.0",
"camelcase": "^8.0.0",
"chroma-js": "^2.4.2",
+ "clone": "^2.1.2",
"dayjs": "^1.11.8",
"deepmerge": "^4.3.1",
"eslint": "8.41.0",
@@ -79,6 +80,7 @@
},
"devDependencies": {
"@types/chroma-js": "^2.4.0",
+ "@types/clone": "^2.1.3",
"@types/har-format": "^1.2.14",
"@types/jest": "^29.5.5",
"@types/js-yaml": "^4.0.5",
diff --git a/generator/konfig-next-app/src/components/CopyButton.tsx b/generator/konfig-next-app/src/components/CopyButton.tsx
index 3a8d0c01a..46be84203 100644
--- a/generator/konfig-next-app/src/components/CopyButton.tsx
+++ b/generator/konfig-next-app/src/components/CopyButton.tsx
@@ -30,7 +30,7 @@ export function CopyButton({ value }: { value: string }) {
{
['bg-white/5 hover:bg-white/7.5 dark:bg-white/2.5 shadow dark:shadow-none dark:hover:bg-white/5']:
!copied,
- ['dark:bg-brand-400/10 bg-brand-600/20 ring-1 ring-inset dark:ring-brand-400/20 ring-brand-600/40']:
+ ['bg-brand-600/20 ring-1 ring-inset dark:ring-brand-400/20 ring-brand-600/40']:
copied,
}
)}
diff --git a/generator/konfig-next-app/src/components/OperationFormGeneratedCode.tsx b/generator/konfig-next-app/src/components/OperationFormGeneratedCode.tsx
index ebeb5a5af..689e00779 100644
--- a/generator/konfig-next-app/src/components/OperationFormGeneratedCode.tsx
+++ b/generator/konfig-next-app/src/components/OperationFormGeneratedCode.tsx
@@ -11,23 +11,41 @@ import { CopyButton } from './CopyButton'
export function OperationFormGeneratedCode(
args: CodeGeneratorConstructorArgs & { language: Language }
) {
- const [data, setData] = useState('Loading...') // Initial state
+ const [data, setData] = useState('Loading...')
+ const [copyData, setCopyData] = useState('')
+
+ console.log(args.formData)
useEffect(() => {
if (args.language === 'typescript') {
new CodeGeneratorTypeScript(args).snippet().then((result) => {
setData(result)
})
+ new CodeGeneratorTypeScript({ ...args, mode: 'copy' })
+ .snippet()
+ .then((result) => {
+ setCopyData(result)
+ })
} else if (args.language === 'python') {
new CodeGeneratorPython(args).snippet().then((result) => {
setData(result)
})
+ new CodeGeneratorPython({ ...args, mode: 'copy' })
+ .snippet()
+ .then((result) => {
+ setCopyData(result)
+ })
} else if (args.language === 'bash') {
new CodeGeneratorHttpsnippet({ ...args, targetId: 'shell' })
.snippet()
.then((result) => {
setData(result)
})
+ new CodeGeneratorHttpsnippet({ ...args, targetId: 'shell', mode: 'copy' })
+ .snippet()
+ .then((result) => {
+ setCopyData(result)
+ })
} else {
throw Error(`Unxpected language: "${args.language}"`)
}
@@ -50,7 +68,7 @@ export function OperationFormGeneratedCode(
{data}
-
+
)
}
diff --git a/generator/konfig-next-app/src/components/OperationReferenceMain.tsx b/generator/konfig-next-app/src/components/OperationReferenceMain.tsx
index 225de9bb8..129c76c2f 100644
--- a/generator/konfig-next-app/src/components/OperationReferenceMain.tsx
+++ b/generator/konfig-next-app/src/components/OperationReferenceMain.tsx
@@ -220,8 +220,6 @@ export function OperationReferenceMain({
}
}
- const [language, setLanguage] = useState('typescript')
-
const header = operation.operation.summary ?? operation.path
return (
diff --git a/generator/konfig-next-app/src/utils/code-generator-httpsnippet.ts b/generator/konfig-next-app/src/utils/code-generator-httpsnippet.ts
index 25e305cc7..0b726377c 100644
--- a/generator/konfig-next-app/src/utils/code-generator-httpsnippet.ts
+++ b/generator/konfig-next-app/src/utils/code-generator-httpsnippet.ts
@@ -15,10 +15,14 @@ export class CodeGeneratorHttpsnippet extends CodeGenerator {
options: Options
constructor(args: CodeGeneratorHttpSnippetConstructorArgs) {
super(args)
+
+ const securities = this.isCopyMode()
+ ? this.nonEmptySecurity()
+ : this.nonEmptySecurityMasked()
const harRequest = convertToHarRequest(
- this.nonEmptyParameters,
- this.mode === 'ui' ? this.nonEmptySecurityMasked : this.nonEmptySecurity,
- this.requestBodyValue,
+ this.nonEmptyParameters(),
+ securities,
+ this.requestBodyValue(),
// can't use URL because we don't want to encode { and } in path yet
`${this.basePath}${this.configuration.path}`,
this.configuration.httpMethod.toUpperCase(),
diff --git a/generator/konfig-next-app/src/utils/code-generator-python.ts b/generator/konfig-next-app/src/utils/code-generator-python.ts
index d0a0628d8..54f6cc9cd 100644
--- a/generator/konfig-next-app/src/utils/code-generator-python.ts
+++ b/generator/konfig-next-app/src/utils/code-generator-python.ts
@@ -43,13 +43,13 @@ from ${this.packageName} import ${this.clientName}`
get setupArgs(): string {
const args = []
- for (const [name, security] of this.nonEmptySecurity) {
+ for (const [name, security] of this.nonEmptySecurity()) {
if (security[SECURITY_TYPE_PROPERTY] === 'clientState') {
args.push(`${this.snake_case(name)}="${this.mask(security.value)}"`)
} else if (security[SECURITY_TYPE_PROPERTY] === 'apiKey') {
args.push(
`${
- this.hasMultipleApiKeys
+ this.hasMultipleApiKeys()
? this.snake_case(security[API_KEY_NAME_PROPERTY])
: 'api_key'
}="${this.mask(security.value)}"`
@@ -118,9 +118,9 @@ from ${this.packageName} import ${this.clientName}`
}
get args(): string {
- if (this.isArrayRequestBody) {
+ if (this.isArrayRequestBody()) {
const arrayValue = this.recursivelyRemoveEmptyValues(
- this.requestBodyValue
+ this.requestBodyValue()
)
if (Array.isArray(arrayValue)) {
return `[${arrayValue
@@ -130,7 +130,7 @@ from ${this.packageName} import ${this.clientName}`
if (arrayValue === '') return ''
}
const args: string[] = []
- for (const [parameter, value] of this.nonEmptyParameters) {
+ for (const [parameter, value] of this.nonEmptyParameters()) {
args.push(
`${this.snake_case(parameter.name)}=${this.toPythonLiteralString(
value,
diff --git a/generator/konfig-next-app/src/utils/code-generator-typescript.ts b/generator/konfig-next-app/src/utils/code-generator-typescript.ts
index e8061ac3e..00c0f780f 100644
--- a/generator/konfig-next-app/src/utils/code-generator-typescript.ts
+++ b/generator/konfig-next-app/src/utils/code-generator-typescript.ts
@@ -45,15 +45,15 @@ export class CodeGeneratorTypeScript extends CodeGenerator {
const ${this.clientNameLowercase} = new ${this.client}(${this.setupArgs})
-${this.mode === 'ui' ? `const response =` : 'return'} await ${
+${this.isUiOrCopyMode() ? `const response =` : 'return'} await ${
this.clientNameLowercase
}.${this.namespace}.${this.methodName}(${this.args})
-${this.mode === 'ui' ? `console.log(response.data)` : ''}
+${this.isUiOrCopyMode() ? `console.log(response.data)` : ''}
`
}
get importStatement(): string {
- if (this.mode === 'ui') {
+ if (this.isUiOrCopyMode()) {
if (this.args.includes('fs.readFileSync'))
return `import { ${this.clientName} } from '${this.packageName}'\nimport fs from 'fs'`
return `import { ${this.clientName} } from '${this.packageName}'`
@@ -62,54 +62,48 @@ ${this.mode === 'ui' ? `console.log(response.data)` : ''}
}
get client(): string {
- if (this.mode === 'ui') {
+ if (this.isUiOrCopyMode()) {
return `${this.clientName}`
}
return `client.${this.clientName}`
}
get setupArgs(): string {
- return Object.keys(this.nonEmptySecurity).length === 0
+ return Object.keys(this.nonEmptySecurity()).length === 0
? `{${this.proxySetupArgs}}`
: `{
-${this.nonEmptySecurity
+${this.nonEmptySecurity()
.map(([_name, value]) => {
if (value.type === 'oauth2-client-credentials') {
- // convert value.clientSecret to string of same length with all values replace with char 'X'
const clientSecret =
this.mode === 'execution'
? value.clientSecret
- : value.clientSecret.replace(/./g, 'X')
+ : this.mask(value.clientSecret)
const clientId =
- this.mode === 'execution'
- ? value.clientId
- : value.clientId.replace(/./g, 'X')
+ this.mode === 'execution' ? value.clientId : this.mask(value.clientId)
return ` "oauthClientId": "${clientId}",
"oauthClientSecret": "${clientSecret}",`
}
if (value.type === 'bearer') {
- // convert value.value to string of same length with all values replace with char 'X'
const bearer =
- this.mode === 'execution' ? value.value : value.value.replace(/./g, 'X')
+ this.mode === 'execution' ? value.value : this.mask(value.value)
return ` "accessToken": "${bearer}",`
}
const securityValue = value.type === 'apiKey' ? value.value : value.value
const securityKey = this.quoteIfNecessary(
value.type === 'apiKey'
- ? this.hasMultipleApiKeys
+ ? this.hasMultipleApiKeys()
? value.key
: 'apiKey'
: value.name
)
// convert securityValue to string of same length with all values replace with char 'X'
const securityValueMasked =
- this.mode === 'execution'
- ? securityValue
- : securityValue.replace(/./g, 'X')
+ this.mode === 'execution' ? securityValue : this.mask(securityValue)
return ` ${securityKey}: '${securityValueMasked}',`
})
.join('\n')}${
- this.oauthTokenUrl !== null && this.isUsingCustomOAuthTokenUrl
+ this.oauthTokenUrl !== null && this.isUsingCustomOAuthTokenUrl()
? `oauthTokenUrl: "${this.oauthTokenUrl}",`
: ''
}
@@ -128,8 +122,8 @@ ${this.nonEmptySecurity
}
get proxySetupArgs(): string {
- return this.mode === 'ui'
- ? this.isUsingCustomBasePath
+ return this.isUiOrCopyMode()
+ ? this.isUsingCustomBasePath()
? `basePath: "${this.basePath}",`
: ''
: `basePath: "/api/proxy", baseOptions: {headers: {"x-proxy-target": "${this.basePath}"}}`
@@ -206,8 +200,8 @@ ${this.nonEmptySecurity
}
get args(): string {
- if (this.isArrayRequestBody) {
- const arrayValue = this.requestBodyValue
+ if (this.isArrayRequestBody()) {
+ const arrayValue = this.requestBodyValue()
if (Array.isArray(arrayValue)) {
return `[${arrayValue.map((v) => this.argValue(v)).join(', ')}]`
}
@@ -219,14 +213,14 @@ ${this.nonEmptySecurity
}
return ''
}
- const nonBodyParameters = this.nonEmptyParameters
+ const nonBodyParameters = this.nonEmptyParameters()
.filter(([{ parameter }]) => {
return parameter.in !== 'body'
})
.map(([{ name }, value]) => {
return [name, value] as [string, SdkArg]
})
- const bodyParameters = this.nonEmptyParameters
+ const bodyParameters = this.nonEmptyParameters()
.filter(([{ parameter }]) => {
return parameter.in === 'body'
})
diff --git a/generator/konfig-next-app/src/utils/code-generator.test.ts b/generator/konfig-next-app/src/utils/code-generator.test.ts
new file mode 100644
index 000000000..88628418a
--- /dev/null
+++ b/generator/konfig-next-app/src/utils/code-generator.test.ts
@@ -0,0 +1,75 @@
+import { HttpMethodsEnum } from 'konfig-lib'
+import { CodeGenerator, CodeGeneratorConstructorArgs } from './code-generator'
+import { SECURITY_FORM_NAME_PREFIX } from './generate-initial-operation-form-values'
+import clone from 'clone'
+
+class CodeGeneratorTest extends CodeGenerator {
+ protected format(code: string): Promise {
+ throw new Error(
+ 'This class is only used for testing shared functions in CodeGenerator'
+ )
+ }
+ protected gen(): string {
+ throw new Error(
+ 'This class is only used for testing shared functions in CodeGenerator'
+ )
+ }
+}
+
+/**
+ * Creates new instance of CodeGeneratorTest with values used for testing
+ */
+function testArgs(): CodeGeneratorConstructorArgs {
+ return {
+ path: '',
+ contentType: 'application/json',
+ httpMethod: HttpMethodsEnum.POST,
+ securitySchemes: {
+ apiKey: {
+ type: 'apiKey',
+ in: 'header',
+ name: 'X-Api-Key',
+ },
+ },
+ formData: {
+ requestBody: '',
+ parameters: {},
+ security: {
+ apiKey: {
+ type: 'apiKey',
+ in: 'header',
+ key: 'X-API-Key',
+ value: 'my_api_key',
+ },
+ },
+ },
+ parameters: [],
+ languageConfigurations: {
+ typescript: {
+ clientName: 'Test',
+ packageName: 'test',
+ },
+ python: {
+ clientName: 'Test',
+ packageName: 'tet',
+ },
+ },
+ tag: 'Test',
+ operationId: 'Test_test',
+ requestBody: null,
+ basePath: 'https://test.com/api',
+ requestBodyRequired: true,
+ servers: ['https://test.com/api'],
+ oauthTokenUrl: null,
+ originalOauthTokenUrl: null,
+ }
+}
+
+test('ensure nonEmptySecurityMasked does not modify security values in-place', async () => {
+ const args: CodeGeneratorConstructorArgs = testArgs()
+ const test = new CodeGeneratorTest(args)
+ const before = clone(test.configuration.formData[SECURITY_FORM_NAME_PREFIX])
+ test.nonEmptySecurityMasked()
+ const after = test.configuration.formData[SECURITY_FORM_NAME_PREFIX]
+ expect(before).toStrictEqual(after)
+})
diff --git a/generator/konfig-next-app/src/utils/code-generator.ts b/generator/konfig-next-app/src/utils/code-generator.ts
index c17fcc75d..c4756d652 100644
--- a/generator/konfig-next-app/src/utils/code-generator.ts
+++ b/generator/konfig-next-app/src/utils/code-generator.ts
@@ -1,4 +1,5 @@
import { Parameter } from '@/components/OperationParameter'
+import clone from 'clone'
import {
API_KEY_VALUE_PROPERTY,
BEARER_VALUE_PROPERTY,
@@ -149,6 +150,9 @@ export abstract class CodeGenerator {
* @returns The masked value
*/
mask(value: string) {
+ // Don't mask if in copy mode
+ if (this.isCopyMode()) return value
+
return value.replace(/./g, 'X')
}
@@ -156,15 +160,15 @@ export abstract class CodeGenerator {
return await this.format(this.gen())
}
- get isUsingCustomBasePath(): boolean {
+ isUsingCustomBasePath(): boolean {
return this.basePath !== this.servers[0]
}
- get isUsingCustomOAuthTokenUrl(): boolean {
+ isUsingCustomOAuthTokenUrl(): boolean {
return this.originalOauthTokenUrl !== this.oauthTokenUrl
}
- get hasMultipleApiKeys(): boolean {
+ hasMultipleApiKeys(): boolean {
if (this.configuration.securitySchemes === null) return false
const hasMultipleApiKeys =
Object.values(this.configuration.securitySchemes).filter(
@@ -173,7 +177,7 @@ export abstract class CodeGenerator {
return hasMultipleApiKeys
}
- get isArrayRequestBody(): boolean {
+ isArrayRequestBody(): boolean {
return this.configuration.requestBody?.schema?.type === 'array'
}
@@ -185,14 +189,14 @@ export abstract class CodeGenerator {
* a scalar or array request body, the request body cannot be flattened so it
* is passed as a separate argument.
*/
- get requestBodyValue(): FormInputValue {
+ requestBodyValue(): FormInputValue {
return this.recursivelyRemoveEmptyValues(this._formData['requestBody'])
}
/**
* Returns the setup values that are non-empty and exist as part of passed parameters
*/
- get nonEmptyParameters(): NonEmptyParameters {
+ nonEmptyParameters(): NonEmptyParameters {
const parameters = Object.entries(
this._formData[PARAMETER_FORM_NAME_PREFIX]
)
@@ -286,8 +290,8 @@ export abstract class CodeGenerator {
return filtered.length > 0
}
- get nonEmptySecurityMasked(): CodeGenerator['nonEmptySecurity'] {
- return this.nonEmptySecurity.map(([name, security]) => {
+ nonEmptySecurityMasked(): NonEmptySecurity {
+ return clone(this.nonEmptySecurity()).map(([name, security]) => {
if (security.type === 'apiKey') {
security[API_KEY_VALUE_PROPERTY] = this.mask(
security[API_KEY_VALUE_PROPERTY]
@@ -308,10 +312,18 @@ export abstract class CodeGenerator {
})
}
+ isUiOrCopyMode(): boolean {
+ return this.mode === 'ui' || this.mode === 'copy'
+ }
+
+ isCopyMode(): boolean {
+ return this.mode === 'copy'
+ }
+
/**
* Returns the security schemes that are non-empty
*/
- get nonEmptySecurity() {
+ nonEmptySecurity() {
return Object.entries(this._formData[SECURITY_FORM_NAME_PREFIX]).filter(
([_name, security]) => {
if (security.type === 'apiKey') {
@@ -342,4 +354,4 @@ export abstract class CodeGenerator {
}
}
-export type NonEmptySecurity = CodeGenerator['nonEmptySecurity']
+export type NonEmptySecurity = ReturnType
diff --git a/generator/konfig-next-app/yarn.lock b/generator/konfig-next-app/yarn.lock
index a4361a59b..673d58a98 100644
--- a/generator/konfig-next-app/yarn.lock
+++ b/generator/konfig-next-app/yarn.lock
@@ -1680,6 +1680,11 @@
resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.4.0.tgz#476a16ae848c77478079d6749236fdb98837b92c"
integrity sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw==
+"@types/clone@^2.1.3":
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/@types/clone/-/clone-2.1.3.tgz#91d16af4006a24fd3d683f5454d48b29a695b0e9"
+ integrity sha512-DxFaNYaIUXW1OSRCVCC1UHoLcvk6bVJ0v9VvUaZ6kR5zK8/QazXlOThgdvnK0Xpa4sBq+b/Yoq/mnNn383hVRw==
+
"@types/debug@^4.0.0":
version "4.1.8"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317"
@@ -2487,6 +2492,11 @@ cliui@^8.0.1:
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
+clone@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+ integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
+
clsx@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"