From 856236c918f7c02c983316c508a00f37e8450a5e Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Sun, 8 Oct 2023 01:57:30 +0530 Subject: [PATCH] feat(#334): Bru lang updates for collection.bru file --- .../bruno-lang/v2/src/collectionBruToJson.js | 273 ++++++++++++++++++ .../bruno-lang/v2/src/jsonToCollectionBru.js | 185 ++++++++++++ .../bruno-lang/v2/tests/collection.spec.js | 24 ++ .../v2/tests/fixtures/collection.bru | 43 +++ .../v2/tests/fixtures/collection.json | 61 ++++ 5 files changed, 586 insertions(+) create mode 100644 packages/bruno-lang/v2/src/collectionBruToJson.js create mode 100644 packages/bruno-lang/v2/src/jsonToCollectionBru.js create mode 100644 packages/bruno-lang/v2/tests/collection.spec.js create mode 100644 packages/bruno-lang/v2/tests/fixtures/collection.bru create mode 100644 packages/bruno-lang/v2/tests/fixtures/collection.json diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js new file mode 100644 index 000000000..d78f752c0 --- /dev/null +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -0,0 +1,273 @@ +const ohm = require('ohm-js'); +const _ = require('lodash'); +const { outdentString } = require('../../v1/src/utils'); + +const grammar = ohm.grammar(`Bru { + BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)* + auths = authbasic | authbearer + + nl = "\\r"? "\\n" + st = " " | "\\t" + stnl = st | nl + tagend = nl "}" + optionalnl = ~tagend nl + keychar = ~(tagend | st | nl | ":") any + valuechar = ~(nl | tagend) any + + // Dictionary Blocks + dictionary = st* "{" pairlist? tagend + pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)* + pair = st* key st* ":" st* value st* + key = keychar* + value = valuechar* + + // Text Blocks + textblock = textline (~tagend nl textline)* + textline = textchar* + textchar = ~nl any + + meta = "meta" dictionary + + auth = "auth" dictionary + + headers = "headers" dictionary + + query = "query" dictionary + + vars = varsreq | varsres + varsreq = "vars:pre-request" dictionary + varsres = "vars:post-response" dictionary + + authbasic = "auth:basic" dictionary + authbearer = "auth:bearer" dictionary + + script = scriptreq | scriptres + scriptreq = "script:pre-request" st* "{" nl* textblock tagend + scriptres = "script:post-response" st* "{" nl* textblock tagend + tests = "tests" st* "{" nl* textblock tagend + docs = "docs" st* "{" nl* textblock tagend +}`); + +const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => { + if (!pairList.length) { + return []; + } + return _.map(pairList[0], (pair) => { + let name = _.keys(pair)[0]; + let value = pair[name]; + + if (!parseEnabled) { + return { + name, + value + }; + } + + let enabled = true; + if (name && name.length && name.charAt(0) === '~') { + name = name.slice(1); + enabled = false; + } + + return { + name, + value, + enabled + }; + }); +}; + +const concatArrays = (objValue, srcValue) => { + if (_.isArray(objValue) && _.isArray(srcValue)) { + return objValue.concat(srcValue); + } +}; + +const mapPairListToKeyValPair = (pairList = []) => { + if (!pairList || !pairList.length) { + return {}; + } + + return _.merge({}, ...pairList[0]); +}; + +const sem = grammar.createSemantics().addAttribute('ast', { + BruFile(tags) { + if (!tags || !tags.ast || !tags.ast.length) { + return {}; + } + + return _.reduce( + tags.ast, + (result, item) => { + return _.mergeWith(result, item, concatArrays); + }, + {} + ); + }, + dictionary(_1, _2, pairlist, _3) { + return pairlist.ast; + }, + pairlist(_1, pair, _2, rest, _3) { + return [pair.ast, ...rest.ast]; + }, + pair(_1, key, _2, _3, _4, value, _5) { + let res = {}; + res[key.ast] = value.ast ? value.ast.trim() : ''; + return res; + }, + key(chars) { + return chars.sourceString ? chars.sourceString.trim() : ''; + }, + value(chars) { + return chars.sourceString ? chars.sourceString.trim() : ''; + }, + textblock(line, _1, rest) { + return [line.ast, ...rest.ast].join('\n'); + }, + textline(chars) { + return chars.sourceString; + }, + textchar(char) { + return char.sourceString; + }, + nl(_1, _2) { + return ''; + }, + st(_) { + return ''; + }, + tagend(_1, _2) { + return ''; + }, + _iter(...elements) { + return elements.map((e) => e.ast); + }, + meta(_1, dictionary) { + let meta = mapPairListToKeyValPair(dictionary.ast) || {}; + + meta.type = 'collection'; + + return { + meta + }; + }, + auth(_1, dictionary) { + let auth = mapPairListToKeyValPair(dictionary.ast) || {}; + + return { + auth: { + mode: auth ? auth.mode : 'none' + } + }; + }, + query(_1, dictionary) { + return { + query: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + headers(_1, dictionary) { + return { + headers: mapPairListToKeyValPairs(dictionary.ast) + }; + }, + authbasic(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const usernameKey = _.find(auth, { name: 'username' }); + const passwordKey = _.find(auth, { name: 'password' }); + const username = usernameKey ? usernameKey.value : ''; + const password = passwordKey ? passwordKey.value : ''; + return { + auth: { + basic: { + username, + password + } + } + }; + }, + authbearer(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const tokenKey = _.find(auth, { name: 'token' }); + const token = tokenKey ? tokenKey.value : ''; + return { + auth: { + bearer: { + token + } + } + }; + }, + varsreq(_1, dictionary) { + const vars = mapPairListToKeyValPairs(dictionary.ast); + _.each(vars, (v) => { + let name = v.name; + if (name && name.length && name.charAt(0) === '@') { + v.name = name.slice(1); + v.local = true; + } else { + v.local = false; + } + }); + + return { + vars: { + req: vars + } + }; + }, + varsres(_1, dictionary) { + const vars = mapPairListToKeyValPairs(dictionary.ast); + _.each(vars, (v) => { + let name = v.name; + if (name && name.length && name.charAt(0) === '@') { + v.name = name.slice(1); + v.local = true; + } else { + v.local = false; + } + }); + + return { + vars: { + res: vars + } + }; + }, + scriptreq(_1, _2, _3, _4, textblock, _5) { + return { + script: { + req: outdentString(textblock.sourceString) + } + }; + }, + scriptres(_1, _2, _3, _4, textblock, _5) { + return { + script: { + res: outdentString(textblock.sourceString) + } + }; + }, + tests(_1, _2, _3, _4, textblock, _5) { + return { + tests: outdentString(textblock.sourceString) + }; + }, + docs(_1, _2, _3, _4, textblock, _5) { + return { + docs: outdentString(textblock.sourceString) + }; + } +}); + +const parser = (input) => { + const match = grammar.match(input); + + if (match.succeeded()) { + return sem(match).ast; + } else { + throw new Error(match.message); + } +}; + +module.exports = parser; diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js new file mode 100644 index 000000000..f20fbc680 --- /dev/null +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -0,0 +1,185 @@ +const _ = require('lodash'); + +const { indentString } = require('../../v1/src/utils'); + +const enabled = (items = []) => items.filter((item) => item.enabled); +const disabled = (items = []) => items.filter((item) => !item.enabled); + +// remove the last line if two new lines are found +const stripLastLine = (text) => { + if (!text || !text.length) return text; + + return text.replace(/(\r?\n)$/, ''); +}; + +const jsonToBru = (json) => { + const { meta, query, headers, auth, script, tests, vars, docs } = json; + + let bru = ''; + + if (meta) { + bru += 'meta {\n'; + for (const key in meta) { + bru += ` ${key}: ${meta[key]}\n`; + } + bru += '}\n\n'; + } + + if (query && query.length) { + bru += 'query {'; + if (enabled(query).length) { + bru += `\n${indentString( + enabled(query) + .map((item) => `${item.name}: ${item.value}`) + .join('\n') + )}`; + } + + if (disabled(query).length) { + bru += `\n${indentString( + disabled(query) + .map((item) => `~${item.name}: ${item.value}`) + .join('\n') + )}`; + } + + bru += '\n}\n\n'; + } + + if (headers && headers.length) { + bru += 'headers {'; + if (enabled(headers).length) { + bru += `\n${indentString( + enabled(headers) + .map((item) => `${item.name}: ${item.value}`) + .join('\n') + )}`; + } + + if (disabled(headers).length) { + bru += `\n${indentString( + disabled(headers) + .map((item) => `~${item.name}: ${item.value}`) + .join('\n') + )}`; + } + + bru += '\n}\n\n'; + } + + if (auth && auth.mode) { + bru += `auth { +${indentString(`mode: ${auth.mode}`)} +} + +`; + } + + if (auth && auth.basic) { + bru += `auth:basic { +${indentString(`username: ${auth.basic.username}`)} +${indentString(`password: ${auth.basic.password}`)} +} + +`; + } + + if (auth && auth.bearer) { + bru += `auth:bearer { +${indentString(`token: ${auth.bearer.token}`)} +} + +`; + } + + let reqvars = _.get(vars, 'req'); + let resvars = _.get(vars, 'res'); + if (reqvars && reqvars.length) { + const varsEnabled = _.filter(reqvars, (v) => v.enabled && !v.local); + const varsDisabled = _.filter(reqvars, (v) => !v.enabled && !v.local); + const varsLocalEnabled = _.filter(reqvars, (v) => v.enabled && v.local); + const varsLocalDisabled = _.filter(reqvars, (v) => !v.enabled && v.local); + + bru += `vars:pre-request {`; + + if (varsEnabled.length) { + bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`; + } + + if (varsLocalEnabled.length) { + bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`; + } + + if (varsDisabled.length) { + bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`; + } + + if (varsLocalDisabled.length) { + bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`; + } + + bru += '\n}\n\n'; + } + if (resvars && resvars.length) { + const varsEnabled = _.filter(resvars, (v) => v.enabled && !v.local); + const varsDisabled = _.filter(resvars, (v) => !v.enabled && !v.local); + const varsLocalEnabled = _.filter(resvars, (v) => v.enabled && v.local); + const varsLocalDisabled = _.filter(resvars, (v) => !v.enabled && v.local); + + bru += `vars:post-response {`; + + if (varsEnabled.length) { + bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`; + } + + if (varsLocalEnabled.length) { + bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`; + } + + if (varsDisabled.length) { + bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`; + } + + if (varsLocalDisabled.length) { + bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`; + } + + bru += '\n}\n\n'; + } + + if (script && script.req && script.req.length) { + bru += `script:pre-request { +${indentString(script.req)} +} + +`; + } + + if (script && script.res && script.res.length) { + bru += `script:post-response { +${indentString(script.res)} +} + +`; + } + + if (tests && tests.length) { + bru += `tests { +${indentString(tests)} +} + +`; + } + + if (docs && docs.length) { + bru += `docs { +${indentString(docs)} +} + +`; + } + + return stripLastLine(bru); +}; + +module.exports = jsonToBru; diff --git a/packages/bruno-lang/v2/tests/collection.spec.js b/packages/bruno-lang/v2/tests/collection.spec.js new file mode 100644 index 000000000..4bdb7f9dc --- /dev/null +++ b/packages/bruno-lang/v2/tests/collection.spec.js @@ -0,0 +1,24 @@ +const fs = require('fs'); +const path = require('path'); +const collectionBruToJson = require('../src/collectionBruToJson'); +const jsonToCollectionBru = require('../src/jsonToCollectionBru'); + +describe('collectionBruToJson', () => { + it('should parse the collection bru file', () => { + const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'collection.bru'), 'utf8'); + const expected = require('./fixtures/collection.json'); + const output = collectionBruToJson(input); + + expect(output).toEqual(expected); + }); +}); + +describe('jsonToCollectionBru', () => { + it('should convert the collection json to bru', () => { + const input = require('./fixtures/collection.json'); + const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'collection.bru'), 'utf8'); + const output = jsonToCollectionBru(input); + + expect(output).toEqual(expected); + }); +}); diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.bru b/packages/bruno-lang/v2/tests/fixtures/collection.bru new file mode 100644 index 000000000..a02be30cb --- /dev/null +++ b/packages/bruno-lang/v2/tests/fixtures/collection.bru @@ -0,0 +1,43 @@ +meta { + type: collection +} + +headers { + content-type: application/json + Authorization: Bearer 123 + ~transaction-id: {{transactionId}} +} + +auth { + mode: none +} + +auth:basic { + username: john + password: secret +} + +auth:bearer { + token: 123 +} + +vars:pre-request { + departingDate: 2020-01-01 + ~returningDate: 2020-01-02 +} + +vars:post-response { + ~transactionId: $res.body.transactionId +} + +script:pre-request { + console.log("In Collection pre Request Script"); +} + +script:post-response { + console.log("In Collection post Request Script"); +} + +docs { + This request needs auth token to be set in the headers. +} diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.json b/packages/bruno-lang/v2/tests/fixtures/collection.json new file mode 100644 index 000000000..de827d11e --- /dev/null +++ b/packages/bruno-lang/v2/tests/fixtures/collection.json @@ -0,0 +1,61 @@ +{ + "meta": { + "type": "collection" + }, + "headers": [ + { + "name": "content-type", + "value": "application/json", + "enabled": true + }, + { + "name": "Authorization", + "value": "Bearer 123", + "enabled": true + }, + { + "name": "transaction-id", + "value": "{{transactionId}}", + "enabled": false + } + ], + "auth": { + "mode": "none", + "basic": { + "username": "john", + "password": "secret" + }, + "bearer": { + "token": "123" + } + }, + "vars": { + "req": [ + { + "name": "departingDate", + "value": "2020-01-01", + "enabled": true, + "local": false + }, + { + "name": "returningDate", + "value": "2020-01-02", + "enabled": false, + "local": false + } + ], + "res": [ + { + "name": "transactionId", + "value": "$res.body.transactionId", + "enabled": false, + "local": false + } + ] + }, + "script": { + "req": "console.log(\"In Collection pre Request Script\");", + "res": "console.log(\"In Collection post Request Script\");" + }, + "docs": "This request needs auth token to be set in the headers." +}