mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
feat: basic annotation syntax support for lang (#7609)
* chore: basic annotation support * chore: string and escape cases * feat: add basic multiline support * fix: simplify dedentation logic in annotation multiline text block * chore: fix asserts * feat: add annotation support to env and collection * feat: enhance annotation parsing with support for quoted arguments and nested parentheses * refactor: feedback, remove inline annotations * feat: move serializeAnnotations function to utils and update imports
This commit is contained in:
@@ -3,6 +3,10 @@ const _ = require('lodash');
|
||||
const { safeParseJson, outdentString } = require('./utils');
|
||||
const parseExample = require('./example/bruToJson');
|
||||
|
||||
// this is done to avoid breaking existing pairlist mapping so
|
||||
// the key is hidden and not added into the json automatically
|
||||
const ANNOTATIONS_KEY = Symbol('annotations');
|
||||
|
||||
/**
|
||||
* A Bru file is made up of blocks.
|
||||
* There are three types of blocks
|
||||
@@ -55,10 +59,27 @@ const grammar = ohm.grammar(`Bru {
|
||||
multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter st* contenttypeannotation?
|
||||
contenttypeannotation = "@contentType(" (~")" any)* ")"
|
||||
|
||||
// Annotation support (decorators on pairs)
|
||||
annotationname = annotationchar+
|
||||
annotationchar = ~("(" | ")" | " " | "\\t" | "\\r" | "\\n" | ":") any
|
||||
annotationsinglequotedargchar = ~"'" any
|
||||
annotationsinglequotedarg = "'" annotationsinglequotedargchar* "'"
|
||||
annotationdoublequotedargchar = ~"\\"" any
|
||||
annotationdoublequotedarg = "\\"" annotationdoublequotedargchar* "\\""
|
||||
annotationunquotedargchar = ~")" any
|
||||
annotationunquotedarg = annotationunquotedargchar*
|
||||
annotationargvalue = annotationsinglequotedarg | annotationdoublequotedarg | annotationunquotedarg
|
||||
annotationmultilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter
|
||||
annotationargscontents = annotationmultilinetextblock | annotationargvalue
|
||||
annotationargs = "(" annotationargscontents ")"
|
||||
annotation = "@" annotationname annotationargs?
|
||||
annotationentry = st* annotation ~":" st* nl
|
||||
pairannotations = annotationentry*
|
||||
|
||||
// Dictionary Blocks
|
||||
dictionary = st* "{" pairlist? tagend
|
||||
dictionary = st* "{" st* pairlist? tagend
|
||||
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
|
||||
pair = st* (quoted_key | key) st* ":" st* value st*
|
||||
pair = st* pairannotations st* (quoted_key | key) st* ":" st* value st*
|
||||
disable_char = "~"
|
||||
quote_char = "\\""
|
||||
esc_char = "\\\\"
|
||||
@@ -72,7 +93,7 @@ const grammar = ohm.grammar(`Bru {
|
||||
// Dictionary for Assert Block
|
||||
assertdictionary = st* "{" assertpairlist? tagend
|
||||
assertpairlist = optionalnl* assertpair (~tagend stnl* assertpair)* (~tagend space)*
|
||||
assertpair = st* assertkey st* ":" st* value st*
|
||||
assertpair = st* pairannotations st* assertkey st* ":" st* value st*
|
||||
assertkey = ~tagend assertkeychar*
|
||||
assertkeychar = ~(tagend | nl | ":") any
|
||||
|
||||
@@ -168,12 +189,12 @@ const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
|
||||
return _.map(pairList[0], (pair) => {
|
||||
let name = _.keys(pair)[0];
|
||||
let value = pair[name];
|
||||
const rawAnnotations = pair[ANNOTATIONS_KEY];
|
||||
|
||||
if (!parseEnabled) {
|
||||
return {
|
||||
name,
|
||||
value
|
||||
};
|
||||
const result = { name, value };
|
||||
if (rawAnnotations && rawAnnotations.length) result.annotations = rawAnnotations;
|
||||
return result;
|
||||
}
|
||||
|
||||
let enabled = true;
|
||||
@@ -182,11 +203,9 @@ const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
enabled
|
||||
};
|
||||
const result = { name, value, enabled };
|
||||
if (rawAnnotations && rawAnnotations.length) result.annotations = rawAnnotations;
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -197,18 +216,16 @@ const mapRequestParams = (pairList = [], type) => {
|
||||
return _.map(pairList[0], (pair) => {
|
||||
let name = _.keys(pair)[0];
|
||||
let value = pair[name];
|
||||
const rawAnnotations = pair[ANNOTATIONS_KEY];
|
||||
let enabled = true;
|
||||
if (name && name.length && name.charAt(0) === '~') {
|
||||
name = name.slice(1);
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
enabled,
|
||||
type
|
||||
};
|
||||
const result = { name, value, enabled, type };
|
||||
if (rawAnnotations && rawAnnotations.length) result.annotations = rawAnnotations;
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -346,19 +363,67 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
{}
|
||||
);
|
||||
},
|
||||
dictionary(_1, _2, pairlist, _3) {
|
||||
dictionary(_1, _2, _3, pairlist, _4) {
|
||||
return pairlist.ast;
|
||||
},
|
||||
pairlist(_1, pair, _2, rest, _3) {
|
||||
return [pair.ast, ...rest.ast];
|
||||
},
|
||||
pair(_1, key, _2, _3, _4, value, _5) {
|
||||
pairannotations(entries) {
|
||||
return entries.ast;
|
||||
},
|
||||
annotationentry(_1, annotation, _2, _3) {
|
||||
return annotation.ast;
|
||||
},
|
||||
annotation(_at, name, argsIter) {
|
||||
const annotObj = { name: name.ast };
|
||||
const argsArr = argsIter.ast;
|
||||
if (argsArr.length > 0) {
|
||||
annotObj.value = argsArr[0];
|
||||
}
|
||||
return annotObj;
|
||||
},
|
||||
annotationname(chars) {
|
||||
return chars.sourceString;
|
||||
},
|
||||
annotationsinglequotedarg(_open, chars, _close) {
|
||||
return chars.sourceString;
|
||||
},
|
||||
annotationdoublequotedarg(_open, chars, _close) {
|
||||
return chars.sourceString;
|
||||
},
|
||||
annotationunquotedarg(chars) {
|
||||
return chars.sourceString;
|
||||
},
|
||||
annotationargvalue(alt) {
|
||||
return alt.ast;
|
||||
},
|
||||
annotationmultilinetextblock(_1, content, _2) {
|
||||
const lines = content.sourceString.split('\n');
|
||||
// NOTE: the number 4 is taken from the `multilinetextblock` implementation
|
||||
let minIndent = 4;
|
||||
const dedented = lines.map((line) => (line.trim() === '' ? '' : line.substring(minIndent)));
|
||||
if (dedented.length > 0 && dedented[0] === '') dedented.shift();
|
||||
if (dedented.length > 0 && dedented[dedented.length - 1] === '') dedented.pop();
|
||||
return dedented.join('\n');
|
||||
},
|
||||
annotationargscontents(alt) {
|
||||
return alt.ast;
|
||||
},
|
||||
annotationargs(_open, value, _close) {
|
||||
return value.ast;
|
||||
},
|
||||
pair(_1, annotations, _keyindent, key, _2, _3, _4, value, _5) {
|
||||
let res = {};
|
||||
if (Array.isArray(value.ast)) {
|
||||
res[key.ast] = value.ast;
|
||||
return res;
|
||||
} else {
|
||||
res[key.ast] = value.ast ? value.ast.trim() : '';
|
||||
}
|
||||
const annotationList = annotations.ast;
|
||||
if (annotationList && annotationList.length > 0) {
|
||||
res[ANNOTATIONS_KEY] = annotationList;
|
||||
}
|
||||
res[key.ast] = value.ast ? value.ast.trim() : '';
|
||||
return res;
|
||||
},
|
||||
esc_quote_char(_1, quote) {
|
||||
@@ -378,9 +443,13 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
assertpairlist(_1, pair, _2, rest, _3) {
|
||||
return [pair.ast, ...rest.ast];
|
||||
},
|
||||
assertpair(_1, key, _2, _3, _4, value, _5) {
|
||||
assertpair(_1, annotations, _2, key, _3, _4, _5, value, _6) {
|
||||
let res = {};
|
||||
res[key.ast] = value.ast ? value.ast.trim() : '';
|
||||
const annotationList = annotations.ast;
|
||||
if (annotationList && annotationList.length > 0) {
|
||||
res[ANNOTATIONS_KEY] = annotationList;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
assertkey(chars) {
|
||||
|
||||
@@ -2,6 +2,10 @@ const ohm = require('ohm-js');
|
||||
const _ = require('lodash');
|
||||
const { safeParseJson, outdentString } = require('./utils');
|
||||
|
||||
// this is done to avoid breaking existing pairlist mapping so
|
||||
// the key is hidden and not added into the json automatically
|
||||
const ANNOTATIONS_KEY = Symbol('annotations');
|
||||
|
||||
const grammar = ohm.grammar(`Bru {
|
||||
BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
|
||||
auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth1 | authOAuth2 | authwsse | authapikey | authOauth2Configs
|
||||
@@ -24,10 +28,27 @@ const grammar = ohm.grammar(`Bru {
|
||||
multilinetextblockdelimiter = "'''"
|
||||
multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter
|
||||
|
||||
// Annotation support (decorators on pairs)
|
||||
annotationname = annotationchar+
|
||||
annotationchar = ~("(" | ")" | " " | "\\t" | "\\r" | "\\n" | ":") any
|
||||
annotationsinglequotedargchar = ~"'" any
|
||||
annotationsinglequotedarg = "'" annotationsinglequotedargchar* "'"
|
||||
annotationdoublequotedargchar = ~"\\"" any
|
||||
annotationdoublequotedarg = "\\"" annotationdoublequotedargchar* "\\""
|
||||
annotationunquotedargchar = ~")" any
|
||||
annotationunquotedarg = annotationunquotedargchar*
|
||||
annotationargvalue = annotationsinglequotedarg | annotationdoublequotedarg | annotationunquotedarg
|
||||
annotationmultilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter
|
||||
annotationargscontents = annotationmultilinetextblock | annotationargvalue
|
||||
annotationargs = "(" annotationargscontents ")"
|
||||
annotation = "@" annotationname annotationargs?
|
||||
annotationentry = st* annotation ~":" st* nl
|
||||
pairannotations = annotationentry*
|
||||
|
||||
// Dictionary Blocks
|
||||
dictionary = st* "{" pairlist? tagend
|
||||
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
|
||||
pair = st* (quoted_key | key) st* ":" st* value st*
|
||||
pair = st* pairannotations st* (quoted_key | key) st* ":" st* value st*
|
||||
disable_char = "~"
|
||||
quote_char = "\\""
|
||||
esc_char = "\\\\"
|
||||
@@ -87,12 +108,12 @@ const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
|
||||
return _.map(pairList[0], (pair) => {
|
||||
let name = _.keys(pair)[0];
|
||||
let value = pair[name];
|
||||
const rawAnnotations = pair[ANNOTATIONS_KEY];
|
||||
|
||||
if (!parseEnabled) {
|
||||
return {
|
||||
name,
|
||||
value
|
||||
};
|
||||
const result = { name, value };
|
||||
if (rawAnnotations && rawAnnotations.length) result.annotations = rawAnnotations;
|
||||
return result;
|
||||
}
|
||||
|
||||
let enabled = true;
|
||||
@@ -101,11 +122,11 @@ const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
enabled
|
||||
};
|
||||
const result = { name, value, enabled };
|
||||
if (rawAnnotations && rawAnnotations.length) {
|
||||
result.annotations = rawAnnotations;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -143,9 +164,56 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
pairlist(_1, pair, _2, rest, _3) {
|
||||
return [pair.ast, ...rest.ast];
|
||||
},
|
||||
pair(_1, key, _2, _3, _4, value, _5) {
|
||||
pairannotations(entries) {
|
||||
return entries.ast;
|
||||
},
|
||||
annotationentry(_1, annotation, _2, _3) {
|
||||
return annotation.ast;
|
||||
},
|
||||
annotation(_at, name, argsIter) {
|
||||
const annotObj = { name: name.ast };
|
||||
const argsArr = argsIter.ast;
|
||||
if (argsArr.length > 0) {
|
||||
annotObj.value = argsArr[0];
|
||||
}
|
||||
return annotObj;
|
||||
},
|
||||
annotationname(chars) {
|
||||
return chars.sourceString;
|
||||
},
|
||||
annotationsinglequotedarg(_open, chars, _close) {
|
||||
return chars.sourceString;
|
||||
},
|
||||
annotationdoublequotedarg(_open, chars, _close) {
|
||||
return chars.sourceString;
|
||||
},
|
||||
annotationunquotedarg(chars) {
|
||||
return chars.sourceString;
|
||||
},
|
||||
annotationargvalue(alt) {
|
||||
return alt.ast;
|
||||
},
|
||||
annotationmultilinetextblock(_1, content, _2) {
|
||||
const lines = content.sourceString.split('\n');
|
||||
let minIndent = 4;
|
||||
const dedented = lines.map((line) => (line.trim() === '' ? '' : line.substring(minIndent)));
|
||||
if (dedented.length > 0 && dedented[0] === '') dedented.shift();
|
||||
if (dedented.length > 0 && dedented[dedented.length - 1] === '') dedented.pop();
|
||||
return dedented.join('\n');
|
||||
},
|
||||
annotationargscontents(alt) {
|
||||
return alt.ast;
|
||||
},
|
||||
annotationargs(_open, value, _close) {
|
||||
return value.ast;
|
||||
},
|
||||
pair(_1, annotations, _2, key, _3, _4, _5, value, _6) {
|
||||
let res = {};
|
||||
res[key.ast] = value.ast ? value.ast.trim() : '';
|
||||
const annotationList = annotations.ast;
|
||||
if (annotationList && annotationList.length > 0) {
|
||||
res[ANNOTATIONS_KEY] = annotationList;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
quoted_key(disabled, _1, chars, _2) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const ohm = require('ohm-js');
|
||||
const _ = require('lodash');
|
||||
|
||||
// this is done to avoid breaking existing pairlist mapping so
|
||||
// the key is hidden and not added into the json automatically
|
||||
const ANNOTATIONS_KEY = Symbol('annotations');
|
||||
|
||||
// Env files use 4-space indentation for multiline content
|
||||
// vars {
|
||||
// API_KEY: '''
|
||||
@@ -28,10 +32,27 @@ const grammar = ohm.grammar(`Bru {
|
||||
multilinetextblock = multilinetextblockstart multilinetextblockcontent multilinetextblockend
|
||||
multilinetextblockcontent = (~multilinetextblockend any)*
|
||||
|
||||
// Annotation support (decorators on pairs)
|
||||
annotationname = annotationchar+
|
||||
annotationchar = ~("(" | ")" | " " | "\\t" | "\\r" | "\\n" | ":") any
|
||||
annotationsinglequotedargchar = ~"'" any
|
||||
annotationsinglequotedarg = "'" annotationsinglequotedargchar* "'"
|
||||
annotationdoublequotedargchar = ~"\\"" any
|
||||
annotationdoublequotedarg = "\\"" annotationdoublequotedargchar* "\\""
|
||||
annotationunquotedargchar = ~")" any
|
||||
annotationunquotedarg = annotationunquotedargchar*
|
||||
annotationargvalue = annotationsinglequotedarg | annotationdoublequotedarg | annotationunquotedarg
|
||||
annotationmultilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter
|
||||
annotationargscontents = annotationmultilinetextblock | annotationargvalue
|
||||
annotationargs = "(" annotationargscontents ")"
|
||||
annotation = "@" annotationname annotationargs?
|
||||
annotationentry = st* annotation ~":" st* nl
|
||||
pairannotations = annotationentry*
|
||||
|
||||
// Dictionary Blocks
|
||||
dictionary = st* "{" pairlist? tagend
|
||||
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
|
||||
pair = st* key st* ":" st* value st*
|
||||
pair = st* pairannotations st* key st* ":" st* value st*
|
||||
key = keychar*
|
||||
value = multilinetextblock | valuechar*
|
||||
|
||||
@@ -54,17 +75,18 @@ const mapPairListToKeyValPairs = (pairList = []) => {
|
||||
return _.map(pairList[0], (pair) => {
|
||||
let name = _.keys(pair)[0];
|
||||
let value = pair[name];
|
||||
const rawAnnotations = pair[ANNOTATIONS_KEY];
|
||||
let enabled = true;
|
||||
if (name && name.length && name.charAt(0) === '~') {
|
||||
name = name.slice(1);
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
enabled
|
||||
};
|
||||
const result = { name, value, enabled };
|
||||
if (rawAnnotations && rawAnnotations.length) {
|
||||
result.annotations = rawAnnotations;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -128,9 +150,56 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
pairlist(_1, pair, _2, rest, _3) {
|
||||
return [pair.ast, ...rest.ast];
|
||||
},
|
||||
pair(_1, key, _2, _3, _4, value, _5) {
|
||||
pairannotations(entries) {
|
||||
return entries.ast;
|
||||
},
|
||||
annotationentry(_1, annotation, _2, _3) {
|
||||
return annotation.ast;
|
||||
},
|
||||
annotation(_at, name, argsIter) {
|
||||
const annotObj = { name: name.ast };
|
||||
const argsArr = argsIter.ast;
|
||||
if (argsArr.length > 0) {
|
||||
annotObj.value = argsArr[0];
|
||||
}
|
||||
return annotObj;
|
||||
},
|
||||
annotationname(chars) {
|
||||
return chars.sourceString;
|
||||
},
|
||||
annotationsinglequotedarg(_open, chars, _close) {
|
||||
return chars.sourceString;
|
||||
},
|
||||
annotationdoublequotedarg(_open, chars, _close) {
|
||||
return chars.sourceString;
|
||||
},
|
||||
annotationunquotedarg(chars) {
|
||||
return chars.sourceString;
|
||||
},
|
||||
annotationargvalue(alt) {
|
||||
return alt.ast;
|
||||
},
|
||||
annotationmultilinetextblock(_1, content, _2) {
|
||||
const lines = content.sourceString.split('\n');
|
||||
let minIndent = 4;
|
||||
const dedented = lines.map((line) => (line.trim() === '' ? '' : line.substring(minIndent)));
|
||||
if (dedented.length > 0 && dedented[0] === '') dedented.shift();
|
||||
if (dedented.length > 0 && dedented[dedented.length - 1] === '') dedented.pop();
|
||||
return dedented.join('\n');
|
||||
},
|
||||
annotationargscontents(alt) {
|
||||
return alt.ast;
|
||||
},
|
||||
annotationargs(_open, value, _close) {
|
||||
return value.ast;
|
||||
},
|
||||
pair(_1, annotations, _2, key, _3, _4, _5, value, _6) {
|
||||
let res = {};
|
||||
res[key.ast] = value.ast ? value.ast.trim() : '';
|
||||
const annotationList = annotations.ast;
|
||||
if (annotationList && annotationList.length > 0) {
|
||||
res[ANNOTATIONS_KEY] = annotationList;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
key(chars) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const { indentString, getValueString, getKeyString, getValueUrl } = require('./utils');
|
||||
const { indentString, getValueString, getKeyString, getValueUrl, serializeAnnotations } = require('./utils');
|
||||
const jsonToExampleBru = require('./example/jsonToBru');
|
||||
|
||||
const enabled = (items = [], key = 'enabled') => items.filter((item) => item[key]);
|
||||
@@ -128,7 +128,7 @@ const jsonToBru = (json) => {
|
||||
if (enabled(queryParams).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(queryParams)
|
||||
.map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -136,7 +136,7 @@ const jsonToBru = (json) => {
|
||||
if (disabled(queryParams).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(queryParams)
|
||||
.map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -147,7 +147,7 @@ const jsonToBru = (json) => {
|
||||
if (pathParams.length) {
|
||||
bru += 'params:path {';
|
||||
|
||||
bru += `\n${indentString(pathParams.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(pathParams.map((item) => `${serializeAnnotations(item.annotations)}${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
@@ -158,7 +158,7 @@ const jsonToBru = (json) => {
|
||||
if (enabled(headers).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(headers)
|
||||
.map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -166,7 +166,7 @@ const jsonToBru = (json) => {
|
||||
if (disabled(headers).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(headers)
|
||||
.map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -179,7 +179,7 @@ const jsonToBru = (json) => {
|
||||
if (enabled(metadata).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(metadata)
|
||||
.map((item) => `${item.name}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}${item.name}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -187,7 +187,7 @@ const jsonToBru = (json) => {
|
||||
if (disabled(metadata).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(metadata)
|
||||
.map((item) => `~${item.name}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}~${item.name}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -527,14 +527,14 @@ ${indentString(body.sparql)}
|
||||
|
||||
if (enabled(body.formUrlEncoded).length) {
|
||||
const enabledValues = enabled(body.formUrlEncoded)
|
||||
.map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n');
|
||||
bru += `${indentString(enabledValues)}\n`;
|
||||
}
|
||||
|
||||
if (disabled(body.formUrlEncoded).length) {
|
||||
const disabledValues = disabled(body.formUrlEncoded)
|
||||
.map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n');
|
||||
bru += `${indentString(disabledValues)}\n`;
|
||||
}
|
||||
@@ -554,8 +554,9 @@ ${indentString(body.sparql)}
|
||||
const contentType
|
||||
= item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';
|
||||
|
||||
const annotPrefix = serializeAnnotations(item.annotations);
|
||||
if (item.type === 'text') {
|
||||
return `${enabled}${getKeyString(item.name)}: ${getValueString(item.value)}${contentType}`;
|
||||
return `${annotPrefix}${enabled}${getKeyString(item.name)}: ${getValueString(item.value)}${contentType}`;
|
||||
}
|
||||
|
||||
if (item.type === 'file') {
|
||||
@@ -563,7 +564,7 @@ ${indentString(body.sparql)}
|
||||
const filestr = filepaths.join('|');
|
||||
|
||||
const value = `@file(${filestr})`;
|
||||
return `${enabled}${getKeyString(item.name)}: ${value}${contentType}`;
|
||||
return `${annotPrefix}${enabled}${getKeyString(item.name)}: ${value}${contentType}`;
|
||||
}
|
||||
})
|
||||
.join('\n')
|
||||
@@ -662,19 +663,19 @@ ${indentString(body.sparql)}
|
||||
bru += `vars:pre-request {`;
|
||||
|
||||
if (varsEnabled.length) {
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${serializeAnnotations(item.annotations)}${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalEnabled.length) {
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `${serializeAnnotations(item.annotations)}@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsDisabled.length) {
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `${serializeAnnotations(item.annotations)}~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalDisabled.length) {
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `${serializeAnnotations(item.annotations)}~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
bru += '\n}\n\n';
|
||||
@@ -688,19 +689,19 @@ ${indentString(body.sparql)}
|
||||
bru += `vars:post-response {`;
|
||||
|
||||
if (varsEnabled.length) {
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${serializeAnnotations(item.annotations)}${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalEnabled.length) {
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `${serializeAnnotations(item.annotations)}@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsDisabled.length) {
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `${serializeAnnotations(item.annotations)}~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalDisabled.length) {
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `${serializeAnnotations(item.annotations)}~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
bru += '\n}\n\n';
|
||||
@@ -712,7 +713,7 @@ ${indentString(body.sparql)}
|
||||
if (enabled(assertions).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(assertions)
|
||||
.map((item) => `${item.name}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}${item.name}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -720,7 +721,7 @@ ${indentString(body.sparql)}
|
||||
if (disabled(assertions).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(assertions)
|
||||
.map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const { indentString, getValueString, getKeyString } = require('./utils');
|
||||
const { indentString, getValueString, getKeyString, serializeAnnotations } = require('./utils');
|
||||
|
||||
const enabled = (items = []) => items.filter((item) => item.enabled);
|
||||
const disabled = (items = []) => items.filter((item) => !item.enabled);
|
||||
@@ -30,7 +30,7 @@ const jsonToCollectionBru = (json) => {
|
||||
if (enabled(query).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(query)
|
||||
.map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ const jsonToCollectionBru = (json) => {
|
||||
if (disabled(query).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(query)
|
||||
.map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ const jsonToCollectionBru = (json) => {
|
||||
if (enabled(headers).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(headers)
|
||||
.map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -59,7 +59,7 @@ const jsonToCollectionBru = (json) => {
|
||||
if (disabled(headers).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(headers)
|
||||
.map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${serializeAnnotations(item.annotations)}~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -379,19 +379,19 @@ ${indentString(
|
||||
bru += `vars:pre-request {`;
|
||||
|
||||
if (varsEnabled.length) {
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${serializeAnnotations(item.annotations)}${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalEnabled.length) {
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `${serializeAnnotations(item.annotations)}@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsDisabled.length) {
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `${serializeAnnotations(item.annotations)}~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalDisabled.length) {
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `${serializeAnnotations(item.annotations)}~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
bru += '\n}\n\n';
|
||||
@@ -405,19 +405,19 @@ ${indentString(
|
||||
bru += `vars:post-response {`;
|
||||
|
||||
if (varsEnabled.length) {
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${serializeAnnotations(item.annotations)}${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalEnabled.length) {
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `${serializeAnnotations(item.annotations)}@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsDisabled.length) {
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `${serializeAnnotations(item.annotations)}~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalDisabled.length) {
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `${serializeAnnotations(item.annotations)}~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
bru += '\n}\n\n';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const { getValueString, indentString } = require('./utils');
|
||||
const { getValueString, indentString, serializeAnnotations } = require('./utils');
|
||||
|
||||
const envToJson = (json) => {
|
||||
const variables = _.get(json, 'variables', []);
|
||||
@@ -8,10 +8,10 @@ const envToJson = (json) => {
|
||||
const vars = variables
|
||||
.filter((variable) => !variable.secret)
|
||||
.map((variable) => {
|
||||
const { name, value, enabled } = variable;
|
||||
const { name, value, enabled, annotations } = variable;
|
||||
const prefix = enabled ? '' : '~';
|
||||
|
||||
return indentString(`${prefix}${name}: ${getValueString(value)}`);
|
||||
return indentString(`${serializeAnnotations(annotations)}${prefix}${name}: ${getValueString(value)}`);
|
||||
});
|
||||
|
||||
const secretVars = variables
|
||||
|
||||
@@ -68,11 +68,28 @@ const getValueUrl = (url) => {
|
||||
return `'''\n${indentString(url, 2)}\n'''`;
|
||||
};
|
||||
|
||||
function serializeAnnotations(annotations) {
|
||||
if (!annotations?.length) return '';
|
||||
return (
|
||||
annotations
|
||||
.map((a) => {
|
||||
if (a.value === undefined) return `@${a.name}`;
|
||||
if (a.value.includes('\n')) {
|
||||
return `@${a.name}('''\n${indentString(a.value)}\n''')`;
|
||||
}
|
||||
const quote = a.value.includes('\'') ? '"' : '\'';
|
||||
return `@${a.name}(${quote}${a.value}${quote})`;
|
||||
})
|
||||
.join('\n') + '\n'
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
safeParseJson,
|
||||
indentString,
|
||||
outdentString,
|
||||
getValueString,
|
||||
getKeyString,
|
||||
getValueUrl
|
||||
getValueUrl,
|
||||
serializeAnnotations
|
||||
};
|
||||
|
||||
833
packages/bruno-lang/v2/tests/annotations.spec.js
Normal file
833
packages/bruno-lang/v2/tests/annotations.spec.js
Normal file
@@ -0,0 +1,833 @@
|
||||
const parser = require('../src/bruToJson');
|
||||
const jsonToBru = require('../src/jsonToBru');
|
||||
const envParser = require('../src/envToJson');
|
||||
const jsonToEnv = require('../src/jsonToEnv');
|
||||
const collectionParser = require('../src/collectionBruToJson');
|
||||
const jsonToCollectionBru = require('../src/jsonToCollectionBru');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
describe('pair annotations', () => {
|
||||
it('above-line annotations in asserts', () => {
|
||||
const input = `
|
||||
assert {
|
||||
@description('hello')
|
||||
res.status: eq 200
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.assertions).toEqual([
|
||||
{ name: 'res.status', value: 'eq 200', enabled: true, annotations: [{ name: 'description', value: 'hello' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('above-line annotation', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@description('hello')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers).toEqual([
|
||||
{ name: 'key', value: 'value', enabled: true, annotations: [{ name: 'description', value: 'hello' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('annotation without args', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@string
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers).toEqual([
|
||||
{ name: 'key', value: 'value', enabled: true, annotations: [{ name: 'string' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('multiple above-line annotations on same pair', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@string
|
||||
@description('x')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers).toEqual([
|
||||
{
|
||||
name: 'key',
|
||||
value: 'value',
|
||||
enabled: true,
|
||||
annotations: [{ name: 'string' }, { name: 'description', value: 'x' }]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('multiple above-line annotations', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@string
|
||||
@description('hello')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers).toEqual([
|
||||
{
|
||||
name: 'key',
|
||||
value: 'value',
|
||||
enabled: true,
|
||||
annotations: [{ name: 'string' }, { name: 'description', value: 'hello' }]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('no annotation — output unchanged (backward compat)', () => {
|
||||
const input = `
|
||||
headers {
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers).toEqual([{ name: 'key', value: 'value', enabled: true }]);
|
||||
expect(output.headers[0]).not.toHaveProperty('annotations');
|
||||
});
|
||||
|
||||
it('disabled pair with annotation', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@string
|
||||
~key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers).toEqual([
|
||||
{ name: 'key', value: 'value', enabled: false, annotations: [{ name: 'string' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('double-quoted annotation arg', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@description("hello")
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'hello' }]);
|
||||
});
|
||||
|
||||
it('single quote inside double-quoted annotation arg (e.g. O\'Reilly)', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@description("O'Reilly")
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'O\'Reilly' }]);
|
||||
});
|
||||
|
||||
it('double quote inside single-quoted annotation arg (e.g. say "hello")', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@description('say "hello"')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'say "hello"' }]);
|
||||
});
|
||||
|
||||
it('smoke test escaping special characters', () => {
|
||||
const input = fs.readFileSync(path.join(__dirname, './fixtures/annotations.bru'));
|
||||
const output = parser(input);
|
||||
expect(output.vars.req[0].annotations).toEqual([{ name: 'description', value: 'found in C:\\Users\\File\\Path' }]);
|
||||
expect(output.vars.req[1].annotations).toEqual([{ name: 'description', value: 'height of 2\' ' }]);
|
||||
});
|
||||
|
||||
it('unquoted annotation arg', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@version(2)
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'version', value: '2' }]);
|
||||
});
|
||||
|
||||
it('float (decimal) unquoted annotation arg', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@version(3.14)
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'version', value: '3.14' }]);
|
||||
});
|
||||
|
||||
it('empty string arg', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@description('')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: '' }]);
|
||||
});
|
||||
|
||||
it('whitespace-only string arg preserves spaces', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@description(' ')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: ' ' }]);
|
||||
});
|
||||
|
||||
it('leading and trailing whitespace in string arg is preserved', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@description(' hello ')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: ' hello ' }]);
|
||||
});
|
||||
|
||||
it('unicode characters in annotation arg', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@description('日本語')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: '日本語' }]);
|
||||
});
|
||||
|
||||
it('URL with query string in annotation arg', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@description('https://example.com/path?q=1&r=2#anchor')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'https://example.com/path?q=1&r=2#anchor' }]);
|
||||
});
|
||||
|
||||
it('colon inside annotation arg value', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@description('Content-Type: application/json')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'Content-Type: application/json' }]);
|
||||
});
|
||||
|
||||
it('email address (@ symbol) inside annotation arg value', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@description('user@example.com')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'user@example.com' }]);
|
||||
});
|
||||
|
||||
it('template variable syntax inside annotation arg value', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@description('{{baseUrl}}/endpoint')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: '{{baseUrl}}/endpoint' }]);
|
||||
});
|
||||
|
||||
it('tab character inside annotation arg value', () => {
|
||||
const input = `headers {\n @description('col1\tcol2')\n key: value\n}\n`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'col1\tcol2' }]);
|
||||
});
|
||||
|
||||
it('multiline string values', () => {
|
||||
const input = `headers {
|
||||
@description('''
|
||||
make it rain
|
||||
make it rain2
|
||||
''')
|
||||
key: value
|
||||
}`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'make it rain\nmake it rain2' }]);
|
||||
});
|
||||
|
||||
it('serializeAnnotations — multiline value uses triple-quote delimiters and roundtrips correctly', () => {
|
||||
const json = {
|
||||
meta: { name: 'test', type: 'http', seq: 1 },
|
||||
http: { method: 'get', url: 'https://example.com' },
|
||||
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: 'line one\nline two' }] }]
|
||||
};
|
||||
const bru = jsonToBru(json);
|
||||
expect(bru).toContain('@description(\'\'\'\n line one\n line two\n \'\'\')\n x-key: val'); const parsed = parser(bru);
|
||||
expect(parsed.headers[0].annotations).toEqual([{ name: 'description', value: 'line one\nline two' }]);
|
||||
});
|
||||
|
||||
it('serializeAnnotations — empty string value roundtrips correctly', () => {
|
||||
const json = {
|
||||
meta: { name: 'test', type: 'http', seq: 1 },
|
||||
http: { method: 'get', url: 'https://example.com' },
|
||||
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: '' }] }]
|
||||
};
|
||||
const bru = jsonToBru(json);
|
||||
const parsed = parser(bru);
|
||||
expect(parsed.headers[0].annotations).toEqual([{ name: 'description', value: '' }]);
|
||||
});
|
||||
|
||||
it('serializeAnnotations — URL with special chars uses single-quote delimiters', () => {
|
||||
const json = {
|
||||
meta: { name: 'test', type: 'http', seq: 1 },
|
||||
http: { method: 'get', url: 'https://example.com' },
|
||||
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: 'https://example.com?q=1&r=2' }] }]
|
||||
};
|
||||
const bru = jsonToBru(json);
|
||||
expect(bru).toContain('@description(\'https://example.com?q=1&r=2\')\n x-key: val');
|
||||
});
|
||||
|
||||
it('serializeAnnotations — template variable in value roundtrips correctly', () => {
|
||||
const json = {
|
||||
meta: { name: 'test', type: 'http', seq: 1 },
|
||||
http: { method: 'get', url: 'https://example.com' },
|
||||
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: '{{baseUrl}}/path' }] }]
|
||||
};
|
||||
const bru = jsonToBru(json);
|
||||
const parsed = parser(bru);
|
||||
expect(parsed.headers[0].annotations).toEqual([{ name: 'description', value: '{{baseUrl}}/path' }]);
|
||||
});
|
||||
|
||||
it('annotation on params:query block', () => {
|
||||
const input = `
|
||||
params:query {
|
||||
@string
|
||||
q: search
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.params).toEqual([
|
||||
{ name: 'q', value: 'search', enabled: true, type: 'query', annotations: [{ name: 'string' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('annotation on vars:pre-request block', () => {
|
||||
const input = `
|
||||
vars:pre-request {
|
||||
@description('base url')
|
||||
myVar: http://localhost
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.vars.req).toEqual([
|
||||
{
|
||||
name: 'myVar',
|
||||
value: 'http://localhost',
|
||||
enabled: true,
|
||||
local: false,
|
||||
annotations: [{ name: 'description', value: 'base url' }]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('roundtrip: bru → json → bru → json equal', () => {
|
||||
const input = `get {
|
||||
url: https://example.com
|
||||
}
|
||||
|
||||
headers {
|
||||
@description('Content type')
|
||||
content-type: application/json
|
||||
@string
|
||||
~accept: */*
|
||||
}
|
||||
`;
|
||||
const json1 = parser(input);
|
||||
const bru = jsonToBru(json1);
|
||||
const json2 = parser(bru);
|
||||
expect(json2.headers).toEqual(json1.headers);
|
||||
});
|
||||
|
||||
it('serializeAnnotations — annotation without value', () => {
|
||||
const json = {
|
||||
meta: { name: 'test', type: 'http', seq: 1 },
|
||||
http: { method: 'get', url: 'https://example.com' },
|
||||
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'string' }] }]
|
||||
};
|
||||
const bru = jsonToBru(json);
|
||||
expect(bru).toContain('@string\n x-key: val');
|
||||
});
|
||||
|
||||
it('serializeAnnotations — annotation with value', () => {
|
||||
const json = {
|
||||
meta: { name: 'test', type: 'http', seq: 1 },
|
||||
http: { method: 'get', url: 'https://example.com' },
|
||||
headers: [
|
||||
{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: 'my header' }] }
|
||||
]
|
||||
};
|
||||
const bru = jsonToBru(json);
|
||||
expect(bru).toContain('@description(\'my header\')\n x-key: val');
|
||||
});
|
||||
|
||||
it('serializeAnnotations — disabled pair with annotation', () => {
|
||||
const json = {
|
||||
meta: { name: 'test', type: 'http', seq: 1 },
|
||||
http: { method: 'get', url: 'https://example.com' },
|
||||
headers: [{ name: 'x-key', value: 'val', enabled: false, annotations: [{ name: 'string' }] }]
|
||||
};
|
||||
const bru = jsonToBru(json);
|
||||
expect(bru).toContain('@string\n ~x-key: val');
|
||||
});
|
||||
|
||||
it('serializeAnnotations — value with single quote uses double-quote delimiters (e.g. O\'Reilly)', () => {
|
||||
const json = {
|
||||
meta: { name: 'test', type: 'http', seq: 1 },
|
||||
http: { method: 'get', url: 'https://example.com' },
|
||||
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: 'O\'Reilly' }] }]
|
||||
};
|
||||
const bru = jsonToBru(json);
|
||||
expect(bru).toContain('@description("O\'Reilly")\n x-key: val');
|
||||
});
|
||||
|
||||
it('serializeAnnotations — value with double quote uses single-quote delimiters (e.g. say "hello")', () => {
|
||||
const json = {
|
||||
meta: { name: 'test', type: 'http', seq: 1 },
|
||||
http: { method: 'get', url: 'https://example.com' },
|
||||
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: 'say "hello"' }] }]
|
||||
};
|
||||
const bru = jsonToBru(json);
|
||||
expect(bru).toContain('@description(\'say "hello"\')\n x-key: val');
|
||||
});
|
||||
|
||||
it('parseAndSerialise - bru sourced roundtrip check - headers', () => {
|
||||
const input = `headers {
|
||||
@description('hello')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const parsed = parser(input);
|
||||
const output = jsonToBru(parsed);
|
||||
|
||||
expect(input).toEqual(output);
|
||||
});
|
||||
|
||||
it('parseAndSerialise - json sourced roundtrip check - headers', () => {
|
||||
const input = {
|
||||
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: 'say "hello"' }] }]
|
||||
};
|
||||
const stringified = jsonToBru(input);
|
||||
const output = parser(stringified);
|
||||
|
||||
expect(input).toEqual(output);
|
||||
});
|
||||
|
||||
it('parseAndSerialise - bru sourced roundtrip check - asserts', () => {
|
||||
const input = `assert {
|
||||
@description('make it rain')
|
||||
res.status: eq 200
|
||||
}
|
||||
`;
|
||||
|
||||
const parsed = parser(input);
|
||||
const output = jsonToBru(parsed);
|
||||
|
||||
expect(input).toEqual(output);
|
||||
});
|
||||
|
||||
it('parseAndSerialise - json sourced roundtrip check - asserts', () => {
|
||||
const input = {
|
||||
assertions: [
|
||||
{
|
||||
annotations: [{ name: 'description', value: 'hello' }],
|
||||
name: 'res.status', value: 'eq 200', enabled: true }
|
||||
]
|
||||
};
|
||||
|
||||
const parsed = jsonToBru(input);
|
||||
const output = parser(parsed);
|
||||
|
||||
expect(input).toEqual(output);
|
||||
});
|
||||
|
||||
it('paren inside single-quoted annotation arg — Token (JWT)', () => {
|
||||
const input = `headers {
|
||||
@description('Token (JWT)')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'Token (JWT)' }]);
|
||||
});
|
||||
|
||||
it('paren inside double-quoted annotation arg — Result (OK)', () => {
|
||||
const input = `headers {
|
||||
@description("Result (OK)")
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'Result (OK)' }]);
|
||||
});
|
||||
|
||||
it('multiple parens inside single-quoted annotation arg', () => {
|
||||
const input = `headers {
|
||||
@description('func(a, b) returns (c)')
|
||||
key: value
|
||||
}
|
||||
`;
|
||||
const output = parser(input);
|
||||
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'func(a, b) returns (c)' }]);
|
||||
});
|
||||
|
||||
it('roundtrip — value containing parens survives json→bru→json — Token (JWT)', () => {
|
||||
const json = {
|
||||
meta: { name: 'test', type: 'http', seq: 1 },
|
||||
http: { method: 'get', url: 'https://example.com' },
|
||||
headers: [
|
||||
{ name: 'Authorization', value: 'Bearer token', enabled: true, annotations: [{ name: 'description', value: 'Token (JWT)' }] }
|
||||
]
|
||||
};
|
||||
const bru = jsonToBru(json);
|
||||
const parsed = parser(bru);
|
||||
expect(parsed.headers[0].annotations).toEqual([{ name: 'description', value: 'Token (JWT)' }]);
|
||||
});
|
||||
|
||||
it('inline annotation on a header is rejected', () => {
|
||||
const input = `
|
||||
headers {
|
||||
@string key: value
|
||||
}
|
||||
`;
|
||||
expect(() => parser(input)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('env pair annotations', () => {
|
||||
it('above-line annotation with string arg on a var', () => {
|
||||
const input = `vars {
|
||||
@description('my api key')
|
||||
API_KEY: abc123
|
||||
}
|
||||
`;
|
||||
const output = envParser(input);
|
||||
expect(output.variables).toEqual([
|
||||
{ name: 'API_KEY', value: 'abc123', enabled: true, secret: false, annotations: [{ name: 'description', value: 'my api key' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('above-line annotation on a var', () => {
|
||||
const input = `vars {
|
||||
@deprecated
|
||||
OLD_KEY: old_value
|
||||
}
|
||||
`;
|
||||
const output = envParser(input);
|
||||
expect(output.variables).toEqual([
|
||||
{ name: 'OLD_KEY', value: 'old_value', enabled: true, secret: false, annotations: [{ name: 'deprecated' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('annotation without args on a var', () => {
|
||||
const input = `vars {
|
||||
@string
|
||||
API_KEY: abc
|
||||
}
|
||||
`;
|
||||
const output = envParser(input);
|
||||
expect(output.variables[0].annotations).toEqual([{ name: 'string' }]);
|
||||
});
|
||||
|
||||
it('multiple annotations on a var', () => {
|
||||
const input = `vars {
|
||||
@string
|
||||
@description('base url')
|
||||
BASE_URL: http://localhost
|
||||
}
|
||||
`;
|
||||
const output = envParser(input);
|
||||
expect(output.variables[0].annotations).toEqual([{ name: 'string' }, { name: 'description', value: 'base url' }]);
|
||||
});
|
||||
|
||||
it('disabled var with annotation', () => {
|
||||
const input = `vars {
|
||||
@deprecated
|
||||
~OLD_KEY: old_value
|
||||
}
|
||||
`;
|
||||
const output = envParser(input);
|
||||
expect(output.variables).toEqual([
|
||||
{ name: 'OLD_KEY', value: 'old_value', enabled: false, secret: false, annotations: [{ name: 'deprecated' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('no annotation — output unchanged (backward compat)', () => {
|
||||
const input = `vars {
|
||||
API_KEY: abc123
|
||||
}
|
||||
`;
|
||||
const output = envParser(input);
|
||||
expect(output.variables[0]).not.toHaveProperty('annotations');
|
||||
expect(output.variables[0]).toEqual({ name: 'API_KEY', value: 'abc123', enabled: true, secret: false });
|
||||
});
|
||||
|
||||
it('secret vars are unaffected by annotation support', () => {
|
||||
const input = `vars:secret [
|
||||
SECRET_KEY
|
||||
]
|
||||
`;
|
||||
const output = envParser(input);
|
||||
expect(output.variables).toEqual([{ name: 'SECRET_KEY', value: '', enabled: true, secret: true }]);
|
||||
});
|
||||
|
||||
it('serializeAnnotations in jsonToEnv — annotation without value', () => {
|
||||
const json = {
|
||||
variables: [{ name: 'API_KEY', value: 'abc', enabled: true, secret: false, annotations: [{ name: 'deprecated' }] }]
|
||||
};
|
||||
const bru = jsonToEnv(json);
|
||||
expect(bru).toContain('@deprecated\n API_KEY: abc');
|
||||
});
|
||||
|
||||
it('serializeAnnotations in jsonToEnv — annotation with value', () => {
|
||||
const json = {
|
||||
variables: [{ name: 'BASE_URL', value: 'http://localhost', enabled: true, secret: false, annotations: [{ name: 'description', value: 'base url' }] }]
|
||||
};
|
||||
const bru = jsonToEnv(json);
|
||||
expect(bru).toContain('@description(\'base url\')\n BASE_URL: http://localhost');
|
||||
});
|
||||
|
||||
it('serializeAnnotations in jsonToEnv — disabled var with annotation', () => {
|
||||
const json = {
|
||||
variables: [{ name: 'OLD_KEY', value: 'old', enabled: false, secret: false, annotations: [{ name: 'deprecated' }] }]
|
||||
};
|
||||
const bru = jsonToEnv(json);
|
||||
expect(bru).toContain('@deprecated\n ~OLD_KEY: old');
|
||||
});
|
||||
|
||||
it('parseAndSerialise - bru sourced roundtrip check - env vars', () => {
|
||||
const input = `vars {
|
||||
@description('api key')
|
||||
API_KEY: abc123
|
||||
}
|
||||
`;
|
||||
const parsed = envParser(input);
|
||||
const output = jsonToEnv(parsed);
|
||||
expect(output).toEqual(input);
|
||||
});
|
||||
|
||||
it('parseAndSerialise - json sourced roundtrip check - env vars', () => {
|
||||
const input = {
|
||||
variables: [{ name: 'API_KEY', value: 'abc123', enabled: true, secret: false, annotations: [{ name: 'description', value: 'api key' }] }]
|
||||
};
|
||||
const bru = jsonToEnv(input);
|
||||
const output = envParser(bru);
|
||||
expect(output).toEqual(input);
|
||||
});
|
||||
|
||||
it('inline annotation on an env var is rejected', () => {
|
||||
const input = `vars {
|
||||
@deprecated API_KEY: abc
|
||||
}
|
||||
`;
|
||||
expect(() => envParser(input)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('collection pair annotations', () => {
|
||||
it('above-line annotation on a header (collection)', () => {
|
||||
const input = `headers {
|
||||
@description('content type')
|
||||
content-type: application/json
|
||||
}
|
||||
`;
|
||||
const output = collectionParser(input);
|
||||
expect(output.headers).toEqual([
|
||||
{ name: 'content-type', value: 'application/json', enabled: true, annotations: [{ name: 'description', value: 'content type' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('above-line annotation on a header', () => {
|
||||
const input = `headers {
|
||||
@deprecated
|
||||
old-header: old-value
|
||||
}
|
||||
`;
|
||||
const output = collectionParser(input);
|
||||
expect(output.headers).toEqual([
|
||||
{ name: 'old-header', value: 'old-value', enabled: true, annotations: [{ name: 'deprecated' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('annotation on a query param', () => {
|
||||
const input = `query {
|
||||
@string
|
||||
q: search
|
||||
}
|
||||
`;
|
||||
const output = collectionParser(input);
|
||||
expect(output.query).toEqual([
|
||||
{ name: 'q', value: 'search', enabled: true, annotations: [{ name: 'string' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('disabled header with annotation', () => {
|
||||
const input = `headers {
|
||||
@deprecated
|
||||
~x-old: value
|
||||
}
|
||||
`;
|
||||
const output = collectionParser(input);
|
||||
expect(output.headers).toEqual([
|
||||
{ name: 'x-old', value: 'value', enabled: false, annotations: [{ name: 'deprecated' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('annotation on vars:pre-request', () => {
|
||||
const input = `vars:pre-request {
|
||||
@description('base url')
|
||||
BASE_URL: http://localhost
|
||||
}
|
||||
`;
|
||||
const output = collectionParser(input);
|
||||
expect(output.vars.req).toEqual([
|
||||
{ name: 'BASE_URL', value: 'http://localhost', enabled: true, local: false, annotations: [{ name: 'description', value: 'base url' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('annotation on vars:post-response', () => {
|
||||
const input = `vars:post-response {
|
||||
@string
|
||||
token: abc
|
||||
}
|
||||
`;
|
||||
const output = collectionParser(input);
|
||||
expect(output.vars.res).toEqual([
|
||||
{ name: 'token', value: 'abc', enabled: true, local: false, annotations: [{ name: 'string' }] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('local var (@-prefixed) is not misidentified as annotation', () => {
|
||||
const input = `vars:pre-request {
|
||||
@localVar: http://localhost
|
||||
}
|
||||
`;
|
||||
const output = collectionParser(input);
|
||||
expect(output.vars.req).toEqual([
|
||||
{ name: 'localVar', value: 'http://localhost', enabled: true, local: true }
|
||||
]);
|
||||
expect(output.vars.req[0]).not.toHaveProperty('annotations');
|
||||
});
|
||||
|
||||
it('no annotation — output unchanged (backward compat)', () => {
|
||||
const input = `headers {
|
||||
content-type: application/json
|
||||
}
|
||||
`;
|
||||
const output = collectionParser(input);
|
||||
expect(output.headers[0]).not.toHaveProperty('annotations');
|
||||
expect(output.headers[0]).toEqual({ name: 'content-type', value: 'application/json', enabled: true });
|
||||
});
|
||||
|
||||
it('serializeAnnotations in jsonToCollectionBru — header without value', () => {
|
||||
const json = {
|
||||
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'string' }] }]
|
||||
};
|
||||
const bru = jsonToCollectionBru(json);
|
||||
expect(bru).toContain('@string\n x-key: val');
|
||||
});
|
||||
|
||||
it('serializeAnnotations in jsonToCollectionBru — header with annotation value', () => {
|
||||
const json = {
|
||||
headers: [{ name: 'content-type', value: 'application/json', enabled: true, annotations: [{ name: 'description', value: 'content type' }] }]
|
||||
};
|
||||
const bru = jsonToCollectionBru(json);
|
||||
expect(bru).toContain('@description(\'content type\')\n content-type: application/json');
|
||||
});
|
||||
|
||||
it('serializeAnnotations in jsonToCollectionBru — disabled header with annotation', () => {
|
||||
const json = {
|
||||
headers: [{ name: 'x-old', value: 'val', enabled: false, annotations: [{ name: 'deprecated' }] }]
|
||||
};
|
||||
const bru = jsonToCollectionBru(json);
|
||||
expect(bru).toContain('@deprecated\n ~x-old: val');
|
||||
});
|
||||
|
||||
it('serializeAnnotations in jsonToCollectionBru — query param with annotation', () => {
|
||||
const json = {
|
||||
query: [{ name: 'q', value: 'search', enabled: true, annotations: [{ name: 'string' }] }]
|
||||
};
|
||||
const bru = jsonToCollectionBru(json);
|
||||
expect(bru).toContain('@string\n q: search');
|
||||
});
|
||||
|
||||
it('serializeAnnotations in jsonToCollectionBru — vars:pre-request with annotation', () => {
|
||||
const json = {
|
||||
vars: {
|
||||
req: [{ name: 'BASE_URL', value: 'http://localhost', enabled: true, local: false, annotations: [{ name: 'description', value: 'base url' }] }]
|
||||
}
|
||||
};
|
||||
const bru = jsonToCollectionBru(json);
|
||||
expect(bru).toContain('@description(\'base url\')\n BASE_URL: http://localhost');
|
||||
});
|
||||
|
||||
it('parseAndSerialise - bru sourced roundtrip check - collection headers', () => {
|
||||
const input = `headers {
|
||||
@description('content type')
|
||||
content-type: application/json
|
||||
}
|
||||
`;
|
||||
const parsed = collectionParser(input);
|
||||
const output = jsonToCollectionBru(parsed);
|
||||
expect(output).toEqual(input);
|
||||
});
|
||||
|
||||
it('parseAndSerialise - json sourced roundtrip check - collection headers', () => {
|
||||
const input = {
|
||||
headers: [{ name: 'content-type', value: 'application/json', enabled: true, annotations: [{ name: 'description', value: 'content type' }] }]
|
||||
};
|
||||
const bru = jsonToCollectionBru(input);
|
||||
const output = collectionParser(bru);
|
||||
expect(output).toEqual(input);
|
||||
});
|
||||
|
||||
it('parseAndSerialise - bru sourced roundtrip check - collection vars:pre-request', () => {
|
||||
const input = `vars:pre-request {
|
||||
@description('base url')
|
||||
BASE_URL: http://localhost
|
||||
}
|
||||
`;
|
||||
const parsed = collectionParser(input);
|
||||
const output = jsonToCollectionBru(parsed);
|
||||
expect(output).toEqual(input);
|
||||
});
|
||||
|
||||
it('inline annotation on a collection header is rejected', () => {
|
||||
const input = `headers {
|
||||
@string x-key: val
|
||||
}
|
||||
`;
|
||||
expect(() => collectionParser(input)).toThrow();
|
||||
});
|
||||
});
|
||||
6
packages/bruno-lang/v2/tests/fixtures/annotations.bru
vendored
Normal file
6
packages/bruno-lang/v2/tests/fixtures/annotations.bru
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
vars:pre-request {
|
||||
@description("found in C:\Users\File\Path")
|
||||
key:value
|
||||
@description("height of 2' ")
|
||||
key2:value
|
||||
}
|
||||
Reference in New Issue
Block a user