From e7d332c7d7826b9defe3e9a9fb4f369554547de9 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Sat, 4 Feb 2023 16:02:27 +0530 Subject: [PATCH] feat: bru lang - support for vars, asserts and docs --- packages/bruno-lang/v2/src/index.js | 247 ++++++++++++++++----- packages/bruno-lang/v2/tests/index.spec.js | 90 +++++++- 2 files changed, 285 insertions(+), 52 deletions(-) diff --git a/packages/bruno-lang/v2/src/index.js b/packages/bruno-lang/v2/src/index.js index ae10df89..b21459cc 100644 --- a/packages/bruno-lang/v2/src/index.js +++ b/packages/bruno-lang/v2/src/index.js @@ -1,45 +1,88 @@ 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 = (script | test | querydisabled | query | headersdisabled | headers | bodies )* + 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 - headers = "headers" pairblock - headersdisabled = "headers:disabled" pairblock - - query = "query" pairblock - querydisabled = "query:disabled" pairblock - - pairblock = st* "{" pairlist? tagend + // 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" pairblock - bodyformurlencodeddisabled = "body:form-urlencoded:disabled" pairblock - bodymultipart = "body:multipart-form" pairblock - bodymultipartdisabled = "body:multipart-form:disabled" pairblock + 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* "{" textblock tagend - - textblock = textline (~tagend nl textline)* - textline = textchar* - textchar = ~nl any + test = "test" st* "{" nl* textblock tagend + docs = "docs" st* "{" nl* textblock tagend }`); const mapPairListToKeyValPairs = (pairList = [], enabled = true) => { @@ -62,6 +105,14 @@ const concatArrays = (objValue, 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) { @@ -72,27 +123,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { return _.mergeWith(result, item, concatArrays); }, {}); }, - query(_1, pairblock) { - return { - query: mapPairListToKeyValPairs(pairblock.ast) - }; - }, - querydisabled(_1, pairblock) { - return { - query: mapPairListToKeyValPairs(pairblock.ast, false) - }; - }, - headers(_1, pairblock) { - return { - headers: mapPairListToKeyValPairs(pairblock.ast) - }; - }, - headersdisabled(_1, pairblock) { - return { - headers: mapPairListToKeyValPairs(pairblock.ast, false) - }; - }, - pairblock(_1, _2, pairlist, _3) { + dictionary(_1, _2, pairlist, _3) { return pairlist.ast; }, pairlist(_1, pair, _2, rest, _3) { @@ -109,31 +140,112 @@ const sem = grammar.createSemantics().addAttribute('ast', { value(chars) { return chars.sourceString ? chars.sourceString.trim() : ''; }, - bodyformurlencoded(_1, pairblock) { + meta(_1, dictionary) { return { - body: { - formUrlEncoded: mapPairListToKeyValPairs(pairblock.ast) + meta: mapPairListToKeyValPair(dictionary.ast) + }; + }, + get(_1, dictionary) { + return { + http: { + method: 'GET', + ...mapPairListToKeyValPair(dictionary.ast) } }; }, - bodyformurlencodeddisabled(_1, pairblock) { + post(_1, dictionary) { return { - body: { - formUrlEncoded: mapPairListToKeyValPairs(pairblock.ast, false) + http: { + method: 'POST', + ...mapPairListToKeyValPair(dictionary.ast) } }; }, - bodymultipart(_1, pairblock) { + put(_1, dictionary) { return { - body: { - multipartForm: mapPairListToKeyValPairs(pairblock.ast) + http: { + method: 'PUT', + ...mapPairListToKeyValPair(dictionary.ast) } }; }, - bodymultipartdisabled(_1, pairblock) { + 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: { - multipartForm: mapPairListToKeyValPairs(pairblock.ast, false) + 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) } }; }, @@ -176,16 +288,51 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + 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, textblock, _4) { + 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'); }, diff --git a/packages/bruno-lang/v2/tests/index.spec.js b/packages/bruno-lang/v2/tests/index.spec.js index ad9e3bfb..53ef20d6 100644 --- a/packages/bruno-lang/v2/tests/index.spec.js +++ b/packages/bruno-lang/v2/tests/index.spec.js @@ -3,6 +3,17 @@ const parser = require("../src/index"); describe("parser", () => { it("should parse the bru file", () => { const input = ` +meta { + name: Send Bulk SMS + type: http + seq: 1 +} + +get { + url: https://api.textlocal.in/send/ + body: json +} + query { apiKey: secret numbers: 998877665 @@ -73,15 +84,53 @@ body:graphql:vars { } } -script { +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. +} `; const output = parser(input); const expected = { + "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", @@ -155,7 +204,44 @@ script { } ] }, - "script": " function onResponse(request, response) {\n expect(response.status).to.equal(200);\n }" + "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." } // console.log(JSON.stringify(output, null, 2));