mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
Feat: add openapi to bruno import in cli
This commit is contained in:
230
packages/bruno-cli/src/commands/import.js
Normal file
230
packages/bruno-cli/src/commands/import.js
Normal file
@@ -0,0 +1,230 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chalk = require('chalk');
|
||||
const jsyaml = require('js-yaml');
|
||||
const axios = require('axios');
|
||||
const { openApiToBruno } = require('@usebruno/converters');
|
||||
const { exists, isDirectory, sanitizeName } = require('../utils/filesystem');
|
||||
const { createCollectionFromBrunoObject } = require('../utils/collection');
|
||||
|
||||
const command = 'import <type>';
|
||||
const desc = 'Import a collection from other formats';
|
||||
|
||||
const builder = (yargs) => {
|
||||
yargs
|
||||
.positional('type', {
|
||||
describe: 'Type of collection to import',
|
||||
type: 'string',
|
||||
choices: ['openapi']
|
||||
})
|
||||
.option('source', {
|
||||
alias: 's',
|
||||
describe: 'Path to the source file or URL',
|
||||
type: 'string',
|
||||
demandOption: true
|
||||
})
|
||||
.option('output', {
|
||||
alias: 'o',
|
||||
describe: 'Path to the output directory',
|
||||
type: 'string',
|
||||
conflicts: 'output-file'
|
||||
})
|
||||
.option('output-file', {
|
||||
alias: 'f',
|
||||
describe: 'Path to the output JSON file',
|
||||
type: 'string',
|
||||
conflicts: 'output'
|
||||
})
|
||||
.option('collection-name', {
|
||||
alias: 'n',
|
||||
describe: 'Name for the imported collection',
|
||||
type: 'string'
|
||||
})
|
||||
.option('insecure', {
|
||||
type: 'boolean',
|
||||
describe: 'Skip SSL certificate verification when fetching from URLs',
|
||||
default: false
|
||||
})
|
||||
.example('$0 import openapi --source api.yml --output ~/Desktop/my-collection --collection-name "My API"')
|
||||
.example('$0 import openapi -s api.yml -o ~/Desktop/my-collection -n "My API"')
|
||||
.example('$0 import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name "Remote API"')
|
||||
.example('$0 import openapi --source https://self-signed.example.com/api.json --insecure --output ~/Desktop')
|
||||
.example('$0 import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name "My API"')
|
||||
.example('$0 import openapi -s api.yml -f ~/Desktop/my-collection.json -n "My API"');
|
||||
};
|
||||
|
||||
const isUrl = (str) => {
|
||||
try {
|
||||
return Boolean(new URL(str));
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const readOpenApiFile = async (source, options = {}) => {
|
||||
try {
|
||||
let content;
|
||||
|
||||
if (isUrl(source)) {
|
||||
// Handle URL input
|
||||
console.log(chalk.yellow(`Fetching specification from URL: ${source}`));
|
||||
try {
|
||||
const axiosOptions = {
|
||||
timeout: 30000, // 30 second timeout
|
||||
maxContentLength: 10 * 1024 * 1024,
|
||||
validateStatus: status => status >= 200 && status < 300
|
||||
};
|
||||
|
||||
// Skip SSL certificate validation if insecure flag is set
|
||||
if (options.insecure) {
|
||||
console.log(chalk.yellow('Warning: SSL certificate verification is disabled. Use with caution.'));
|
||||
axiosOptions.httpsAgent = new (require('https')).Agent({ rejectUnauthorized: false });
|
||||
}
|
||||
|
||||
const response = await axios.get(source, axiosOptions);
|
||||
content = response.data;
|
||||
} catch (error) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
throw new Error('Request timed out. The server took too long to respond.');
|
||||
} else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' ||
|
||||
error.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
|
||||
throw new Error(`SSL Certificate error: ${error.code}. Try using --insecure if you trust this source.`);
|
||||
} else if (error.response) {
|
||||
throw new Error(`Failed to fetch from URL: ${error.response.status} ${error.response.statusText}`);
|
||||
} else if (error.request) {
|
||||
throw new Error(`No response received from server. Check the URL and your network connection.`);
|
||||
} else {
|
||||
throw new Error(`Error fetching URL: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If response is already an object, return it directly
|
||||
if (typeof content === 'object' && content !== null) {
|
||||
return content;
|
||||
}
|
||||
} else {
|
||||
// Handle file input
|
||||
if (!await exists(source)) {
|
||||
throw new Error(`File does not exist: ${source}`);
|
||||
}
|
||||
content = fs.readFileSync(source, 'utf8');
|
||||
}
|
||||
|
||||
// If content is a string, try to parse as JSON or YAML
|
||||
if (typeof content === 'string') {
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (jsonError) {
|
||||
try {
|
||||
return jsyaml.load(content);
|
||||
} catch (yamlError) {
|
||||
throw new Error('Failed to parse content as JSON or YAML');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
} catch (error) {
|
||||
// Let the specific error handling from above propagate
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handler = async (argv) => {
|
||||
try {
|
||||
const { type, source, output, outputFile, collectionName, insecure } = argv;
|
||||
|
||||
if (!type || type !== 'openapi') {
|
||||
console.error(chalk.red('Only OpenAPI import is supported currently'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!source) {
|
||||
console.error(chalk.red('Source file or URL is required'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!output && !outputFile) {
|
||||
console.error(chalk.red('Either --output or --output-file is required'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(`Reading OpenAPI specification from ${source}...`));
|
||||
|
||||
const openApiSpec = await readOpenApiFile(source, { insecure });
|
||||
|
||||
if (!openApiSpec) {
|
||||
console.error(chalk.red('Failed to parse OpenAPI specification'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.yellow('Converting OpenAPI specification to Bruno format...'));
|
||||
|
||||
// Convert OpenAPI to Bruno format
|
||||
let brunoCollection = openApiToBruno(openApiSpec);
|
||||
|
||||
// Override collection name if provided
|
||||
if (collectionName) {
|
||||
brunoCollection.name = collectionName;
|
||||
}
|
||||
|
||||
if (outputFile) {
|
||||
// Save as JSON file
|
||||
const outputPath = path.resolve(outputFile);
|
||||
fs.writeFileSync(outputPath, JSON.stringify(brunoCollection, null, 2));
|
||||
console.log(chalk.green(`Bruno collection saved as JSON to ${outputPath}`));
|
||||
} else if (output) {
|
||||
const resolvedOutput = path.resolve(output);
|
||||
|
||||
// Check if output is an existing directory
|
||||
const isOutputDirectory = await exists(resolvedOutput) && isDirectory(resolvedOutput);
|
||||
|
||||
// Determine the final output directory
|
||||
let outputDir;
|
||||
if (isOutputDirectory) {
|
||||
// If output is an existing directory, use collection name to create a subdirectory
|
||||
const dirName = sanitizeName(brunoCollection.name);
|
||||
outputDir = path.join(resolvedOutput, dirName);
|
||||
|
||||
// Check if this subfolder already exists
|
||||
if (await exists(outputDir)) {
|
||||
const dirContents = fs.readdirSync(outputDir);
|
||||
if (dirContents.length > 0) {
|
||||
console.error(chalk.red(`Output directory is not empty: ${outputDir}`));
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Create the subfolder
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
} else {
|
||||
// If output doesn't exist or is not a directory, use it directly
|
||||
outputDir = resolvedOutput;
|
||||
|
||||
// Check if parent directory exists
|
||||
const parentDir = path.dirname(outputDir);
|
||||
if (!await exists(parentDir)) {
|
||||
console.error(chalk.red(`Parent directory does not exist: ${parentDir}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
await createCollectionFromBrunoObject(brunoCollection, outputDir);
|
||||
console.log(chalk.green(`Bruno collection created at ${outputDir}`));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
command,
|
||||
desc,
|
||||
builder,
|
||||
handler,
|
||||
isUrl,
|
||||
readOpenApiFile
|
||||
};
|
||||
Reference in New Issue
Block a user