diff --git a/packages/bruno-lang/package.json b/packages/bruno-lang/package.json
index c243c7980..632aed4c5 100644
--- a/packages/bruno-lang/package.json
+++ b/packages/bruno-lang/package.json
@@ -10,6 +10,7 @@
"test": "jest"
},
"dependencies": {
- "arcsecond": "^5.0.0"
+ "arcsecond": "^5.0.0",
+ "ohm-js": "^16.6.0"
}
}
diff --git a/packages/bruno-lang/v2/src/index.js b/packages/bruno-lang/v2/src/index.js
new file mode 100644
index 000000000..b21459cc3
--- /dev/null
+++ b/packages/bruno-lang/v2/src/index.js
@@ -0,0 +1,369 @@
+const ohm = require("ohm-js");
+const _ = require('lodash');
+
+/**
+ * A Bru file is made up of blocks.
+ * There are two types of blocks
+ *
+ * 1. Dictionary Blocks - These are blocks that have key value pairs
+ * ex:
+ * headers {
+ * content-type: application/json
+ * }
+ *
+ * 2. Text Blocks - These are blocks that have text
+ * ex:
+ * body:json {
+ * {
+ * "username": "John Nash",
+ * "password": "governingdynamics
+ * }
+ *
+ */
+const grammar = ohm.grammar(`Bru {
+ BruFile = (meta | http | querydisabled | query | headersdisabled | headers | bodies | varsandassert | script | test | docs)*
+ bodies = bodyjson | bodytext | bodyxml | bodygraphql | bodygraphqlvars | bodyforms
+ bodyforms = bodyformurlencodeddisabled | bodyformurlencoded | bodymultipartdisabled | bodymultipart
+
+ nl = "\\r"? "\\n"
+ st = " " | "\\t"
+ tagend = nl "}"
+ validkey = ~(st | ":") any
+ validvalue = ~nl any
+
+ // Dictionary Blocks
+ dictionary = st* "{" pairlist? tagend
+ pairlist = nl* pair (~tagend nl pair)* (~tagend space)*
+ pair = st* key st* ":" st* value? st*
+ key = ~tagend validkey*
+ value = ~tagend validvalue*
+
+ // Text Blocks
+ textblock = textline (~tagend nl textline)*
+ textline = textchar*
+ textchar = ~nl any
+
+ meta = "meta" dictionary
+
+ http = get | post | put | delete | options | head | connect | trace
+ get = "get" dictionary
+ post = "post" dictionary
+ put = "put" dictionary
+ delete = "delete" dictionary
+ options = "options" dictionary
+ head = "head" dictionary
+ connect = "connect" dictionary
+ trace = "trace" dictionary
+
+ headers = "headers" dictionary
+ headersdisabled = "headers:disabled" dictionary
+
+ query = "query" dictionary
+ querydisabled = "query:disabled" dictionary
+
+ varsandassert = vars | varsdisabled | varslocal | varslocaldisabled | assert | assertdisabled
+ vars = "vars" dictionary
+ varsdisabled = "vars:disabled" dictionary
+ varslocal = "vars:local" dictionary
+ varslocaldisabled = "vars:local:disabled" dictionary
+ assert = "assert" dictionary
+ assertdisabled = "assert:disabled" dictionary
+
+ bodyjson = "body:json" st* "{" nl* textblock tagend
+ bodytext = "body:text" st* "{" nl* textblock tagend
+ bodyxml = "body:xml" st* "{" nl* textblock tagend
+ bodygraphql = "body:graphql" st* "{" nl* textblock tagend
+ bodygraphqlvars = "body:graphql:vars" st* "{" nl* textblock tagend
+
+ bodyformurlencoded = "body:form-urlencoded" dictionary
+ bodyformurlencodeddisabled = "body:form-urlencoded:disabled" dictionary
+ bodymultipart = "body:multipart-form" dictionary
+ bodymultipartdisabled = "body:multipart-form:disabled" dictionary
+
+ script = "script" st* "{" nl* textblock tagend
+ test = "test" st* "{" nl* textblock tagend
+ docs = "docs" st* "{" nl* textblock tagend
+}`);
+
+const mapPairListToKeyValPairs = (pairList = [], enabled = true) => {
+ if(!pairList.length) {
+ return [];
+ }
+ return _.map(pairList[0], pair => {
+ const key = _.keys(pair)[0];
+ return {
+ name: key,
+ value: pair[key],
+ enabled: 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] = _.get(value, 'ast[0]', '');
+ return res;
+ },
+ key(chars) {
+ return chars.sourceString;
+ },
+ value(chars) {
+ return chars.sourceString ? chars.sourceString.trim() : '';
+ },
+ meta(_1, dictionary) {
+ return {
+ meta: mapPairListToKeyValPair(dictionary.ast)
+ };
+ },
+ get(_1, dictionary) {
+ return {
+ http: {
+ method: 'GET',
+ ...mapPairListToKeyValPair(dictionary.ast)
+ }
+ };
+ },
+ post(_1, dictionary) {
+ return {
+ http: {
+ method: 'POST',
+ ...mapPairListToKeyValPair(dictionary.ast)
+ }
+ };
+ },
+ put(_1, dictionary) {
+ return {
+ http: {
+ method: 'PUT',
+ ...mapPairListToKeyValPair(dictionary.ast)
+ }
+ };
+ },
+ delete(_1, dictionary) {
+ return {
+ http: {
+ method: 'DELETE',
+ ...mapPairListToKeyValPair(dictionary.ast)
+ }
+ };
+ },
+ options(_1, dictionary) {
+ return {
+ http: {
+ method: 'OPTIONS',
+ ...mapPairListToKeyValPair(dictionary.ast)
+ }
+ };
+ },
+ head(_1, dictionary) {
+ return {
+ http: {
+ method: 'HEAD',
+ ...mapPairListToKeyValPair(dictionary.ast)
+ }
+ };
+ },
+ connect(_1, dictionary) {
+ return {
+ http: {
+ method: 'CONNECT',
+ ...mapPairListToKeyValPair(dictionary.ast)
+ }
+ };
+ },
+ query(_1, dictionary) {
+ return {
+ query: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ querydisabled(_1, dictionary) {
+ return {
+ query: mapPairListToKeyValPairs(dictionary.ast, false)
+ };
+ },
+ headers(_1, dictionary) {
+ return {
+ headers: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ headersdisabled(_1, dictionary) {
+ return {
+ headers: mapPairListToKeyValPairs(dictionary.ast, false)
+ };
+ },
+ bodyformurlencoded(_1, dictionary) {
+ return {
+ body: {
+ formUrlEncoded: mapPairListToKeyValPairs(dictionary.ast)
+ }
+ };
+ },
+ bodyformurlencodeddisabled(_1, dictionary) {
+ return {
+ body: {
+ formUrlEncoded: mapPairListToKeyValPairs(dictionary.ast, false)
+ }
+ };
+ },
+ bodymultipart(_1, dictionary) {
+ return {
+ body: {
+ multipartForm: mapPairListToKeyValPairs(dictionary.ast)
+ }
+ };
+ },
+ bodymultipartdisabled(_1, dictionary) {
+ return {
+ body: {
+ multipartForm: mapPairListToKeyValPairs(dictionary.ast, false)
+ }
+ };
+ },
+ bodyjson(_1, _2, _3, _4, textblock, _5) {
+ return {
+ body: {
+ json: textblock.sourceString
+ }
+ };
+ },
+ bodytext(_1, _2, _3, _4, textblock, _5) {
+ return {
+ body: {
+ text: textblock.sourceString
+ }
+ };
+ },
+ bodyxml(_1, _2, _3, _4, textblock, _5) {
+ return {
+ body: {
+ xml: textblock.sourceString
+ }
+ };
+ },
+ bodygraphql(_1, _2, _3, _4, textblock, _5) {
+ return {
+ body: {
+ graphql: {
+ query: textblock.sourceString
+ }
+ }
+ };
+ },
+ bodygraphqlvars(_1, _2, _3, _4, textblock, _5) {
+ return {
+ body: {
+ graphql: {
+ variables: textblock.sourceString
+ }
+ }
+ };
+ },
+ vars(_1, dictionary) {
+ return {
+ vars: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ varsdisabled(_1, dictionary) {
+ return {
+ vars: mapPairListToKeyValPairs(dictionary.ast, false)
+ };
+ },
+ varslocal(_1, dictionary) {
+ return {
+ varsLocal: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ varslocaldisabled(_1, dictionary) {
+ return {
+ varsLocal: mapPairListToKeyValPairs(dictionary.ast, false)
+ };
+ },
+ assert(_1, dictionary) {
+ return {
+ assert: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ assertdisabled(_1, dictionary) {
+ return {
+ assert: mapPairListToKeyValPairs(dictionary.ast, false)
+ };
+ },
+ script(_1, _2, _3, _4, textblock, _5) {
+ return {
+ script: textblock.sourceString
+ };
+ },
+ test(_1, _2, _3, _4, textblock, _5) {
+ return {
+ test: textblock.sourceString
+ };;
+ },
+ docs(_1, _2, _3, _4, textblock, _5) {
+ return {
+ docs: textblock.sourceString
+ };
+ },
+ 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);
+ }
+});
+
+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/tests/dictionary.spec.js b/packages/bruno-lang/v2/tests/dictionary.spec.js
new file mode 100644
index 000000000..46d5b31f7
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/dictionary.spec.js
@@ -0,0 +1,138 @@
+/**
+ * This test file is used to test the text parser.
+ */
+
+const parser = require("../src/index");
+
+const assertSingleHeader = (input) => {
+ const output = parser(input);
+
+ const expected = {
+ "headers": [{
+ "name": "hello",
+ "value": "world",
+ "enabled": true
+ }]
+ };
+ expect(output).toEqual(expected);
+};
+
+describe("headers parser", () => {
+ it("should parse empty header", () => {
+ const input = `
+headers {
+}`;
+
+ const output = parser(input);
+ const expected = {
+ "headers": []
+ };
+ expect(output).toEqual(expected);
+ });
+
+ it("should parse single header", () => {
+ const input = `
+headers {
+ hello: world
+}`;
+
+ assertSingleHeader(input);
+ });
+
+ it("should parse single header with spaces", () => {
+ const input = `
+headers {
+ hello: world
+}`;
+
+ assertSingleHeader(input);
+ });
+
+ it("should parse single header with spaces and newlines", () => {
+ const input = `
+headers {
+
+ hello: world
+
+
+}`;
+
+ assertSingleHeader(input);
+ });
+
+ it("should parse single header with empty value", () => {
+ const input = `
+headers {
+ hello:
+}`;
+
+ const output = parser(input);
+ const expected = {
+ "headers": [{
+ "name": "hello",
+ "value": "",
+ "enabled": true
+ }]
+ };
+ expect(output).toEqual(expected);
+ });
+
+ it("should parse multi headers", () => {
+ const input = `
+headers {
+ content-type: application/json
+ Authorization: JWT secret
+}`;
+
+ const output = parser(input);
+ const expected = {
+ "headers": [{
+ "name": "content-type",
+ "value": "application/json",
+ "enabled": true
+ }, {
+ "name": "Authorization",
+ "value": "JWT secret",
+ "enabled": true
+ }]
+ };
+ expect(output).toEqual(expected);
+ });
+
+ it("should parse disabled headers", () => {
+ const input = `
+headers:disabled {
+ content-type: application/json
+}`;
+
+ const output = parser(input);
+ const expected = {
+ "headers": [{
+ "name": "content-type",
+ "value": "application/json",
+ "enabled": false
+ }]
+ };
+ expect(output).toEqual(expected);
+ });
+
+ it("should throw error on invalid header", () => {
+ const input = `
+headers {
+ hello: world
+ foo
+}`;
+
+ expect(() => parser(input)).toThrow();
+ });
+
+ it("should throw error on invalid header", () => {
+ const input = `
+headers {
+ hello: world
+ foo: bar}`;
+
+ expect(() => parser(input)).toThrow();
+ });
+});
+
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru
new file mode 100644
index 000000000..9db94d4dd
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/fixtures/request.bru
@@ -0,0 +1,114 @@
+meta {
+ name: Send Bulk SMS
+ type: http
+ seq: 1
+}
+
+get {
+ url: https://api.textlocal.in/send/
+ body: json
+}
+
+query {
+ apiKey: secret
+ numbers: 998877665
+}
+
+query:disabled {
+ message: hello
+}
+
+headers {
+ content-type: application/json
+ Authorization: Bearer 123
+}
+
+headers:disabled {
+ transaction-id: {{transactionId}}
+}
+
+body:form-urlencoded {
+ apikey: secret
+ numbers: +91998877665
+}
+
+body:form-urlencoded:disabled {
+ message: hello
+}
+
+body:multipart-form {
+ apikey: secret
+ numbers: +91998877665
+}
+
+body:multipart-form:disabled {
+ message: hello
+}
+
+body:json {
+ {
+ "hello": "world"
+ }
+}
+
+body:text {
+ This is a text body
+}
+
+body:xml {
+
+ John
+ 30
+
+}
+
+body:graphql {
+ {
+ launchesPast {
+ launch_site {
+ site_name
+ }
+ launch_success
+ }
+ }
+}
+
+body:graphql:vars {
+ {
+ "limit": 5
+ }
+}
+
+vars {
+ token: $res.body.token
+}
+
+vars:disabled {
+ petId: $res.body.id
+}
+
+vars:local {
+ orderNumber: $res.body.orderNumber
+}
+
+vars:local:disabled {
+ transactionId: $res.body.transactionId
+}
+
+assert {
+ $res.status: 200
+}
+
+assert:disabled {
+ $res.body.message: success
+}
+
+test {
+ function onResponse(request, response) {
+ expect(response.status).to.equal(200);
+ }
+}
+
+docs {
+ This request needs auth token to be set in the headers.
+}
\ No newline at end of file
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json
new file mode 100644
index 000000000..167af5f60
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/fixtures/request.json
@@ -0,0 +1,123 @@
+{
+ "meta": {
+ "name": "Send Bulk SMS",
+ "type": "http",
+ "seq": "1"
+ },
+ "http": {
+ "method": "GET",
+ "url": "https://api.textlocal.in/send/",
+ "body": "json"
+ },
+ "query": [{
+ "name": "apiKey",
+ "value": "secret",
+ "enabled": true
+ }, {
+ "name": "numbers",
+ "value": "998877665",
+ "enabled": true
+ }, {
+ "name": "message",
+ "value": "hello",
+ "enabled": false
+ }],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/json",
+ "enabled": true
+ },
+ {
+ "name": "Authorization",
+ "value": "Bearer 123",
+ "enabled": true
+ },
+ {
+ "name": "transaction-id",
+ "value": "{{transactionId}}",
+ "enabled": false
+ }
+ ],
+ "body": {
+ "json": " {\n \"hello\": \"world\"\n }",
+ "text": " This is a text body",
+ "xml": " \n John\n 30\n ",
+ "graphql": {
+ "query": " {\n launchesPast {\n launch_site {\n site_name\n }\n launch_success\n }\n }",
+ "variables": " {\n \"limit\": 5\n }"
+ },
+ "formUrlEncoded": [
+ {
+ "name": "apikey",
+ "value": "secret",
+ "enabled": true
+ },
+ {
+ "name": "numbers",
+ "value": "+91998877665",
+ "enabled": true
+ },
+ {
+ "name": "message",
+ "value": "hello",
+ "enabled": false
+ }
+ ],
+ "multipartForm": [
+ {
+ "name": "apikey",
+ "value": "secret",
+ "enabled": true
+ },
+ {
+ "name": "numbers",
+ "value": "+91998877665",
+ "enabled": true
+ },
+ {
+ "name": "message",
+ "value": "hello",
+ "enabled": false
+ }
+ ]
+ },
+ "vars": [
+ {
+ "name": "token",
+ "value": "$res.body.token",
+ "enabled": true
+ },
+ {
+ "name": "petId",
+ "value": "$res.body.id",
+ "enabled": false
+ }
+ ],
+ "varsLocal": [
+ {
+ "name": "orderNumber",
+ "value": "$res.body.orderNumber",
+ "enabled": true
+ },
+ {
+ "name": "transactionId",
+ "value": "$res.body.transactionId",
+ "enabled": false
+ }
+ ],
+ "assert": [
+ {
+ "name": "$res.status",
+ "value": "200",
+ "enabled": true
+ },
+ {
+ "name": "$res.body.message",
+ "value": "success",
+ "enabled": false
+ }
+ ],
+ "test": " function onResponse(request, response) {\n expect(response.status).to.equal(200);\n }",
+ "docs": " This request needs auth token to be set in the headers."
+}
\ No newline at end of file
diff --git a/packages/bruno-lang/v2/tests/index.spec.js b/packages/bruno-lang/v2/tests/index.spec.js
new file mode 100644
index 000000000..c33cd7630
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/index.spec.js
@@ -0,0 +1,14 @@
+const fs = require("fs");
+const path = require("path");
+const parser = require("../src/index");
+
+describe("parser", () => {
+ it("should parse the bru file", () => {
+ const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'request.bru'), 'utf8');
+ const expected = require("./fixtures/request.json");
+ const output = parser(input);
+
+ // console.log(JSON.stringify(output, null, 2));
+ expect(output).toEqual(expected);
+ });
+});
diff --git a/packages/bruno-lang/v2/tests/text.spec.js b/packages/bruno-lang/v2/tests/text.spec.js
new file mode 100644
index 000000000..caefa0874
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/text.spec.js
@@ -0,0 +1,20 @@
+/**
+ * This test file is used to test the text parser.
+ */
+const parser = require("../src/index");
+
+describe("script parser", () => {
+ it("should parse script body", () => {
+ const input = `
+script {
+ function onResponse(request, response) {
+ expect(response.status).to.equal(200);
+ }
+}
+`;
+
+ const output = parser(input);
+ const expected = " function onResponse(request, response) {\n expect(response.status).to.equal(200);\n }";
+ expect(output.script).toEqual(expected);
+ });
+});