diff --git a/package.json b/package.json index 8966fcef..910590c8 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "onCommand:rest-client.show-raw-response", "onCommand:rest-client.clear-cookies", "onLanguage:http", - "onLanguage:markdown" + "onLanguage:markdown", + "onCommand:rest-client.import-swagger" ], "main": "./dist/extension", "contributes": { @@ -197,6 +198,11 @@ "command": "rest-client.show-raw-response", "title": "Raw", "category": "Rest Client" + }, + { + "command": "rest-client.import-swagger", + "title": "Import from file", + "category": "Rest Client" } ], "menus": { diff --git a/src/controllers/swaggerController.ts b/src/controllers/swaggerController.ts new file mode 100644 index 00000000..d6419656 --- /dev/null +++ b/src/controllers/swaggerController.ts @@ -0,0 +1,91 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import dayjs from 'dayjs'; +import { SwaggerUtils } from '../utils/swaggerUtils'; + +export class SwaggerController { + private swaggerUtils: SwaggerUtils; + + public constructor(private context: vscode.ExtensionContext) { + this.swaggerUtils = new SwaggerUtils(); + } + + async import() { + const existingFiles = this.context.workspaceState.get<{ [fileName: string]: { content: string, timestamp: number } }>('importedFiles') || {}; + const importFromFileItem: vscode.QuickPickItem = { + label: 'Import from file...', + detail: 'Import from Swagger/OpenAPI', + }; + const recentImportsItems: vscode.QuickPickItem[] = Object.keys(existingFiles).map((fileName) => ({ + label: fileName, + detail: `${dayjs().to(existingFiles[fileName].timestamp)}`, + })); + const clearStateItem: vscode.QuickPickItem = { + label: 'Clear imported files', + }; + const items = [importFromFileItem, ...recentImportsItems]; + if (recentImportsItems.length > 0) { + items.push(clearStateItem); + } + const selectedItem = await vscode.window.showQuickPick(items, { + placeHolder: 'Select an option', + }); + + // Handle the user's selection + if (selectedItem) { + if (selectedItem === importFromFileItem) { + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: 'Import', + filters: { + 'YAML and JSON files': ['yml', 'yaml', 'json'], + }, + }; + + const fileUri = await vscode.window.showOpenDialog(options); + if (fileUri && fileUri[0]) { + const fileContent = fs.readFileSync(fileUri[0].fsPath, 'utf8'); + const fileName = path.basename(fileUri[0].fsPath); + this.createNewFileWithProcessedContent(fileContent); + this.storeImportedFile(fileName, fileContent); + } + } else if (selectedItem === clearStateItem) { + this.clearImportedFiles(); + vscode.window.showInformationMessage('Imported files have been cleared.'); + } else { + const selectedFile = selectedItem.label; + const fileContent = existingFiles[selectedFile]; + this.createNewFileWithProcessedContent(fileContent.content); + } + } else { + vscode.window.showInformationMessage('No option selected'); + } + } + + private storeImportedFile(fileName: string, content: string) { + const existingFiles = this.context.workspaceState.get<{ [fileName: string]: { content: string, timestamp: number } }>('importedFiles') || {}; + existingFiles[fileName] = { + content, + timestamp: Date.now(), + }; + this.context.workspaceState.update('importedFiles', existingFiles); + } + + private clearImportedFiles() { + this.context.workspaceState.update('importedFiles', {}); + } + + async createNewFileWithProcessedContent(originalContent: string) { + try { + const processedContent = this.swaggerUtils.parseOpenApiYaml(originalContent); + const newFile = await vscode.workspace.openTextDocument({ + content: processedContent, + language: 'http', + }); + vscode.window.showTextDocument(newFile); + } catch (error) { + vscode.window.showErrorMessage(error.message); + } + } +} diff --git a/src/extension.ts b/src/extension.ts index db35828e..2367b111 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -22,6 +22,7 @@ import { RequestVariableHoverProvider } from './providers/requestVariableHoverPr import { AadTokenCache } from './utils/aadTokenCache'; import { ConfigurationDependentRegistration } from './utils/dependentRegistration'; import { UserDataManager } from './utils/userDataManager'; +import { SwaggerController } from './controllers/swaggerController'; // this method is called when your extension is activated // your extension is activated the very first time the command is executed @@ -32,6 +33,7 @@ export async function activate(context: ExtensionContext) { const historyController = new HistoryController(); const codeSnippetController = new CodeSnippetController(context); const environmentController = await EnvironmentController.create(); + const swaggerController = new SwaggerController(context); context.subscriptions.push(requestController); context.subscriptions.push(historyController); context.subscriptions.push(codeSnippetController); @@ -51,6 +53,8 @@ export async function activate(context: ExtensionContext) { window.showErrorMessage(error.message); }); })); + context.subscriptions.push(commands.registerCommand('rest-client.import-swagger', async () => swaggerController.import())); + const documentSelector = [ { language: 'http', scheme: '*' } diff --git a/src/utils/swaggerUtils.ts b/src/utils/swaggerUtils.ts new file mode 100644 index 00000000..aa2812f2 --- /dev/null +++ b/src/utils/swaggerUtils.ts @@ -0,0 +1,77 @@ +import * as yaml from 'js-yaml'; + +export class SwaggerUtils { + generateRestClientOutput(openApiYaml: any): string { + const info = openApiYaml.info; + const baseUrl = `${openApiYaml.servers[0].url}`; + const paths = openApiYaml.paths; + const components = openApiYaml.components; + + let restClientOutput = ""; + restClientOutput += `### ${info.title}\n`; + + for (const endpoint in paths) { + const methods = paths[endpoint]; + for (const operation in methods) { + const details = methods[operation]; + restClientOutput += this.generateOperationBlock(operation, baseUrl, endpoint, details, components); + } + } + return restClientOutput; + } + + generateOperationBlock(operation: string, baseUrl: string, endpoint: string, details: any, components: any): string { + const summary = details.summary ? `- ${details.summary}` : ""; + let operationBlock = `\n#${operation.toUpperCase()} ${summary}\n`; + + if (details.requestBody) { + const content = details.requestBody.content; + for (const content_type in content) { + const exampleObject = this.getExampleObjectFromSchema(components, content[content_type].schema); + operationBlock += `${operation.toUpperCase()} ${baseUrl}${endpoint} HTTP/1.1\n`; + operationBlock += `Content-Type: ${content_type}\n`; + operationBlock += `${JSON.stringify(exampleObject, null, 2)}\n\n`; + } + } else { + operationBlock += `${operation.toUpperCase()} ${baseUrl}${endpoint} HTTP/1.1\n`; + } + operationBlock += '\n###' + return operationBlock; + } + + getExampleObjectFromSchema(components: any, schema: any): any { + if (!schema) return; + + if (schema.$ref) { + const schemaRef = schema.$ref; + const schemaPath = schemaRef.replace("#/components/", "").split("/"); + schema = schemaPath.reduce((obj, key) => obj[key], components); + } + + switch (schema.type) { + case "object": + const obj = {}; + for (const prop in schema.properties) { + if (schema.anyOf) { + return this.getExampleObjectFromSchema(components, + schema.anyOf[0]); + } + obj[prop] = this.getExampleObjectFromSchema(components, schema.properties[prop]); + } + return obj; + case "array": + return [this.getExampleObjectFromSchema(components, schema.items)]; + default: + return schema.example || schema.type; + } + } + + parseOpenApiYaml(data: string): string | undefined { + try { + const openApiYaml = yaml.load(data); + return this.generateRestClientOutput(openApiYaml); + } catch (error) { + throw error; + } + } +}