feat: cli runner can now run a single request

This commit is contained in:
Anoop M D 2023-02-06 03:40:13 +05:30
parent 404a516fef
commit 1c869013c6
8 changed files with 399 additions and 19 deletions

View File

@ -13,6 +13,7 @@
"@usebruno/js": "0.1.0",
"@usebruno/lang": "0.1.0",
"chalk": "^3.0.0",
"fs-extra": "^10.1.0",
"inquirer": "^9.1.4",
"yargs": "^17.6.2"
}

View File

@ -1,43 +1,44 @@
const chalk = require('chalk');
const {
exists,
isFile,
isDirectory
} = require('../utils/filesystem');
const {
runSingleRequest
} = require('../runner/run-single-request');
const {
CLI_EPILOGUE,
} = require('../constants');
const command = 'run';
const command = 'run <filename>';
const desc = 'Run a request';
const cmdArgs = {
filename: {
desc: 'Run a request',
type: 'string',
}
};
const builder = async (yargs) => {
yargs.options(cmdArgs).epilogue(CLI_EPILOGUE).help();
yargs.example('$0 filename', 'Run a request');
yargs.example('$0 run request.bru', 'Run a request');
};
const handler = async function (argv) {
try {
if (!argv.filename) {
console.log(chalk.cyan('Please specify a filename'));
console.log(`Example: ${argv.$0} run request.bru`);
const { filename } = argv;
return;
const pathExists = await exists(filename);
if(!pathExists) {
console.error(chalk.red(`File or directory ${filename} does not exist`));
}
const _isFile = await isFile(filename);
if(_isFile) {
runSingleRequest(filename);
}
console.log("here");
} catch (err) {
console.error(err);
}
};
module.exports = {
command,
desc,
builder,
cmdArgs,
handler
};

View File

@ -3,7 +3,6 @@ const chalk = require('chalk');
const { CLI_EPILOGUE, CLI_VERSION } = require('./constants');
const printBanner = () => {
console.log(chalk.yellow(`Bru CLI ${CLI_VERSION}`));
}

View File

@ -0,0 +1,55 @@
const _ = require('lodash');
const {
bruToJsonV2
} = require('@usebruno/lang');
/**
* The transformer function for converting a BRU file to JSON.
*
* We map the json response from the bru lang and transform it into the DSL
* format that is used by the bruno app
*
* @param {string} bru The BRU file content.
* @returns {object} The JSON representation of the BRU file.
*/
const bruToJson = (bru) => {
try {
const json = bruToJsonV2(bru);
let requestType = _.get(json, "meta.type");
if(requestType === "http") {
requestType = "http-request"
} else if(requestType === "graphql") {
requestType = "graphql-request";
} else {
requestType = "http";
}
const sequence = _.get(json, "meta.seq")
const transformedJson = {
"type": requestType,
"name": _.get(json, "meta.name"),
"seq": !isNaN(sequence) ? Number(sequence) : 1,
"request": {
"method": _.upperCase(_.get(json, "http.method")),
"url": _.get(json, "http.url"),
"params": _.get(json, "query", []),
"headers": _.get(json, "headers", []),
"body": _.get(json, "body", {}),
},
"script": _.get(json, "script", ""),
"test": _.get(json, "test", "")
};
transformedJson.request.body.mode = _.get(json, "http.mode", "none");
return transformedJson;
} catch (err) {
return Promise.reject(err);
}
};
module.exports = {
bruToJson
};

View File

@ -0,0 +1,54 @@
const Mustache = require('mustache');
const { each, forOwn } = require('lodash');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
return value;
};
const interpolateVars = (request, envVars = {}, collectionVariables ={}) => {
const interpolate = (str) => {
if(!str || !str.length || typeof str !== "string") {
return str;
}
// collectionVariables take precedence over envVars
const combinedVars = {
...envVars,
...collectionVariables
};
return Mustache.render(str, combinedVars);
};
request.url = interpolate(request.url);
forOwn(request.headers, (value, key) => {
request.headers[key] = interpolate(value);
});
if(request.headers["content-type"] === "application/json") {
if(typeof request.data === "object") {
try {
let parsed = JSON.stringify(request.data);
parsed = interpolate(parsed);
request.data = JSON.parse(parsed);
} catch (err) {
}
}
if(typeof request.data === "string") {
if(request.data.length) {
request.data = interpolate(request.data);
}
}
}
each(request.params, (param) => {
param.value = interpolate(param.value);
});
return request;
};
module.exports = interpolateVars;

View File

@ -0,0 +1,71 @@
const { get, each, filter } = require('lodash');
const qs = require('qs');
const prepareRequest = (request) => {
const headers = {};
each(request.headers, (h) => {
if (h.enabled) {
headers[h.name] = h.value;
}
});
let axiosRequest = {
method: request.method,
url: request.url,
headers: headers
};
request.body = request.body || {};
if (request.body.mode === 'json') {
axiosRequest.headers['content-type'] = 'application/json';
try {
axiosRequest.data = JSON.parse(request.body.json);
} catch (ex) {
axiosRequest.data = request.body.json;
}
}
if (request.body.mode === 'text') {
axiosRequest.headers['content-type'] = 'text/plain';
axiosRequest.data = request.body.text;
}
if (request.body.mode === 'xml') {
axiosRequest.headers['content-type'] = 'text/xml';
axiosRequest.data = request.body.xml;
}
if (request.body.mode === 'formUrlEncoded') {
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
const params = {};
const enabledParams = filter(request.body.formUrlEncoded, (p) => p.enabled);
each(enabledParams, (p) => (params[p.name] = p.value));
axiosRequest.data = qs.stringify(params);
}
if (request.body.mode === 'multipartForm') {
const params = {};
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
each(enabledParams, (p) => (params[p.name] = p.value));
axiosRequest.headers['content-type'] = 'multipart/form-data';
axiosRequest.data = params;
}
if (request.body.mode === 'graphql') {
const graphqlQuery = {
query: get(request, 'body.graphql.query'),
variables: JSON.parse(get(request, 'body.graphql.variables') || '{}')
};
axiosRequest.headers['content-type'] = 'application/json';
axiosRequest.data = graphqlQuery;
}
if (request.script && request.script.length) {
axiosRequest.script = request.script;
}
return axiosRequest;
};
module.exports = prepareRequest;

View File

@ -0,0 +1,85 @@
const Mustache = require('mustache');
const fs = require('fs');
const { forOwn, each, extend, get } = require('lodash');
const FormData = require('form-data');
const axios = require('axios');
const prepareRequest = require('./prepare-request');
const { ScriptRuntime, TestRuntime } = require('@usebruno/js');
const {
bruToJson
} = require('./bru');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
return value;
};
const getEnvVars = (environment = {}) => {
const variables = environment.variables;
if (!variables || !variables.length) {
return {};
}
const envVars = {};
each(variables, (variable) => {
if(variable.enabled) {
envVars[variable.name] = Mustache.escape(variable.value);
}
});
return envVars;
};
const runSingleRequest = async function (filepath) {
try {
const bruContent = fs.readFileSync(filepath, 'utf8');
const bruJson = bruToJson(bruContent);
const request = prepareRequest(bruJson.request);
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
if(request.headers && request.headers['content-type'] === 'multipart/form-data') {
const form = new FormData();
forOwn(request.data, (value, key) => {
form.append(key, value);
});
extend(request.headers, form.getHeaders());
request.data = form;
}
const envVars = getEnvVars({});
//todo:
const collectionVariables = {};
const collectionPath = '/Users/anoop/Github/github-rest-api-collection';
if(request.script && request.script.length) {
let script = request.script + '\n if (typeof onRequest === "function") {onRequest(__brunoRequest);}';
const scriptRuntime = new ScriptRuntime();
const result = scriptRuntime.runRequestScript(script, request, envVars, collectionVariables, collectionPath);
}
const response = await axios(request);
if(request.script && request.script.length) {
let script = request.script + '\n if (typeof onResponse === "function") {onResponse(__brunoResponse);}';
const scriptRuntime = new ScriptRuntime();
const result = scriptRuntime.runResponseScript(script, response, envVars, collectionVariables, collectionPath);
}
const testFile = get(bruJson, 'request.tests');
if(testFile && testFile.length) {
const testRuntime = new TestRuntime();
const result = testRuntime.runTests(testFile, request, response, envVars, collectionVariables, collectionPath);
}
console.log(response.status);
} catch (err) {
Promise.reject(err);
}
};
module.exports = {
runSingleRequest
};

View File

@ -0,0 +1,114 @@
const path = require('path');
const fs = require('fs-extra');
const fsPromises = require('fs/promises');
const exists = async p => {
try {
await fsPromises.access(p);
return true;
} catch (_) {
return false;
}
};
const isSymbolicLink = filepath => {
try {
return fs.existsSync(filepath) && fs.lstatSync(filepath).isSymbolicLink();
} catch (_) {
return false;
}
};
const isFile = filepath => {
try {
return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile();
} catch (_) {
return false;
}
};
const isDirectory = dirPath => {
try {
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
} catch (_) {
return false;
}
};
const normalizeAndResolvePath = pathname => {
if (isSymbolicLink(pathname)) {
const absPath = path.dirname(pathname);
const targetPath = path.resolve(absPath, fs.readlinkSync(pathname));
if (isFile(targetPath) || isDirectory(targetPath)) {
return path.resolve(targetPath);
}
console.error(`Cannot resolve link target "${pathname}" (${targetPath}).`)
return '';
}
return path.resolve(pathname);
};
const writeFile = async (pathname, content) => {
try {
fs.writeFileSync(pathname, content, {
encoding: "utf8"
});
} catch (err) {
return Promise.reject(err);
}
};
const hasJsonExtension = filename => {
if (!filename || typeof filename !== 'string') return false
return ['json'].some(ext => filename.toLowerCase().endsWith(`.${ext}`))
}
const hasBruExtension = filename => {
if (!filename || typeof filename !== 'string') return false
return ['bru'].some(ext => filename.toLowerCase().endsWith(`.${ext}`))
}
const createDirectory = async (dir) => {
if(!dir) {
throw new Error(`directory: path is null`);
}
if (fs.existsSync(dir)){
throw new Error(`directory: ${dir} already exists`);
}
return fs.mkdirSync(dir);
};
const searchForFiles = (dir, extension) => {
let results = [];
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
results = results.concat(searchForFiles(filePath, extension));
} else if (path.extname(file) === extension) {
results.push(filePath);
}
}
return results;
}
const searchForBruFiles = (dir) => {
return searchForFiles(dir, '.bru');
};
module.exports = {
exists,
isSymbolicLink,
isFile,
isDirectory,
normalizeAndResolvePath,
writeFile,
hasJsonExtension,
hasBruExtension,
createDirectory,
searchForFiles,
searchForBruFiles
};