diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index e9cdb48f2..5147bb984 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -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" } diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 4e63f204c..b8a742943 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -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 '; 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 }; diff --git a/packages/bruno-cli/src/index.js b/packages/bruno-cli/src/index.js index 875f166ec..cf9fa23ee 100644 --- a/packages/bruno-cli/src/index.js +++ b/packages/bruno-cli/src/index.js @@ -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}`)); } diff --git a/packages/bruno-cli/src/runner/bru/index.js b/packages/bruno-cli/src/runner/bru/index.js new file mode 100644 index 000000000..384a8f382 --- /dev/null +++ b/packages/bruno-cli/src/runner/bru/index.js @@ -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 +}; diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js new file mode 100644 index 000000000..6eacb3c1d --- /dev/null +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -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; diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js new file mode 100644 index 000000000..bcc388d9c --- /dev/null +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -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; diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js new file mode 100644 index 000000000..5e62bf623 --- /dev/null +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -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 +}; diff --git a/packages/bruno-cli/src/utils/filesystem.js b/packages/bruno-cli/src/utils/filesystem.js new file mode 100644 index 000000000..10657b397 --- /dev/null +++ b/packages/bruno-cli/src/utils/filesystem.js @@ -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 +};