Skip to content

Commit

Permalink
feat: import open api / swagger files (#1207)
Browse files Browse the repository at this point in the history
  • Loading branch information
danishjoseph authored May 10, 2024
1 parent 0d290ef commit 3d82783
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 1 deletion.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down
91 changes: 91 additions & 0 deletions src/controllers/swaggerController.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
4 changes: 4 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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: '*' }
Expand Down
77 changes: 77 additions & 0 deletions src/utils/swaggerUtils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}

0 comments on commit 3d82783

Please sign in to comment.